How Episerver Catalog content versions work

This is an excerpt from my book – Pro Episerver Commerce. Use to get 20% off the book during Easter holiday (Mar 24th – Mar 28th)

One of the most important features in CatalogContentProvider is it bring versioning to catalog content. It was somewhat limited with Commerce 7.5 (the languages handling was a bit sloppy), but it has been much more mature since Commerce 9. The versioning system in Commerce 9 is now more or less on par with versioning in CMS, and it’s a good thing.

If you’re new to Episerver CMS/Commerce, then it might be useful to know how version and save action work in content system. Of course, you can skip this section if you already know about it. The version status is defined in EPiServer.Core.VersionStatus. When you save a content, you have to pass a EPiServer.DataAccess.SaveAction to IContentRepository.Save method.

The documentation for those enum:s are pretty good, and the combinations of SaveActions can be quite complicated, but we can consider a basic case so you’ll get the idea:

var parentLink = ContentReference.Parse("1073741845__CatalogContent");
var contentRepo = ServiceLocation.ServiceLocator.Current.GetInstance<IContentRepository>();
//Unsaved content, should have status of NotCreated.
var variationContent = contentRepo.GetDefault(parentLink);
variationContent.Name = "New variation";
//Save the content, now it is CheckoutOut
var variationLink = contentRepo.Save(variationContent, SaveAction.Save, AccessLevel.NoAccess);
//A saved content is readonly. To edit it, we must create a "writable" clone
variationContent = contentRepo.Get<VariationContent>(variationLink)
variationContent.Code = "New-varation";
//Check in, in the UI, it's Ready to Publish, which mean the edit was complete.
//The content status is now CheckedIn.
variationLink = contentRepo.Save(variationContent, SaveAction.CheckIn, AccessLevel.NoAccess);
variationContent = contentRepo.Get(variationLink);
//Oops, made a typo. reject it.
variationContent = contentRepo.Get<VariationContent>(variationLink)
//Now it's Rejected.
variationLink = contentRepo.Save(variationContent, SaveAction.Reject, AccessLevel.NoAccess);
//Correct the mistake.
variationContent = contentRepo.Get<VariationContent>(variationLink).CreateWritableClone<VariationContent>();
variationContent.Code = "New-variation";
//Publish it directly. Use ForceCurrentVersion flag so no new version will be created
//it will overwrite the "rejected" version.
variationLink = contentRepo.Save(variationContent,
SaveAction.Publish | SaveAction.ForceCurrentVersion, AccessLevel.NoAccess);

One thing to remember about the code above is that we used the versioned-ContentReference:s (WorkId > 0). By default, if you pass a ContentReference without version to IContentRepository.Get<T>, you will get back the published version (of the master language), or the CommonDraft version if there is no published version available. With WorkId, a specific version is returned (given that version exists).

One fundamental change in Commerce 9 versioning is the uniqueness of WorkID, as we mentioned earlier. Prior to Commerce 9, WorkID is only unique for a specific content, but now it’s unique across system. It does mean from the WorkID, you can know anything, from the content itself to the version you’re pointing to. This also means WorkId triumphs everything else. So for some reasons, you get your ContentReference wrong, such as the ID points to a content, but the WorkId points to a version belongs to another content, then the WorkId wins, and the content returned is the content WorkId points to (In Commerce 8, the content ID wins). That’s why you should always make sure you get the correct version of ContentReference. If you want to load a version with a specific status without knowing its WorkId, make sure can use IContentVersionRepository. For example, to get the latest “Previously Published” version in English:

var contentVersionRepository = ServiceLocator.Current.GetInstance<IContentVersionRepository>();
var contentLink = ContentReference.Parse("83__CatalogContent");
var versions = contentVersionRepository.List(contentLink);
var previouslyPublished = versions.OrderByDescending(c => c.Saved)
.FirstOrDefault(v => v.Status == VersionStatus.PreviouslyPublished && v.LanguageBranch == "en");

The unique WorkId across system is, again, consistent with CMS. The other changes, comes at a much lower level – database.

Versioning was the reason catalog system in Commerce 7.5 was significant slower than Commerce R3. The non-version parts (ICatalogSystem and MetaDataPlus) are still fast, but the implementation of versioning in Dynamic Data Store[*] was the bottleneck. Firstly, DDS was was not designed to handle a “store” with multiple millions of rows. Secondly, the queries are generated automatically and they are less than optimal to access data.

The idea for storage was pretty simple and perhaps was choosen because it looked quite straightforward. Each version (which was call CatalogContentDraft) is stored in one row, and except the “static” data which is supposed to be on all version (such as content link, code, etc), all properties (aka IContent.Property) are serialized and stored in a big column of NVARCHAR(MAX). Compared to the way it’s stored in metadata classes, this was of course slower and contribute to the slowness of system as well.

And having data in two places means you have to sync it everytime you save, which adds even more overheads to the system.

To improve the performance, those issues must be addressed: DDS must be ditched, serialization should be minimized and synchronization should be reduced. All were done in Commerce 9, by rewriting the versioning storage entirely.

[*]: You can read more about Dynamic Data Store here. If you want to take a look at how catalog versions were stored (Given you have Commerce 8 databases), check tblBigTable in CMS database. The catalog content drafts are in CatalogContentDraftStore store.

Language versions.

CMS content in general, has the concept of master language. For catalog content, the master language of products is defined by the default language of their catalog. When a property is decorated with CultureSpecific attribute (aka its corresponding metafield has MultiLanguageValue = false), it is supposed to save in master language, and is shared between other languages. Those properties are not editable when you edit non-master language version:

Non CultureSpecific properties are greyout
Non CultureSpecific properties are grey out

By default, these properties are non-CultureSpecific:

  • Code
  • Name
  • Markets
  • Assets
  • Prices, Inventories are also uneditable in non master languages

That goes the same when you update the content with API:s. This code will not throw any error, but it does not really save anything to database:

var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
var contentLink = ContentReference.Parse("83__CatalogContent");
var content = contentRepository.Get<VariationContent>(contentLink, CultureInfo.GetCultureInfo("sv"))
//Assuming Facet_Size is non CultureSpecific.
content.Facet_Size = content.Facet_Size + " edited";
contentRepository.Save(content, DataAccess.SaveAction.Publish, EPiServer.Security.AccessLevel.Publish);

When a content is loaded, how are its properties treated? If the language is different from master language: CultureSpecific properties are loaded from that language, but non-CultureSpecific properties are loaded from master language. That’s why Catalog content always requires master language version to be published before publishing any other versions.

This also comes with a caveat: You should not change the default language of a catalog, otherwise all non-CultureSpecific property will be lost (until you want to get your hands dirty and update directly in database). This is, however, an extreme case and I don’t expect you to do such action. Episerver Commerce team is aware of this issue and newer versions of Commerce might fix it.

The same rule of “WorkId triumphs everything else” also applies to language. IContentRepository and IContentVersionRepository both provide methods for you to get a specific version of a content.

Version settings

There are two important settings for version in Commerce:

The first one is DisableVersionSync. This can be set by a key in appSettings section. If this setting is true, when you update catalog content from lower level than CatalogContentProvider, such as using ICatalogSystem directly, the update will also delete all other versions (only latest, published version is kept). This setting comes in handy when you don’t not want to keep the old versions in the system, only latest, published one is needed. This is pretty much the same behavior with R3 where no versions existed. When you don’t need versioning, this might be one way to improve performance.

The second one is UIMaxVersions. However, this setting will affect both catalog content and CMS content, so think carefully before using it. This setting allows you to set the maximum number of versions per content you want to keep, if the number is reached, the oldest version would be trimmed. You can either set this by code:
EPiServer.Configuration.Settings.Instance.UIMaxVersions = 1;
Or by adding uiMaxVersions attribute to siteSettings in episerver.config.

If you don’t set the value, by default both CMS and Commerce will keep 20 most recent versions of each catalog content. It’s a best practice to set this to a specific value of your choice to keep your version history manageable (and in long term, maintain performance, too many versions might be bad for performance).

Another option to clear old versions is to use the extended flag for SaveActionClearVersions. Instead of just using Save.Publish when you publish content, make sure to add the extended flag:
var extendedAction = SaveAction.Publish.SetExtendedActionFlag(ExtendedSaveAction.ClearVersions);
Note that compared to two previous options, this option is more flexible. You can apply it on certain catalog content which match specific criteria.

4 thoughts on “How Episerver Catalog content versions work”

  1. Hi Quan, a little off topic but…

    If you edit Code in the CMS (like the screenshoot you have) in master language, then it doesn’t get updated in other languages. I guess that is a bug as it should get updated in all languages? If you edit the code in Commerce Manager it get updated in all languages.

  2. Hi Sebastian,
    You are correct – it should, the Code is non CultureSpecific then all languages share the same Code. I just checked on latest version and it does not seem to act correctly with Code. Name, in other hands, works well. I’ll file a bug for this.
    Thank you for bringing this on.

Leave a Reply

Your email address will not be published. Required fields are marked *