Blog about tips & tricks for CMS enhancement

eric.petersson

Update every item's Code value in Episerver Commerce Catalog


This post contains some tips & tricks for migrating an existing PIM system's code id:s into an existing Episerver Commerce solution.

A recent client of mine had upgraded their current PIM-system (inRiver) for importering product information into my client's Episerver Commerce solution. The product information in that PIM-system was migrated, but not the system entity id:s. This reflected all previous imported entries for the Commerce solution as well as the existing import job as a scheduled job in custom Episerver business logic.

Here are some pitfalls and concerns to avoid when doing similiar migrating task.

1. Make sure you get a mapped ID dictionary of some kind

If your current provider of the PIM-system is planning to migrate existing PIM data but lacks the compability to get the system id:s from the old system into the new, consider asking for a mapping dictionary with the new and old ids associated to each other.

In my client's approach they were moving from inRiver on-prem to inRiver Cloud and therefor could easily give a list of the old and new ids, like below (provided in json-format):

[{"OnPremId":118410,"iPMCId":357}]

In an Episerver Scheduled job you could create a similiar read method to get all old/new mappings:

 private string ReadInRiverUpdatedSystemEntityIdFile(string filePath)
{
    try
    {
        OnStatusChanged($"Reading file...");
        using (StreamReader reader = new StreamReader(filePath))
        {
            return _jsonContent = reader.ReadToEnd();
        }
    }
    catch (Exception ex)
    {
        _log.Error($"Could not retrieve json feed from inRiver System Entity Id file", ex);
    }
    return string.Empty;
}
    
private IEnumerable<InRiverEntity> GetInRiverEntities()
{
    if (string.IsNullOrEmpty(_jsonContent))
    {
        return Enumerable.Empty<InRiverEntity>();
    }
    var inRiverEntities = JsonConvert.DeserializeObject<List<InRiverEntity>>(_jsonContent, new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore
});
    return inRiverEntities;
}

2. Get all Catalogues, Products and Variants separetaly

Further down in the steps it will be a more convenient way to migrate each different section of Commerce separetaly. First we will load all catalogues, then all dependent products to that catalogue and at last all dependent variants to that product.

Therefor your logic to retrieve every item in Commerce should look something like this:

Get all catalogues in Episerver Commerce

 private IEnumerable<CatalogContentBase> GetExistingCatalogueEntries()
{
    var defaultRootCatalog = _referenceConverter.GetRootLink();
    var defaultCatalog = _contentRepository.GetChildren<CatalogContent>(defaultRootCatalog)
        .FirstOrDefault(c => c.Name == "DefaultCatalog");
    var catalogueEntries = _contentRepository.GetChildren<CatalogContentBase>(fagerhultDefaultCatalog.ContentLink, new LoaderOptions
    {
        LanguageLoaderOption.MasterLanguage()
    });
    return catalogueEntries != null && catalogueEntries.Any() ? catalogueEntries : Enumerable.Empty<CatalogContentBase>();
}

Get all products in Episerver Commerce (dependent upon GetExistingCatalogueEntries above)

private IEnumerable<ProductModel> GetExistingProductEntries()
{
    var products = new List<ProductModel>();
    var families = GetExistingCatalogueEntries();
    foreach (var catalogue in catalogues)
    {
        if (catalogue == null)
        {
            continue;
        }
        var productEntries = _contentRepository.GetChildren<ProductModel>(catalogue.ContentLink, new LoaderOptions
        {
            LanguageLoaderOption.MasterLanguage()
        });
        foreach (var product in productEntries)
        {
            if (product == null)
            {
                continue;
            }
            products.Add(product);
        }
    }
    return products != null && products.Any() ? products : Enumerable.Empty<ProductModel>();
}

Get all variants in Episerver Commerce (dependent upon GetExistingProductEntries above)

private IEnumerable<string> GetExistingItemCodes()
{
    var items = new List<string>();
    var products = GetExistingProductEntries();
    foreach (var product in products)
    {
        if (product == null)
        {
            continue;
        }
        var productVariations = _relationRepository.GetChildren<ProductVariation>(product.ContentLink);
        foreach (var item in productVariations)
        {
            var itemCode = _referenceConverter.GetCode(item.Child);
            if (itemCode == null)
            {
                continue;
            }
            items.Add(itemCode);
        }
    }
    return items != null && items.Any() ? items : Enumerable.Empty<string>();
}

3. Update every Catalogue/Product/Variant's existing Code temporarily

Once you've got all the old items to be updated, you need to double check that an existing Product code in the Commerce solution doesn't already exist. For instance, an old Commerce id with the value of 100 could also exist as the new imported one. Therefor, temporarily set all existing Commerce items to be T(and your existing code). T as in Temporary.

Here's the example:

public void TemporaryUpdateExistingProducts()
{
    var products = GetExistingProductEntries();
    foreach (var product in products)
    {
        try
        {
            if (_stopSignaled)
            {
                OnStatusChanged("Stop signal called! Exiting job...");
                break;
            }
            var oldCode = _referenceConverter.GetCode(product.ContentLink);
            OnStatusChanged($"Temporary updating existing Product ({product.DisplayName}) with the following old Code: {oldCode}.");
            var clone = product.CreateWritableClone() as ProductModel;
            if (clone == null)
            {
                continue;
            }
            clone.Code = $"T{clone.Code}";
            _contentRepository.Save(clone, SaveAction.ForceCurrentVersion, AccessLevel.NoAccess);
        }
        catch (Exception ex)
        {
            _log.Error($"Error when updating Product to new system entity id: {ex}");
        }
    }
}

This should of course also be made for the catalogue and the item. The example above is for products.

4. Update every Catalogue/Product/Variant to the new Code

So now that we have temporarily set every existing item in Commerce not to be in conflict with our new codes (system entity ids), we may begin the real deal and set the old codes to the new ones.

Yet again another example:

public void UpdateExistingProductsToNewCode()
{
    var products = GetExistingProductEntries();
    var inRiverEntities = GetInRiverEntities();
    foreach (var product in products)
    {
        try
        {
            if (_stopSignaled)
            {
                OnStatusChanged("Stop signal called! Exiting job...");
                break;
            }
            var oldCode = _referenceConverter.GetCode(product.ContentLink);
            OnStatusChanged($"Updating existing Product ({product.DisplayName}) with the following old Code: {oldCode}.");
            // find the legacy code by comparing inRiverEntity with Commerce Product's old code id
            var newEntity = inRiverEntities.SingleOrDefault(i => $"T{i.OnPremId.ToString()}" == oldCode);
            if (newEntity != null)
            {
                var clone = product.CreateWritableClone() as ProductModel;
                var newCode = newEntity.IPMCId;
                var newFamilyCode = Convert.ToInt32(_referenceConverter.GetCode(clone.ParentLink));
                clone.Code = newCode.ToString();
                clone.ProductFamilyId = newFamilyCode;
                clone.ProductId = newCode;
                _contentRepository.Save(clone, SaveAction.ForceCurrentVersion, AccessLevel.NoAccess);
                _updatedProducts++;
                _log.Info($"Updating existing Item ({product.DisplayName}) from old Code: {oldCode} into new code: {newCode}");
            }
        }
        catch (Exception ex)
        {
            _log.Error($"Error when updating Product to new system entity id: {ex}");
        }
    }
}

A quite tedious but safe method to take each indiviual product in the time and update seperately. This should be of course also done with the catalogues and items and items aswell.

5. Reconsider indexing to Episerver Find in another step

If your Episerver Commerce solution is dependent upon Episerver Find to display and reflect the product result on the web, than reconsider to reindex everything once everything has been migrated by your custom scheduled job, and not during the migration process since you will in that case increase the risks of timeout or error exception handlings by each indiviual item.

You may run the built in Episerver Reindex job when done migrating.

Hopefully this post has shed some light for you some pitfalls to avoid if you are about to adjust every Episerver Commerce items' code values.