Engineering Notes

Setting Up Episerver Commerce on an Existing Optimizely CMS Project: A Complete Guide

By Ginbok7 min read

Introduction

Adding Episerver Commerce functionality to an existing Optimizely CMS installation can seem daunting, especially when dealing with database configuration, catalog content types, and permissions. This guide walks you through the entire process, including common pitfalls and their solutions.

What You'll Learn:

Prerequisites:


Step 1: Install Commerce Packages

First, add the necessary Commerce packages to your solution. If you're using Central Package Management (CPM), update your Directory.Packages.props:

<ItemGroup>
  <PackageVersion Include="EPiServer.Commerce" Version="14.42.1" />
  <PackageVersion Include="EPiServer.Commerce.Core" Version="14.42.1" />
  <PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
</ItemGroup>

Then reference them in your projects:

CmsIv.Web/CmsIv.Web.csproj:

<ItemGroup>
  <PackageReference Include="EPiServer.Commerce" />
  <PackageReference Include="Microsoft.Data.SqlClient" />
</ItemGroup>

CmsIv.Model/CmsIv.Model.csproj:

<ItemGroup>
  <PackageReference Include="EPiServer.Commerce.Core" />
</ItemGroup>

Step 2: Register Commerce Services

In your Startup.cs, add Commerce services to the service collection:

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddCmsAspNetIdentity<ApplicationUser>()
        .AddCms()
        .AddCommerce()  // Add this line
        .AddFind()
        .AddAdminUserRegistration()
        .AddEmbeddedLocalization<Startup>();
}

Step 3: Configure Database Connection

Add the Commerce database connection string to your appsettings.Development.json:

{
  "ConnectionStrings": {
    "EPiServerDB": "Server=.;Database=cmsiv;User Id=sa;Password=YourPassword;TrustServerCertificate=True",
    "EcfSqlConnection": "Server=.;Database=cmsiv-commerce;User Id=sa;Password=YourPassword;TrustServerCertificate=True"
  }
}

For production (appsettings.Production.json):

{
  "ConnectionStrings": {
    "EPiServerDB": "Server=.;Database=cmsiv;User Id=sa;Password=YourPassword;TrustServerCertificate=True",
    "EcfSqlConnection": "Server=.;Database=cmsiv-commerce;User Id=sa;Password=YourPassword;TrustServerCertificate=True"
  }
}

Note: Use Windows Authentication (Integrated Security=True) in development if your SQL Server is configured for it. Use SQL Authentication for production environments.


Step 4: Automatic Database Creation

Create an initialization module to automatically create the Commerce database if it doesn't exist:

CommerceDatabaseInitialization.cs:

using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace YourProject.Web.Initialization
{
    [InitializableModule]
    public class CommerceDatabaseInitialization : IInitializableModule
    {
        private ILogger<CommerceDatabaseInitialization> _logger;
        private IConfiguration _configuration;

        public void Initialize(InitializationEngine context)
        {
            _logger = context.Locate.Advanced.GetInstance<ILogger<CommerceDatabaseInitialization>>();
            _configuration = context.Locate.Advanced.GetInstance<IConfiguration>();

            try
            {
                var commerceConnectionString = _configuration.GetConnectionString("EcfSqlConnection");
                
                if (string.IsNullOrEmpty(commerceConnectionString))
                {
                    _logger.LogWarning("EcfSqlConnection not found. Skipping database initialization.");
                    return;
                }

                EnsureCommerceDatabaseExists(commerceConnectionString);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error during Commerce database initialization");
                throw;
            }
        }

        private void EnsureCommerceDatabaseExists(string connectionString)
        {
            var builder = new SqlConnectionStringBuilder(connectionString);
            var databaseName = builder.InitialCatalog;
            
            builder.InitialCatalog = "master";
            var masterConnectionString = builder.ConnectionString;

            using (var connection = new SqlConnection(masterConnectionString))
            {
                connection.Open();

                var checkDbCommand = connection.CreateCommand();
                checkDbCommand.CommandText = "SELECT database_id FROM sys.databases WHERE Name = @DatabaseName";
                checkDbCommand.Parameters.AddWithValue("@DatabaseName", databaseName);
                
                var databaseId = checkDbCommand.ExecuteScalar();

                if (databaseId == null)
                {
                    _logger.LogInformation($"Database '{databaseName}' does not exist. Creating...");

                    var createDbCommand = connection.CreateCommand();
                    createDbCommand.CommandText = $@"
                        CREATE DATABASE [{databaseName}]
                        COLLATE SQL_Latin1_General_CP1_CI_AS";
                    createDbCommand.ExecuteNonQuery();

                    _logger.LogInformation($"Database '{databaseName}' created successfully.");
                }
            }
        }

        public void Uninitialize(InitializationEngine context) { }
    }
}

Step 5: Create Catalog Content Types

Episerver Commerce requires catalog content types decorated with [CatalogContentType]. Create base types for your catalog:

GenericNode.cs (Category):

using EPiServer.Commerce.Catalog.ContentTypes;
using EPiServer.Commerce.Catalog.DataAnnotations;
using EPiServer.DataAnnotations;
using System.ComponentModel.DataAnnotations;

namespace YourProject.Model.Commerce
{
    [CatalogContentType(
        DisplayName = "Generic Category",
        GUID = "D8D7F6E5-4C2D-7E1D-E0F9-A8B7C6D5E4F3",
        Description = "Generic category for products")]
    public class GenericNode : NodeContent
    {
        [CultureSpecific]
        [Display(Name = "Category Name", GroupName = SystemTabNames.Content)]
        public virtual string CategoryName { get; set; }

        [CultureSpecific]
        [Display(Name = "Description", GroupName = SystemTabNames.Content)]
        public virtual XhtmlString CategoryDescription { get; set; }
    }
}

GenericProduct.cs:

[CatalogContentType(
    DisplayName = "Generic Product",
    GUID = "A1B2C3D4-E5F6-7A8B-9C0D-1E2F3A4B5C6D",
    Description = "Generic product with variants")]
public class GenericProduct : ProductContent
{
    [Display(Name = "Product Name", GroupName = SystemTabNames.Content)]
    public virtual string ProductName { get; set; }

    [Display(Name = "Brand", GroupName = SystemTabNames.Content)]
    public virtual string Brand { get; set; }

    [Display(Name = "Description", GroupName = SystemTabNames.Content)]
    public virtual XhtmlString Description { get; set; }
}

GenericVariant.cs:

[CatalogContentType(
    DisplayName = "Generic Variant",
    GUID = "1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6",
    Description = "Generic variant for products")]
public class GenericVariant : VariationContent
{
    [Display(Name = "Color", GroupName = SystemTabNames.Content)]
    public virtual string Color { get; set; }

    [Display(Name = "Size", GroupName = SystemTabNames.Content)]
    public virtual string Size { get; set; }
}

Important: GUIDs must be valid hexadecimal values (0-9, A-F). Do not use [ContentType] attribute alongside [CatalogContentType] as it causes conflicts.


Step 6: Register Catalog Content Types

Create a configuration module to register your custom types:

CommerceConfigurationModule.cs:

using EPiServer.Commerce.Catalog.ContentTypes;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
using Microsoft.Extensions.DependencyInjection;

namespace YourProject.Web.Initialization
{
    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    [ModuleDependency(typeof(EPiServer.Commerce.Initialization.InitializationModule))]
    public class CommerceConfigurationModule : IConfigurableModule
    {
        public void ConfigureContainer(ServiceConfigurationContext context)
        {
            context.Services.Configure<CatalogOptions>(config =>
            {
                config.RegisterCatalogContent<GenericNode>();
                config.RegisterCatalogContent<GenericProduct>();
                config.RegisterCatalogContent<GenericVariant>();
            });
        }

        public void Initialize(InitializationEngine context) { }
        public void Uninitialize(InitializationEngine context) { }
    }
}

Step 7: Seed Sample Data (Optional)

Create a data seeder to automatically populate your catalog:

CommerceDataSeeder.cs:

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Commerce.Initialization.InitializationModule))]
public class CommerceDataSeeder : IInitializableModule
{
    private ILogger<CommerceDataSeeder> _logger;
    private IContentRepository _contentRepository;
    private ReferenceConverter _referenceConverter;
    private IContentSecurityRepository _contentSecurityRepository;

    public void Initialize(InitializationEngine context)
    {
        _logger = context.Locate.Advanced.GetInstance<ILogger<CommerceDataSeeder>>();
        _contentRepository = context.Locate.Advanced.GetInstance<IContentRepository>();
        _referenceConverter = context.Locate.Advanced.GetInstance<ReferenceConverter>();
        _contentSecurityRepository = context.Locate.Advanced.GetInstance<IContentSecurityRepository>();

        try
        {
            EnsureCatalogRootAccess();
            SeedCommerceData();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to seed Commerce data");
        }
    }

    private void EnsureCatalogRootAccess()
    {
        var catalogRoot = _referenceConverter.GetRootLink();
        var securityDescriptor = _contentSecurityRepository.Get(catalogRoot)
            .CreateWritableClone() as IContentSecurityDescriptor;
        
        if (securityDescriptor != null)
        {
            var accessControlEntry = new AccessControlEntry(
                EveryoneRole.RoleName,
                AccessLevel.Read | AccessLevel.Create | AccessLevel.Edit | 
                AccessLevel.Delete | AccessLevel.Publish | AccessLevel.Administer);
            
            securityDescriptor.AddEntry(accessControlEntry);
            _contentSecurityRepository.Save(catalogRoot, securityDescriptor, SecuritySaveType.Replace);
        }
    }

    private void SeedCommerceData()
    {
        var catalogs = _contentRepository.GetChildren<CatalogContent>(_referenceConverter.GetRootLink());
        if (catalogs.Any())
        {
            _logger.LogInformation("Catalogs already exist. Skipping seeding.");
            return;
        }

        // Create catalog, categories, products, and variants here
        // See full implementation in the repository
    }
}

Common Errors and Solutions

Error 1: "No process is on the other end of the pipe"

Cause: SQL Server connection authentication issue.

Solution:

Error 2: "The 'SEO' tab has been defined more than once"

Cause: Duplicate tab definition conflicting with Commerce's built-in SEO tab.

Solution: Remove custom SEO tab definition from your TabNames.cs file.

Error 3: "No Catalog Content models were found"

Cause: Catalog content types not properly registered.

Solution:

Error 4: "Access denied to content -1073741823__CatalogContent"

Cause: Catalog root lacks proper access permissions.

Solution: Set security descriptor for catalog root using IContentSecurityRepository as shown in Step 7.

Error 5: "Cannot open database 'cmsiv-commerce'"

Cause: Database doesn't exist.

Solution: The CommerceDatabaseInitialization module automatically creates it. Ensure your SQL user has CREATE DATABASE permissions or dbcreator role.

Error 6: "There are incomplete migration steps"

Cause: Episerver Commerce schema not initialized.

Solution: Run the application once to let Episerver automatically initialize the Commerce database schema. The initialization happens after database creation.


Verification

After completing all steps:

  1. Run your application
  2. Navigate to: https://localhost:5000/episerver/commerce/catalog
  3. You should see:
    • Default Catalog
    • Sample categories and products (if you implemented seeding)
    • No access denied errors

Best Practices

  1. Database Creation: Use automatic initialization modules rather than manual database creation
  2. Permissions: Always set proper access rights to catalog root for Commerce UI
  3. Content Types: Create generic base types that can be extended for specific product needs
  4. Seeding: Implement data seeding for development environments to speed up testing
  5. Configuration: Use different connection strings per environment (Development, Production)

Next Steps

Now that you have Commerce set up, consider:


Conclusion

Integrating Episerver Commerce into an existing Optimizely CMS project involves several steps, but following this guide systematically will help you avoid common pitfalls. The key is ensuring proper database configuration, content type registration, and access permissions.

Remember to test thoroughly in a development environment before deploying to production, and always keep your packages updated to benefit from the latest features and security patches.

Resources:


Author's Note: This guide is based on real-world implementation experience with Optimizely CMS 12 and Episerver Commerce 14. All code samples are production-tested.

Last Updated: January 2026
Optimizely Version: CMS 12.33.1, Commerce 14.42.1
Target Framework: .NET 8.0

#optimizely#backend
← Back to Articles
How to Add Episerver Commerce to Optimizely CMS | Step-by-Step Guide with Troubleshooting - Ginbok