Migrate Catalog content properties

A colleague asked me yesterday – how do we migrate properties of catalog content. There is, unfortunately, no official way to do it. There are several unofficial ways to do it, however. Today we will explore the way I personally recommend – for its safety and backward compatible.

Let’s say we have FashionProduct with a MSRP property with type of Money, now we would want to change it to Decimal . There are a some hacky ways to do this, but all of them require direct database manipulation which we should try to avoid – if possible.

First we will need this piece of code. it was “stolen” from a colleague of mine and has been used for countless times. You probably want to bookmark it as it’ll likely be useful in the future (I should probably do so myself as I have to find it every time I need). It is a snippet to traverse the catalog structure based on the content type you’d want.

public virtual IEnumerable<T> GetEntriesRecursive<T>(ContentReference parentLink, CultureInfo defaultCulture) where T : EntryContentBase
    {
        foreach (var nodeContent in LoadChildrenBatched<NodeContent>(parentLink, defaultCulture))
        {
            foreach (var entry in GetEntriesRecursive<T>(nodeContent.ContentLink, defaultCulture))
            {
                yield return entry;
            }
        }

        foreach (var entry in LoadChildrenBatched<T>(parentLink, defaultCulture))
        {
            yield return entry;
        }
    }

    private IEnumerable<T> LoadChildrenBatched<T>(ContentReference parentLink, CultureInfo defaultCulture) where T : IContent
    {
        var start = 0;

        while (true)
        {
            var batch = _contentLoader.GetChildren<T>(parentLink, defaultCulture, start, 50);
            if (!batch.Any())
            {
                yield break;
            }

            foreach (var content in batch)
            {
                // Don't include linked products to avoid including them multiple times when traversing the catalog
                if (!parentLink.CompareToIgnoreWorkID(content.ParentLink))
                {
                    continue;
                }

                yield return content;
            }
            start += 50;
        }
    }

To make sure we don’t load to many content at once, the batch is set size 50 but that is of course configurable (up to you)!

Now the fun part, where it actually does the work. Once we have the content, we will need to actually migrate the data, it is can be simple as this

private void MigrateProperty<T>(IEnumerable<T> contents) where T: EntryContentBase
{
      var batch = new List<T>();
      foreach(var content in contents)
      {
           var writeableClone = content.CreateWriteableClone<T>();
           Transform(writeableClone);
           batch.Add(writeableClone);
      }
      _contentRepository.Publish(batch, PublishAction.SyncDraft);
}

With the Transform method you can do whatever you want with the property value. As you might just want to rename it – it can do nothing except assign value to the new property. Or in the case we mentioned at the beginning, convert Money to Decimal is an easy task (Money is the less precision version of Decimal). Note that if you convert between data types, for example from double to int , there are potential data loss, but you are probably aware of that already.

The final step is to publish the change. For performance reasons, it is probably the best that you the Publish extension method of IContentRepository and save multiple content in one batch – may of of size 50 or 100. Those will skip things like creating new versions for optimal performance. You can read it about here New simple batch saving API for Commerce | Optimizely Developer C

The remaining question is where to put it. In a perfect world, I’d say in a migration step (i.e. a class that implement IMigrationStep ), so you ensure that your data will be properly migrated before anything else run, for example your new code that access the new property, or indexing of your content after migration. But if you have a sizeable catalog, this will take time and it might not be a good idea to let your users wait for it to complete. For that, it makes senses to do this in a schedule job and when it completes, you make a switch.

Migrating properties is not an easy or quick task, but it can be done with relative ease. It also reminds us about modeling – try to get it right from beginning so we don’t have to migrate. In the end, the fastest code is the code that does not need to be run!

The do nothing SearchProvider

With Find-backed IEntrySearchService in the previous post , we can now put SearchProvider to rest. There are, however, parts of the framework that still rely on SearchManager, and it expects a configured, working SearchProvider. The Full search index job, and the Incremental search index job are two examples. To make sure we don’t break the system, we might want to give SearchManager something to chew on. A do nothing SearchProvider that is!

And we need a DoNothingSearchProvider

    public class DoNothingSearchProvider : SearchProvider
    {
        public override string QueryBuilderType => GetType().ToString();

        public override void Close(string applicationName, string scope) { }
        public override void Commit(string applicationName) { }
        public override void Index(string applicationName, string scope, ISearchDocument document) { }
        public override int Remove(string applicationName, string scope, string key, string value)
        { return 42; }

        public override void RemoveAll(string applicationName, string scope)
        {
        }
        public override ISearchResults Search(string applicationName, ISearchCriteria criteria)
        {
            return new SearchResults(new SearchDocuments(), new CatalogEntrySearchCriteria());
        }
    }

    

And a DoNothingIndexBuilder

public class DoNothingIndexBuiler : ISearchIndexBuilder
    {
        public SearchManager Manager { get; set; }
        public IndexBuilder Indexer { get; set; }

        public event SearchIndexHandler SearchIndexMessage;

        public void BuildIndex(bool rebuild) { }
        public bool UpdateIndex(IEnumerable<int> itemIds) { return true; }
    }

What remains is simply register them in your appsettings.json

                                       "SearchOptions":  {
                                                             "DefaultSearchProvider":  "DoNothingSearchProvider",
                                                             "MaxHitsForSearchResults":  1000,
                                                             "IndexerBasePath":  "[appDataPath]/Quicksilver/SearchIndex",
                                                             "IndexerConnectionString":  "",
                                                             "SearchProviders":  [
                                                              {
                                                                "Name": "DoNothingSearchProvider",
                                                                "Type": "EPiServer.Reference.Commerce.Site.Infrastructure.Indexing.DoNothingSearchProvider, EPiServer.Reference.Commerce.Site",
                                                                "Parameters": {
                                                                  "queryBuilderType": "EPiServer.Reference.Commerce.Site.Infrastructure.Indexing.DoNothingSearchProvider, EPiServer.Reference.Commerce.Site",
                                                                  "storage": "[appDataPath]/Quicksilver/SearchIndex",
                                                                  "simulateFaceting": "true"
                                                                }
                                                              }
                                                                                 ],
                                                             "Indexers":  [
                                                                              {
                                                                                  "Name":  "catalog",
                                                                                  "Type":  "EPiServer.Reference.Commerce.Site.Infrastructure.Indexing.DoNothingIndexBuilder, EPiServer.Reference.Commerce.Site"
                                                                              }
                                                                          ]
                                                         },

And that’s it.

Delete property no longer available in code

Recently I stumped upon this question Removing a property that no longer exists in the code (optimizely.com) . it’s a valid (and even good) question. It is easy to add a new property to your catalog content type – you can simply add a new property to the model, build and start the site. However the opposite is not easy. In Commerce 14 at least.

A property for the strongly typed content type, is actually mapped and backed by a MetaField in MetaDataPlus system (of course unless you specifically tell it not to, by using IgnoreMetaDataPlusSynchronization attribute). When you add a new property to your content type, build and start your site, your content type is scanned and metafields will be created if necessary. However, if you delete a property from your content type, the scanner will just leave the metafield there. There are a few reasons for that. Firstly, it allows loosely typed content type, i.e. content types with none, or only a few property defined. If you have used some kind of external PIM, you’ll understand why it is important. Lastly, because the property can be mapped with a metafield of different name, the scanner might have trouble figuring out which metafield to delete. All in all, keeping the metafields is the sensible (if not the right) choice.

Then what to do if you want to delete the property and also clean up the metafield? With Commerce 13 and earlier, you can detach a MetaField from its MetaClass(s), then delete it using Commerce Manager. With the dead of CM in Commerce 13, what is your option?

By using code, of course. There are a few APIs – namely MetaField and MetaClass that can be used for that purpose. Note that there are two MetaField and MetaClass, and only the ones in Mediachase.MetaDataPlus.Configurator namespace are what we want (the others are for Business Foundation)

Enough for chit chat, this is the code that you would need to run

        private void DeleteMetaField(string metafieldName)
        {
            var metaField = MetaField.Load(CatalogContext.MetaDataContext, metafieldName);
            if (metaField == null)
            {
                return;
            }
            foreach (int metaClassId in metaField.OwnerMetaClassIdList)
            {
                var metaClass = MetaClass.Load(CatalogContext.MetaDataContext, metaClassId);
                if (metaClass == null)
                {
                    metaClass.DeleteField(metafieldName);
                }
            }
            MetaField.Delete(CatalogContext.MetaDataContext, metaField.Id);
        }

It is pretty straightforward. We load the MetaField by its name, if it is not null, then we remove it from all MetaClass that are using it, then eventually delete it.

In beginning of this post we mentioned strongly typed content type, but note that order system also uses the same metaclass/metafield system, so this code can be used for them as well.

This piece of code can be used in an admin-privilege controller to delete metafields on demand. Until Commerce 14 allows you to do it with a proper UI.

Getting being delete variant codes

I got the question here Content Events and Service API | Optimizely Developer Community , and CHKN contacted Optimizely developer support service which is the good/right thing to do. However it could be beneficial for a wider audience, hence this blogpost.

If you are developing new Optimizely Commerce Cloud now, you should be using the latest version (CMS 12/ Commerce 14), or at least Commerce 13.31 if you have to stay with .NET 4.8. You then could use the IContentEvents to listen to any events that might be fired from Service API.

However, if you are using older versions, you might be limited to lower level, non content event through EventContext. It works, but with a catch: there is no EntryDeleting event in EventContext. At this point I’m not entirely sure why, probably just an overlook. But it’s not impossible to work around that issue.

As I suggested in the post, EntryUpdating is like a blanket event – every change to the entries goes through there. The sender is a CatalogEntryDto, which should contain information about the being deleted entries.

        private void Instance_EntryUpdating(object sender, Mediachase.Commerce.Catalog.Events.EntryEventArgs e)
        {
            var dto = sender as CatalogEntryDto;
            if (dto != null)
            {
                var rows = dto.CatalogEntry.Where(x => x.RowState == DataRowState.Deleted);
                var deletingCodes = rows.Select(x => (string)x["Code", DataRowVersion.Original]);
                //do stuffs with codes being deleted.
            }
        }

And then to listen to the event in one of your IInitializationModule

EventContext.Instance.EntryUpdating += Instance_EntryUpdating;

However, there is a caveat here: as EntryUpdating happens before the entry deletion, it is possible that the change did not go through (i.e. the changes were to be reverted). It is unlikely, but it’s a possibility. You might accept that, or you can:

  • Store the id and code in a “Deleting entry” dictionary
  • Listen to EntryDeleted event and match from DeletedEntryEventArgs parameter (which contains EntryId ) to get the deleted Code, and continue from there.

Commerce relation(ship), a story

There are two big types of relations in Episerver (Optimizely) B2C Commerce: relations between entries and nodes, and between nodes. As you will most likely have to touch one or another, or both, sooner or later, this post aims to give you some understanding on how they are structured/work.

Node-Entry relation

When you add a product (or variant, or package, or bundle) to a category, you are creating a NodeEntryRelation. And there are a two types of NodeEntryRelation

  • Primary NodeEntryRelation, which means the category is counted as true parent of the entry. Each entry can only have at most one primary NodeEntryRelation (Which means it can have no primary NodeEntryRelation at all).
  • Secondary NodeEntryRelation, which means the entry is linked to the category. You do that when it makes sense for the product to be in additional categories. for example, a GoPro can be in Camera (primary category), but can also be in Sport Gears (linked). An entry can have no, or multiple secondary NodeEntryRelation.

The concept of primary NodeEntryRelation was added to Commerce 11. Before that, it’s a bit more of a guess work – the “main” category is determined by the sort order – the relation with lowest sort order is considered “main relation”. That introduces some inconsistency, which prompted the rework on that.

What is the main different between those two things? For one thing, access rights. For Commerce, you can set access rights down to categories, but not entries. The entries will inherit access rights from their true parents (i.e. primary nodes). An entry without primary node-entry relation is considered the direct children of a catalog, and inherits its access right settings.

Another smaller difference is that if you delete a category, true children entries will be deleted. Linked entries will only be detached from that category.

NodeEntryRelation can be managed fully by IRelationRepository, using the NodeEntryRelation type, and you can use a few extension methods to make it easier – for example EntryContentBase.GetCategories().

How are your actions in Catalog UI reflected on a data level:

  • When you create a new entry (product/SKU/etc.) in a category, you create a primary node-entry relation for them
  • When you move (cut then paste) an entry to a new category, you are creating a new primary node-entry relation. If the entry already has a primary node-entry relation, the new one will take over.
  • When you link/detach an entry to/from a new category, you are creating/removing a non-primary node-entry relation

Node-Node Relation

Like Node-Entry relation, a node can be a true parent of a node, or just be a “linked” parent.

Unlike Node-Entry relation, Node-Node relation is quite different that it’s separated in two places.

  • Linked nodes are represented by NodeRelation(s) (it might be interesting to know that NodeRelation is the parent class of NodeEntryRelation. The interpretation is that a NodeRelation is – a relation of a node, which can be with another node, or an entry)
  • There is no primary NodeRelation, the true parent node is identified by a property of the category itself. When you have a NodeContent content, then the ParentLink property points to the true parent.

For that reason, a node will always have a true parent, either a catalog, or a node. You can’t use IRelationRepository (and therefore, ServiceAPI) to manage (delete or update) a true parent of a node , you would have to:

  • Set its ParentLink to something else
  • Use IContentRepository.Move to move it to a new parent.

Note that this is the limitation of the Relations API in ServiceAPI. You can technically change the ParentLink of a node and update via POST. It’s just more work and not as intuitive as the Relations API.

Why the disparity, you might ask? Well, a lot of design decisions in Commerce comes from historical reasons, and after that, constrained resources (time/man power) and priority. While the disparity is probably not the best thing you want, it still works fairly well, and if you understand the quirk then it is all well.

Export catalog, with linked assets

If you are already using a PIM system, you can stop reading!

If you have been using Commerce for a while, you probably have seen this screen – yes, in Commerce Manager

This allow you to export a catalog, but without a caveat: the exported catalog, most likely, does not contains any linked assets. The reason for that was the asset content types need to be present at the context of the site. In Commerce Manager, the general advice is to not deploy the content types there for simpler management.

Import/Export are also missing features in Catalog UI compared to Commerce Manager. I wish I could have added it, but given my Dojo skill, it’s better to write something UI-less, and here you go: a controller to let you download a catalog with everything attached. Well, here is the entire code that you can drop into your project and build:

using EPiServer.Data;
using EPiServer.Framework.Blobs;
using EPiServer.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web.Http;
using EPiServer.Commerce.Catalog.ContentTypes;
using Mediachase.Commerce.Catalog;
using Mediachase.Commerce.Catalog.ImportExport;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Xml;

namespace EPiServer.Personalization.Commerce.CatalogFeed.Internal
{
    /// <summary>
    /// Download a catalog.
    /// </summary>
    public class CatalogExportController : ApiController
    {
        private readonly CatalogImportExport _importExport;
        private readonly IBlobFactory _blobFactory;
        private readonly IContentLoader _contentLoader;
        private readonly ReferenceConverter _referenceConverter;
        internal const string DownloadRoute = "episerverapi/catalogs/";
        private static readonly Guid _blobContainerIdentifier = Guid.Parse("119AD01E-ECD1-4781-898B-6DEC356FC8D8");

        private static readonly ILogger _logger = LogManager.GetLogger(typeof(CatalogExportController));

        /// <summary>
        /// Initializes a new instance of the <see cref="CatalogExportController"/> class.
        /// </summary>
        /// <param name="importExport">Catalog import export</param>
        /// <param name="blobFactory">The blob factory.</param>
        /// <param name="contentLoader">The content loader.</param>
        /// <param name="referenceConverter"></param>
        public CatalogExportController(CatalogImportExport importExport,
            IBlobFactory blobFactory,
            IContentLoader contentLoader,
            ReferenceConverter referenceConverter)
        {
            _importExport = importExport;
            _blobFactory = blobFactory;
            _contentLoader = contentLoader;
            _referenceConverter = referenceConverter;

            _importExport.IsModelsAvailable = true;
        }

        /// <summary>
        /// Direct download catalog export for admins.
        /// </summary>
        /// <param name="catalogName">Name of catalog to be exported.</param>
        /// <returns>
        /// Catalog.zip if successful else HttpResponseMessage containing error.
        /// </returns>
        [HttpGet]
        [Authorize(Roles = "CommerceAdmins")]
        [Route(DownloadRoute)]
        public HttpResponseMessage Index(string catalogName)
        {
            var catalogs = _contentLoader.GetChildren<CatalogContent>(_referenceConverter.GetRootLink());
            var catalog = catalogs.First(x => x.Name.Equals(catalogName, StringComparison.OrdinalIgnoreCase));
            if (catalog != null)
            {
                return GetFile(catalog.Name);
            }

            return new HttpResponseMessage
            { Content = new StringContent($"There is no catalog with name {catalogName}.") };
        }

        private HttpResponseMessage GetFile(string catalogName)
        {
            var container = Blob.GetContainerIdentifier(_blobContainerIdentifier);
            var blob = _blobFactory.CreateBlob(container, ".zip");
            using (var stream = blob.OpenWrite())
            {
                using (var zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, false))
                {
                    var entry = zipArchive.CreateEntry("catalog.xml");

                    using (var entryStream = entry.Open())
                    {
                        _importExport.Export(catalogName, entryStream, Path.GetTempPath());
                    }
                }
            }

            var response = new HttpResponseMessage
            {
                Content = new PushStreamContent(async (outputStream, content, context) =>
                {
                    var fileStream = blob.OpenRead();

                    await fileStream.CopyToAsync(outputStream)
                        .ContinueWith(task =>
                        {
                            fileStream.Close();
                            outputStream.Close();

                            if (task.IsFaulted)
                            {
                                _logger.Error($"Catalog download failed", task.Exception);
                                return;
                            }

                            _logger.Information($"Feed download completed.");

                        });
                }, new MediaTypeHeaderValue("application/zip"))
            };
            return response;
        }
    }
}

And now you can access to this path http://yoursite.com/episerverapi/catalogs?catalogName=fashion to download the catalog named “Fashion”.

A few notes:

  • This requires Admin access, for obvious reasons. You will need to log in to your website first before accessing the path above
  • It can take some time for big catalogs, so be patient if that’s the case ;). Yes another approach is to have this as a scheduled job when you can export the catalog in background, but that make the selection of catalog to export much more complicated. If you have only one catalog, then go ahead!

IContentLoader.Get(contentLink) is considered harmful for catalog content.

A while ago I wrote about how you should be aware of IContentLoader.GetChildren<T>(contentLink) here. However, that is only half of story.

IContentLoader.Get<T>(contentLink) is also considered harmful. Not in terms of it causes damage to your site (we would never, ever let that happen), nor it is slow (not unless you abuse it), but because it can behave very unexpectedly.

As you might already know, catalog content fully supports language versions, which means a catalog might have multiple languages enabled, and each and every catalog item in that catalog (node/category, and entry) will be available in those languages. However, those languages are not equal, (only) one is master language. What’s the difference then?

One of very important characteristics of that is how it affects the properties. Properties with [CultureSpecific] attribute decorated will be different in each language, and therefore, can be edited in each language. Properties without [CultureSpecific] attribute decorated will be the same in all languages, and can only be edited in master language. In Catalog UI, if you switch to non master languages, those properties will be grayed out, indicating they can’t be edited.

Now, why IContentLoader.Get<T>(contentLink) is considered harmful? Because you don’t supply a CultureInfo to let it know which version you want, it relies on the current preferred language to load the content. And if you have a catalog which has master language that is different from the current preferred language, you are loading a non-master language version. And then if you try to edit a non [CultureSpecific] property, then save it, the changes will not be saved, without error or warning.

It then will be very confusing because it sometimes works (someone changes the current preferred language that matches the catalog master language, and sometimes it doesn’t.

Which can cost you hours, if not days, to figure out what is wrong with your code.

Same thing applies to IContentLoader.TryGet<T>(contentLink)

Solution? Always use the overload that takes a CultureInfo or a LoaderOptions parameter, even if you just want to read the content. That creates a “good” habit and you can quickly spot code that might be problematic.

Use this to load master language version, if you wish to update some non CultureSpecific property.

 new LoaderOptions() { LanguageLoaderOption.MasterLanguage() }

Later versions of Commerce will log a warning if you are trying to save a non master language version with one or more changed non [CultureSpecific]properties.

Refactoring Commerce catalog code, a story

It is not a secret that I am a fan of refactoring. Clean. shorter, simpler code is always better. It’s always a pleasure to delete some code while keeping all functionalities: less code means less possible bugs, and less places to change when you have to change.

However, while refactoring can bring a lot of enjoying to the one who actually does it, it’s very hard to share the experience: most of the cases it’s very specific and the problem itself is not that interesting to the outside world. This story is an exception because it might be helpful/useful for other Commerce developer.

Continue reading “Refactoring Commerce catalog code, a story”

Speed up your catalog indexing performance – part 2

Almost two years ago I wrote part 1 here: https://vimvq1987.com/speed-catalog-entries-indexing/ on how to speed up your catalog indexing performance. If you have a fairly big catalog with frequent changes, it might take longer time than necessary to build the index incrementally. (Rebuild index, in other hands, just delete everything and rebuild from scratch, so it is not affected by the long queue in ApplicationLog). I have seen some cases where rebuilding the entire index, is actually faster than waiting for it to build incrementally.

The tip in previous blog post should work very well if you are using anything lower than Commerce 11.6, but that is no longer the case!

Continue reading “Speed up your catalog indexing performance – part 2”

Getting all non published variations

I got a question from a colleague today: A customer has multiple languages (8 of them). They need to make sure all variants are published in all languages. That is of course a reasonable request, but there is no feature builtin for such requirement. But good news is that can be done with ease. If you want to try this as practice, go ahead – I think it’s a good exercise for your Episerver Commerce-fu skills.

To do this task, we need the snippet to traverse the catalog from here https://leanpub.com/epicommercerecipes/read_sample

Continue reading “Getting all non published variations”