Configuring Customers for Multi-Commerce-Site Solutions

Overview

In this post I will be taking you through the configuration and implementation required to enable separate customer accounts per site in a multi-commerce-site solution.

Note: We won't go through the process of adding a second commerce storefront in this post.

Understanding Customer Accounts

Let us first understand how customer accounts are associated to a site using the Sitecore Retail Demo as our reference solution.

First of all, we review the Sitecore configurations, via http://retail.dev.local/sitecore/admin/showconfig.aspx, for pipelines that create users.  We see that the commerce.customers.createUser pipeline employs the SitecoreUserRespository, which handles all CRUD operations for users. Notably, the domain parameter of the repository, 'CommerceUsers' , is utilised to construct the user's name in the format <domain>\<website user name>.

This is great news!

We have just identified that customer accounts are registered with a domain, so our next step is to identify how to configure the domain per site, so that users don't share the same account and login details across sites in a multi-site solution.

Now, we could update the domain on the repository configuration, but this only solves the issue of updating the domain in a single domain implementation. We need to investigate the 'CommerceUsers' domain further.

Further Investigation and Implementation

Looking back into the Sitecore configurations, we want to identify other usages instances of 'CommerceUsers' to determine if they will be relevant to our customisation. We can see that this domain is associated to the following:-

  1. Profile import
    <sitecore>
      <commerceServer>
        <ProfileImport patch:source="CommerceServer.Profiles.config">
          <setting name="FieldForUserNameGeneration" value="GeneralInfo.email_address"/>
          <setting name="EmailAddressFieldName" value="GeneralInfo.email_address"/>
          <setting name="ProfileUniqueQueryKey" value="GeneralInfo.user_id"/>
          <setting name="ErrorThreshold" value="10"/>
          <setting name="Domain" value="CommerceUsers"/>
          <setting name="OutFile" value=""/>
          <setting name="MaxDegreeOfParallelism" value="4"/>
        </ProfileImport>
      </commerceServer>
    </sitecore>
    
  2. Site
    <sitecore>
      <sites>
        <site name="storefront" targetHostName="retail.dev.local" hostName="retail|storefront" commerceShopName="storefront" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content/storefront" startItem="/Home" dictionaryPath="/sitecore/content/storefront/global/dictionary" dictionaryAutoCreate="false" placeholderSettingsRoot="/sitecore/layout/Placeholder Settings/Project/Retail" mailTemplatesRoot="/sitecore/content/Storefront/Global/Mails" domain="CommerceUsers" allowDebug="true" htmlCacheSize="50MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="25MB" filteredItemsCacheSize="10MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" cacheRenderingParameters="true" renderingParametersCacheSize="10MB" formsRoot="/sitecore/system/Modules/Web Forms for Marketers/Retail" loginPage="/login" enableItemLanguageFallback="true" patch:source="z.Sitecore.Demo.Retail.DevSettings.config" database="master" cacheHtml="false"/>
      </sites>
    </sitecore>
    
  3. Sitecore user repository
    <sitecore>
      <sitecoreUserRepository type="Sitecore.Commerce.Data.Customers.SitecoreUserRepository, Sitecore.Commerce" singleInstance="true" patch:source="Sitecore.Commerce.Customers.config">
        <param ref="entityFactory"/>
        <param desc="serializationProperty">__Serialization</param>
        <param desc="domain">CommerceUsers</param>
        <param desc="role">User Role</param>
      </sitecoreUserRepository>
    </sitecore>
    
  4. Profile provider for the Commerce Server
    <sitecore>
      <switchingProviders>
        <profile>
          <provider providerName="cs" storeFullNames="true" wildcard="%" domains="CommerceUsers" patch:source="CommerceServer.Profiles.config"/>
        </profile>
      </switchingProviders>
    </sitecore>
    

Profile Import

The profile import can be identified as out of scope for the base minimum requirements of setting up separate customer accounts per site, so we will leave this as an exercise to be completed separately leveraging the knowledge obtained from this investigation and implementation.

Site

Logically, this is the starting point for unique site configurations, but without the initial investigation developers would most likely fall into the trap of assuming the site's domain attribute was the source domain used in creating users.

As we know that we should utilise the site configured domain when creating users, we don't have to change the value of the domain for now. We will need to keep in mind that SitecoreUserRepository should be retrieving the value from the site configuration and not the static parameter it currently utilises.

Sitecore User Repository

In our initial investigation we determined that this repository was using the domain parameter to construct the db user's name , however we didn't identify how we could replace the parameter with the site domain attribute.

Taking a look closer at how the domain parameter is utilised within the repository, we see that there is a virtual method GetDomain() which we can override to return the site's domain. Let's do this now.

The Sitecore.Foundation.Multisite project seems to be the appropriate place to house the implementation to override the GetDomain() method as the project itself and the customisation we are about to implement are both agnostic of site-specific details.

The following snippet of codes shows how trivial updating the domain is by simply returning the site's domain name. (You will need to reference the Sitecore.Commerce.dll if the project does not already include it)

using Sitecore.Commerce.Entities;

namespace Sitecore.Foundation.MultiSite.Infrastructure.Pipelines
{
  public class SitecoreUserRepository : Sitecore.Commerce.Data.Customers.SitecoreUserRepository
  {
    public SitecoreUserRepository(IEntityFactory entityFactory, string serializationProperty, string domain, string role)
      : base(entityFactory, serializationProperty, domain, role)
    {
    }

    protected override string GetDomain()
    {
      return Context.Domain.Name;
    }
  }
}

We follow this up by registering class in the config

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <sitecoreUserRepository type="Sitecore.Commerce.Data.Customers.SitecoreUserRepository, Sitecore.Commerce">
      <patch:attribute name="type">Sitecore.Foundation.MultiSite.Infrastructure.Pipelines.SitecoreUserRepository, Sitecore.Foundation.MultiSite</patch:attribute>
    </sitecoreUserRepository>
  </sitecore>
</configuration>

We publish our changes, check the showconfig page to ensure the deploy was successful, and register a user (andrew2@test.com) with the debugger attached at a break point with our custom code as a sanity check that the code is being executed. Success!

If we change the domain in our site config to 'DomainA' we should expect to see a user registered under the new domain, right? Let's check.

Register: The external id for the user default\andrew3@test.com is empty.

What happened? Well, upon investigation there are 2 issues here. The first is that the user was created in our Sitecore core database dbo.aspnet_Users table, however the user was not created as a commerce user in the Commerce Server profiles database dbo.UserObject table. The second is that the domain name does not match the 'DomainA' we applied and instead has been set to 'default'.

As we still need to look at the configuration for Profile provider for the Commerce Server, let's start there.

Profile provider for the Commerce Server

Following on from missing user in the Commerce Server profile database and our earlier investigation, we determined that the Commerce Server configuration has not been updated to accept profiles from a domain other than 'CommerceUsers', so let's update the attribute to 'DomainA' and see what happens.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <switchingProviders>
      <profile>
        <provider providerName="cs">
          <patch:attribute name="domains">DomainA</patch:attribute>
        </provider>
      </profile>
    </switchingProviders>
  </sitecore>
</configuration>

Similar to the SitecoreUserRepository configuration, we have just updated the domains attribute to handle a single domain solution, however the domains attribute can be configured with a comma-separated list of values, so no additional customisation is required other than to set the additional domains as required. i.e.

<patch:attribute name="domains">DomainA,DomainB</patch:attribute>

Configuring Domains

We appear to have missed something. In \Website\App_Config\Security folder, the Domains.config contains the registration for domains. In this instance we can just update 'CommerceUsers' directly to 'DomainA' in the domains configuration, or copy the domain definition and change the domain name appropriately. We add the file to our solution, publish and test again.

Note: Domains can also be maintained in the Domain Manager, but by adding the file to our solution, we can ensure that all developers domain configurations are automated and kept in sync via solution deployments.

<?xml version="1.0" encoding="utf-8"?>
<domains xmlns:sc="Sitecore">
  <domain name="sitecore" ensureAnonymousUser="false" />
  <domain name="extranet" />
  <domain name="default" isDefault="true" />
  <sc:templates>
    <domain type="Sitecore.Security.Domains.Domain, Sitecore.Kernel">
      <ensureAnonymousUser>true</ensureAnonymousUser>
      <locallyManaged>false</locallyManaged>
    </domain>
  </sc:templates>
  <domain name="DomainA" defaultProfileItemId="{0FFEC1BD-4D35-49CF-8B7D-7F01930834ED}" ensureAnonymousUser="false" />
  <domain name="CommerceCustomers" />
  <domain name="modules"/>
  <domain name="habitat"/>
  <domain name="DomainB" defaultProfileItemId="{0FFEC1BD-4D35-49CF-8B7D-7F01930834ED}" ensureAnonymousUser="false" />
</domains>

Success!

We are able to register users under a different domain that is associated with the site via configuration of the domain attribute. The accounts have also been confirmed in the Sitecore core database dbo.aspnet_Users table, and the Commerce Server profiles database dbo.UserObject table.

In the future, if we want to change the domain we only need update the site's domain attribute and ensure the domain is registered in the Domains.config.

Summary

In wrapping up, we managed to get a better understanding of how customer accounts are associated to a site (by domain, not directly tied to a site), we were able to identify and implement the customisations required in order to support a multi-commerce-site solution, and we also ended up getting our solution into a state we can manage adding and assigning domains via config to be deployed with the solution without any manual steps outside of configuration.