With Optimizely having taken the leap to .NET 5 in September with Content Cloud version 12 and Commerce Cloud 14, there are many changes to consider, especially when it comes to commerce setup. As we mentioned before, these new versions mean a huge boost in performance and will positively affect your bottom line. However, they also mean that some things have to be done differently from now on. One example is the use of migration steps.
Overview
As part of the release of the new .NET 5 version of the platform, Optimizely 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 available as UI editable settings in Commerce Manager have been removed. Namely, these are:
- 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
When doing a number of demos as part of my Optimizely 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 are 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 IMigrationStep is in the Internal namespace and therefore it is not advisable to use, even though I got this approach from the example code in the Alloy CMS12/Commerce 14 preview.
Solution
In addition to the IMigrationStep, there is also a Migration Step class. The naming is a little confusing, because you might think this class would Inherit IMigrationStep, but that is not the case and the two work differently. You can read about the Migration Step class here.
So, the migration step is not Internal, it is available 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 it 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.
To find out more about the intricacies of Optimizely and how it can meet your business needs, browse our case studies and reach out to Niteco today. You can read more of Scott’s insights into Optimizely on Optimizely World.
to transform your business and drive results?