Scott Reed
+1
Nov 12, 2021
visibility 10309
star star star star star star
(6 votes)

Run Once Migration Step For Use With Commerce 14

Overview

As part of the release of the new .NET 5 version of the platform, Commerce 14 finally got rid of the Commerce Manager needed for the management of some commerce settings. Instead, now we are presented with a number of features that have been added to the standard Commerce Interface when logging in to your main web application. However, as part of this process a number of areas that were previously availible as UI editable settings in Commerce Manager have been removed. 

  • Importing and exporting catalogs
  • Adding countries and regions
  • Adding currencies
  • Working with business objects
  • Working with catalog and order meta classes fields

The recommendation now is to use the API to add these yourselves!

Challenge

So when doing a number of demos as part of my CMS 12 and Commerce 14 masterclass I was looking at the best place to add these. As first I was using an IMigrationStep as this seemed a nice approach. 

By implementing IMigrationStep with a name and description you're presented with the migration in each environment it has not run (as below) and it only ever runs once.

However it was raised to me that IMigartionStep is in the Internal namespace and therefore it's not advisable to use (Even thought I got this approach from the example code in the Alloy CMS12/Commerce 14 preview :p)

Solution

As well as the IMigrationStep there exists a Migration Step class. The naming is a little confusing because you might think this class would Inherit IMigrationStep, however that's not the case and the two work differently. You can read about the MigrationStep here https://www.jondjones.com/learn-episerver-cms/episerver-developers-tutorials/importing-content-into-episerver-programmatically/episerver-migration-steps-explained/ 

So the migration step isn't Internal, it's availible to use but it does have the limitation of running EVERY time the solution initializes. So I decided to replicate what the IMigrationStep does under the covers and use the DDS to persist a record of it running, allowing you to be sure migrations only happen once per environment. 

The code below is from the Alloy Preview so the namespaces can be refactored as you see fit

Dynamic Data Store Model

using EPiServer.Data;
using EPiServer.Data.Dynamic;

namespace EPiServer.Reference.Commerce.Site.Infrastructure.MigrationSteps
{
    [EPiServerDataStore(AutomaticallyRemapStore = true)]
    public class MigrationStepChange : IDynamicData
    {
        public Identity Id { get; set; }

        public string Name { get; set; }
    }
}

Run Once Migration Step

using System.Linq;
using EPiServer.Data.Dynamic;
using EPiServer.DataAbstraction.Migration;

namespace EPiServer.Reference.Commerce.Site.Infrastructure.MigrationSteps
{
    public abstract class RunOnceMigrationStep : MigrationStep
    {
        public string Name { get; }

        protected RunOnceMigrationStep()
        {
            Name = GetType().Name;
        }

        protected abstract void RunOnce();

        public override void AddChanges()
        {
            var store = DynamicDataStoreFactory.Instance.CreateStore(typeof(MigrationStepChange));

            var record = store.Items<MigrationStepChange>().FirstOrDefault(r => r.Name == Name);

            if (record == null)
            {
                RunOnce();
                store.Save(new MigrationStepChange { Name = Name });
            }
        }
    }
}

Example Code

The following code runs once and adds a new currency in to Commerce with the ID TST and the Name of Test Dollar

using EPiServer.DataAbstraction.Migration;

namespace EPiServer.Reference.Commerce.Site.Infrastructure.MigrationSteps
{
    public class CurrencyMigrationStep : RunOnceMigrationStep
    {
        public static readonly CurrencySetup.CurrencyConversion[] ConversionRatesSco = {
            new CurrencySetup.CurrencyConversion("TST", "Test dollar", 1m) };

        protected override void RunOnce()
        {
            var c = new CurrencySetup();
            c.CreateConversions(ConversionRatesSco);
        }
    }
}

I modified the CurrencySetup class in the Preview to make it reusable. Here is the code that works for the above example

using System;
using System.Collections.Generic;
using System.Linq;
using Mediachase.Commerce.Catalog.Managers;
using Mediachase.Commerce.Catalog.Dto;
using Mediachase.Commerce.Core;

namespace EPiServer.Reference.Commerce.Site.Infrastructure
{
    public class CurrencySetup
    {
        public class CurrencyConversion
        {
            public CurrencyConversion(string currency, string name, decimal factor)
            {
                Currency = currency;
                Name = name;
                Factor = factor;
            }

            public readonly string Currency;
            public readonly string Name;
            public readonly decimal Factor;
        }

        public static readonly CurrencyConversion[] ConversionRatesToUsd = {
            new CurrencyConversion("USD", "US dollar", 1m),
            new CurrencyConversion("SEK", "Swedish krona", 0.12m),
            new CurrencyConversion("AUD", "Australian dollar", 0.78m),
            new CurrencyConversion("CAD", "Canadian dollar", 0.81m),
            new CurrencyConversion("EUR", "Euro", 1.07m),
            new CurrencyConversion("BRL", "Brazilian Real", 0.33m),
            new CurrencyConversion("CLP", "Chilean Peso", 0.001637m),
            new CurrencyConversion("JPY", "Japanese yen", 0.008397m),
            new CurrencyConversion("NOK", "Norwegian krone", 0.128333m),
            new CurrencyConversion("SAR", "Saudi Arabian Riyal", 0.734m),
            new CurrencyConversion("GBP", "Pound sterling", 1.49m) };

        public void CreateConversions(CurrencyConversion[] CurrenciesToAdd)
        {
            EnsureCurrencies(CurrenciesToAdd);

            var dto = CurrencyManager.GetCurrencyDto();
            var workingDto = (CurrencyDto) dto.Copy();
            foreach (var conversion in CurrenciesToAdd)
            {
                var toCurrencies = CurrenciesToAdd.Where(c => c != conversion).ToList();
                AddRates(workingDto, conversion, toCurrencies);
            }
            CurrencyManager.SaveCurrency(workingDto);
        }

        private void EnsureCurrencies(CurrencyConversion[] CurrenciesToAdd)
        {
            bool isDirty = false;
            var dto = CurrencyManager.GetCurrencyDto();
            var workingDto = (CurrencyDto) dto.Copy();

            foreach (var conversion in CurrenciesToAdd)
            {
                if (GetCurrency(workingDto, conversion.Currency) == null)
                {
                    workingDto.Currency.AddCurrencyRow(conversion.Currency, conversion.Name, DateTime.Now);
                    isDirty = true;
                }
            }

            if (isDirty)
            {
                CurrencyManager.SaveCurrency(workingDto);
            }
        }

        private void AddRates(CurrencyDto dto, CurrencyConversion from, IEnumerable<CurrencyConversion> toCurrencies)
        {
            var rates = dto.CurrencyRate;
            foreach (var to in toCurrencies)
            {
                var rate = (double)(from.Factor / to.Factor);
                var fromRow = GetCurrency(dto, from.Currency);
                var toRow = GetCurrency(dto, to.Currency);
                rates.AddCurrencyRateRow(rate, rate, DateTime.Now, fromRow, toRow, DateTime.Now);
            }
        }

        private CurrencyDto.CurrencyRow GetCurrency(CurrencyDto dto, string currencyCode)
        {
            return (CurrencyDto.CurrencyRow)dto.Currency.Select("CurrencyCode = '" + currencyCode + "'").SingleOrDefault();
        }
    }
}

Conclusion

This is some simple code wrapping the MigrationStep in a DDS record check but may help some people wondering about a simple approach to working with commerce settings that no longer exist as a UI configuration and must be inserted with code.

Thanks all, feedback and suggestions in the comments!

Nov 12, 2021

Comments

Johnny Mullaney
Johnny Mullaney Nov 15, 2021 10:58 AM

Nice post Scott. I'm relived that these settings are no longer changeable via a Commerce Manager like UI.

For the more complex Market, Shipping, Payment or Tax configuration, Commerce Manager was useful as a "read only" view in double checking configuration if project onboarding or troubleshooting issues. It's not major and would be straightforward to develop helpful add on's for these areas.

Scott Reed
Scott Reed Nov 15, 2021 11:01 AM

Totally Johnny, I was thinking the same, that some of these areas will end up coming back as community projects. It's definately useful for checking as you said.

JSpencer
JSpencer Nov 17, 2021 05:40 PM

Great post Scott! How well does the migration steps handle meta class fields?

Scott Reed
Scott Reed Nov 17, 2021 05:43 PM

The migration step is just with the run once code I added a mechanism for running whatever code you want. So you'll have to look up the world developer docs for any other code changes you want to change when the application runs. My example just provides some example of Currency, atthough there is some more bits on the Alloy preview for other areas.

error Please login to comment.
Latest blogs
Finding Thomas Part 3 - The Moment of Recognition

Remember Thomas? In digital landscape, Thomas is the returning visitor who reads everything, opens every email, converts on nothing. In standard...

Ritu Madan | Jun 26, 2026

Add more scheduled job settings from the Optimizely CMS 12 admin UI -- with OptiScheduledJob.ExtraParameters

  Optimizely (EPiServer) CMS 12 ships a great scheduled-jobs framework, but it has one frustrating gap: a job has nowhere to store its own...

Binh Nguyen Thi | Jun 25, 2026

Automated Search & Navigation to Graph Migration with Claude Code

A Claude Code plugin that scans your S&N codebase, applies Graph SDK transformations, and validates the result. Install once, run one command. CMS ...

Connor Fortin | Jun 24, 2026

Migrating from Find to Graph: Lessons Learned from a Real CMS 13 Project

While migrating a search solution from Optimizely Search & Navigation (Find) to Optimizely Graph in CMS 13, I encountered several issues that were...

Binh Nguyen Thi | Jun 24, 2026