Chapter 3: Associations, relations and assets

If you work with CMS before - prepare to be surprised. How catalog entities are related to each other is much, much more complicated than how CMS contents are.

Associations are the connections between products, so you can cross sell, or up sell other products. For example, if you are selling a table, you would want your customers to buy a chair.

Relations are even more complicated. You have relations between nodes (how node are linked together), between nodes and entries (how entries belong to nodes), between entries (how variants belong to products, how variants belong to packages and bundles).

Assets - or what we will be talking about - is how to associate CMS assets (which are “contents”) to products and categories. Products can be boring without all images, videos, and product manuals - and that’s why we need them.

Working with associations and relations

From Optimizely Commerce 11, IRelationRepository and IAssociationRepository are the main API:s you would use to work with relations and associations, respectively.

IAssociationRepository is a quite simple interface with only three methods which are all easy to understand (which comes from the nature of Associations, as we learned in previous section). Just to remind you, an association defines two entries to have a connection - they can be sold together, customer might be suggested the alternatives.

A sample of how associations can be used to up sell your products
A sample of how associations can be used to up sell your products
 1 public interface IAssociationRepository
 2 {
 3     /// <summary>
 4     /// Gets the associations for the catalog content spe\
 5 cified by the content link.
 6     /// </summary>
 7     IEnumerable<Association> GetAssociations(ContentRefer\
 8 ence contentLink);
 9 
10     /// <summary>
11     /// Removes the associations.
12     /// </summary>
13     void RemoveAssociations(IEnumerable<Association> asso\
14 ciations);
15 
16     /// <summary>
17     /// Updates matching associations and adds new associ\
18 ations for an entry.
19     /// </summary>
20     void UpdateAssociations(IEnumerable<Association> asso\
21 ciations);
22 }

Associations and Relations groups.

When you define an association or relation, you can set “group” of it. It’s just a custom string so you can filter the associations later on. For example, you can have a association group named CrossSell to define the products which can be suggested to customers when they have a product in cart. By default, there is only one group for association (“default”) and one group for relation (well, “default”, too) in the system. Those can’t be set by the Catalog UI, only by code. You can define more association groups by adding this to your initialization module:

1     var associationDefinitionRepository =
2         context.Locate.Advanced.GetInstance<GroupDefiniti\
3 onRepository<AssociationGroupDefinition>>();
4     associationDefinitionRepository.Add(new AssociationGr\
5 oupDefinition { Name = "CrossSell" });
6     associationDefinitionRepository.Add(new AssociationGr\
7 oupDefinition { Name = "Upsell" });
8     associationDefinitionRepository.Add(new AssociationGr\
9 oupDefinition { Name = "ProOptimizelyCommerce" });

These association groups will be added if they haven’t existed in your system, otherwise nothing is change. Now when you edit an association in Catalog UI, you can choose one of those:

You can do the same with relation by using RelationGroupDefinition, however, this is not used elsewhere in the Catalog UI. All the entry relations are “default” by the way.

You can now choose between the defined association groups
You can now choose between the defined association groups

Relations in Commerce

There are essentially three types of relations between entities in Commerce: - Relations between nodes - Relations between nodes and entries - Relations between entries

Those are presented by these three types, respectively, NodeRelation, NodeEntryRelation, and EntryRelation. EntryRelation itself is an abstract class, and has 3 implementations:

  • ProductVariation is for relation between a product and its variants.
  • BundleEntry is for relation between a bundle and its content.
  • PackageEntry is for relation between a package and its content.

All classes above inherit from Relation, and are all managed by IRelationRepository

Another big change in how relations are handled in Commerce 11 is the introduction of IsPrimary property for NodeEntryRelation.

As we learned earlier, an entry can belong to multiple nodes, but only one of them is “true” parent - primary node, and others are linked. However, it is not easy to tell which is primary, and which is linked. In 10.x and earlier, the way to determine the primary node is based on the the lowest SortOrder value. That was based on a wrong assumption, and it comes with the cost of unable to sort the entries within a node. To do that, the SortOrder needs to change and it will screw up the relations.

In Commerce 11, the problem has been corrected. SortOrder is now just … sort order. Node-entry relation has now a new property named IsPrimary - and for all the node relations of an entry, only one can has that as true - and that will be the primary node - and all other ones are linked. When you upgrade to Commerce 11, the relation with lowest SortOrder will be selected to be primary (because it was supposed that way). After that, SortOrder will be truly what it is meant to be: You can drag and drop the entries in a node. Let’s say you have a category of iPhone - and there are plenty of model being sold. At this time of writing, iPhone X is the hot shot, and you would naturally want it to be on top of your category. How would you do it - drag and drop it to the top, baby

Sorry, no iPhones, I could have renamed the entries, but I want to be honest
Sorry, no iPhones, I could have renamed the entries, but I want to be honest

Assets

Products always need some assets. Images, videos, documentations such as specifications and manuals,…As a developer/an editor, you’ll need a way to work with assets.

In the old days, you manage assets via ICatalogSystem - by accessing CatalogItemAsset datatable directly. Since Commerce 7.5, everything can, and should be done via the IAssetContainer interface (which is implemented by EntryContentBase and NodeContent), and is accessible as CommerceMediaCollection.

As mentioned above, we’ll work with CommerceMediaCollection, which itself is a collection of CommerceMedia, as following:

 1 public class CommerceMedia : ICloneable
 2 {
 3     public ContentReference AssetLink { get; set; }
 4 
 5     public string AssetType { get; set; }
 6 
 7     public string GroupName { get; set; }
 8 
 9     public int SortOrder { get; set; }
10     
11     //Other methods and properties omitted for brevity.
12 }

Those properties should explain themselves. AssetLink is the ContentReference to the asset (which is supposed to be another media content), AssetType is the type of class you defined to map with the asset (we’ll talk about it later). GroupName is the group you want the asset to be, for easier management (such as “videos”, or “images”, or “manual”). SortOrder is the order they appear in the Asset tab in Catalog UI, with one convention: the first in the list will be the default one.

Assets, ordered by SortOrder
Assets, ordered by SortOrder

You can add any asset which the type implements IContentMedia, but it would be more convenient if you define your class like this:

 1 using System;
 2 using EPiServer.Commerce.SpecializedProperties;
 3 using EPiServer.DataAnnotations;
 4 using EPiServer.Framework.DataAnnotations;
 5 
 6 namespace EPiServer.Commerce.Sample.Models.Files
 7 {
 8     [ContentType(GUID = "872AA39E-5B79-43BF-B7D5-F34D4155\
 9 53BD")]
10     [MediaDescriptor(ExtensionString = "jpg,jpeg,jpe,ico,\
11 gif,bmp,png")]
12     public class ImageFile : CommerceImage
13     {
14         /// <summary>
15         /// Gets or sets the description.
16         /// </summary>
17         public virtual String Description { get; set; }
18     }
19 }

This is pure “content” stuff. The most important thing in this example class is the MediaDescriptor attribute, which allows you to specific which extensions you want to handle by this class. So in this example, any jpg, jpeg and so on files uploaded to Media gadget will be handled by ImageFile, and if you drag and drop that file into the Asset list of a catalog content, its AssetType will be EPiServer.Commerce.Sample.Models.Files.ImageFile.

There are two interesting things about CommerceImage. It has two properties like this:

1 [ImageDescriptor(Width = 256, Height = 256)]
2 public virtual Blob LargeThumbnail { get; set; }
3 
4 //Inherited
5 [ImageDescriptor(Width = 48, Height = 48)]
6 public override Blob Thumbnail { get; set; }

The first one belong to the CommerceImage itself, it defines a Blob named LargeThumbnail with the size of 256x256. Well, as you might guess, it’s the big header image whenever you edit an entry or node in Catalog UI:

Thumbnail of a catalog content
Thumbnail of a catalog content

The second one is inherited from MediaData, it defines a Blob named Thumbnail with the size of 48x48. It’s the thumbnail in the entries list of Catalogs gadget:

Catalog entry list of Catalogs gadget
Catalog entry list of Catalogs gadget

The first asset of type CommerceImage in your asset list (which has smallest SortOrder value) will be taken to generate those thumbnails. They are background-transparent, and if you don’t like the size, just override it in your class:

 1 [ContentType(GUID = "872AA39E-5B79-43BF-B7D5-F34D415553BD\
 2 ")]
 3 [MediaDescriptor(ExtensionString = "jpg,jpeg,jpe,ico,gif,\
 4 bmp,png")]
 5 public class ImageFile : CommerceImage
 6 {
 7     /// <summary>
 8     /// Gets or sets the description.
 9     /// </summary>
10     public virtual String Description { get; set; }
11 
12     [Editable(false)]
13     [ImageDescriptor(Width = 128, Height = 128)]
14     public override Blob LargeThumbnail { get; set; }
15     
16     [Editable(false)]
17     [ImageDescriptor(Width = 64, Height = 64)]
18     public override Blob Thumbnail { get; set; }
19 }

Now the thumbnail header should be only 128x128. The thumbnail list of Catalogs gadget, however, is styled by an CSS class, so it’s not resized automatically on the UI. If you want to, you’ll have to override this class yourself:

1 .Sleek .epi-thumbnailContentList.dgrid .dgrid-row .epi-th\
2 umbnail {
3     width: 48px;
4     height: 48px;
5 }

There are two limitations regarding the AssetType:

  • The full name of the type must not exceed 190 characters in length. It’s the maximum length of the column for AssetType in CatalogItemAsset table to fit into an index. CatalogContentScannerExtension will validate this during the site start up, and throw an exception if it finds any media class which does violate that rule.
  • Asset type must be resolve-able when you import or export the catalog. If you import or export your catalogs in the context of Commerce Manager, the asset mappings with entries and nodes will be skipped.

Catalog content versions

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 quite 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:

 1 var parentLink = ContentReference.Parse("1073741845__Cata\
 2 logContent");
 3 var contentRepo = ServiceLocation.ServiceLocator.Current.\
 4 GetInstance<IContentRepository>();
 5 
 6 //Unsaved content, should have status of NotCreated.
 7 var variationContent = contentRepo.GetDefault<VariationCo\
 8 ntent>(parentLink);
 9 variationContent.Name = "New variation";
10 
11 //Save the content, now it is CheckoutOut
12 var variationLink = contentRepo.Save(variationContent, Sa\
13 veAction.Save, AccessLevel.NoAccess);
14 
15 //A saved content is read only. To edit it, we must creat\
16 e a "writable" clone
17 variationContent = contentRepo.Get<VariationContent>(vari\
18 ationLink)
19     .CreateWritableClone<VariationContent>();
20 variationContent.Code = "New-variation";
21 
22 //Check in, in the UI, it's Ready to Publish, which mean \
23 the edit was complete. 
24 //The content status is now CheckedIn.
25 variationLink = contentRepo.Save(variationContent, SaveAc\
26 tion.CheckIn, AccessLevel.NoAccess);
27 variationContent = contentRepo.Get<VariationContent>(vari\
28 ationLink);
29 
30 //Oops, made a typo. reject it.
31 variationContent = contentRepo.Get<VariationContent>(vari\
32 ationLink)
33     .CreateWritableClone<VariationContent>();
34 //Now it's Rejected.
35 variationLink = contentRepo.Save(variationContent, SaveAc\
36 tion.Reject, AccessLevel.NoAccess);
37 
38 //Correct the mistake.
39 variationContent = contentRepo.Get<VariationContent>(vari\
40 ationLink).CreateWritableClone<VariationContent>();
41 variationContent.Code = "New-variation";
42 
43 //Publish it directly. Use ForceCurrentVersion flag so no\
44  new version will be created 
45 //it will overwrite the "rejected" version.
46 variationLink = contentRepo.Save(variationContent, 
47     SaveAction.Publish | SaveAction.ForceCurrentVersion, \
48 AccessLevel.NoAccess);

One thing to remember about the code above is that we used the versioned-ContentReference:s (ContentReference with WorkId bigger than 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:

1 var contentLink = ContentReference.Parse("83__CatalogCont\
2 ent");
3 var versions = contentVersionRepository.List(contentLink);
4 var previouslyPublished = versions.OrderByDescending(c =>\
5  c.Saved)
6         .FirstOrDefault(v => v.Status == VersionStatus.Pr\
7 eviouslyPublished && 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 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 Store3 was the bottleneck. First, 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 chosen because it looked quite straightforward. Each version (which was called CatalogContentDraft) is stored in one row, and except the “static” data which is supposed to be on all versions (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 every time 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. The versions of catalog contents are now stored in more or less the same way with “published” versions (from the previous section, you might already know about CatalogContentProperty), and inline with what CMS does. Version information are stored in ecfVersion table, while version properties are stored in ecfVersionProperty table. ecfVersionProperty has same “schema” as CatalogContentProperty.

Language versions.

If you have been working with CMS content - then language is an area that Catalog has significant difference compared with CMS content.

It’s fair to say that Episerver CMS is more “advanced” when it comes to language settings. CMS allows you to set a content exists in a certain language or not. However, Catalog content does not have such flexibility. There are some characteristics to keep in mind:

  • All catalog content language versions are defined by the language setting in the catalog. A content (node or entry) will exist in all languages enabled in its parent catalog. However, it does not mean that when you enable a language, versions in that language will be created automatically. The missing language versions will be created on-the-fly on demand the next time you request it.
All node and entries in this catalog will be available in English and Swedish
All node and entries in this catalog will be available in English and Swedish

Let’s take an example, your catalog was English only, then because you start selling in Sweden market, you want to add Swedish version - that can be done by enable Swedish language in CMS, and then add it to your catalog. At this point your nodes and entries still only have versions in English. When you request the Swedish language version (explicitly or implicitly), it is created on the fly, saved to database and returned to you.

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 grey out
Non CultureSpecific properties are grey out

By default, these properties are non-CultureSpecific: - Code - Name - Markets - Assets - Prices, Inventories are also not editable 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:

1 var contentLink = ContentReference.Parse("83__CatalogCont\
2 ent");
3 var content = contentRepository.Get<FashionItemContent>(c\
4 ontentLink, CultureInfo.GetCultureInfo("sv"))
5     .CreateWritableClone<FashionItemContent>();
6 //Assuming Facet_Size is non CultureSpecific.
7 content.Facet_Size = content.Facet_Size + " edited";
8 contentRepository.Save(content, DataAccess.SaveAction.Pub\
9 lish, 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, which is generally advised against). This is, however, an extreme case and I don’t expect you to do such action.

The same rule of “WorkId triumphs everything else” also applies to language. That means if you want to load a content with a WorkId points to a language which is different to the language you want to load, then the language pointed by WorkId will be loaded. 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 ICatalogSystem directly (which means, in most of the cases, catalog import export), the update will also delete all other versions, only latest, published version is kept. This setting comes in handy when you are using a PIM system to manage your catalog content, and it has some performance benefit.

The second one is UIMaxVersions. However, this setting affects 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 when you save a content. You can either set this by code:

1 EPiServer.Configuration.Settings.Instance.UIMaxVersions =\
2  1;

Or by adding uiMaxVersions (not UIMaxVersions, the attributes in siteSettings are case-sensitive) 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 SaveAction - ClearVersions. Instead of just using Save.Publish when you publish content, make sure to add the extended flag:

1 var extendedAction = SaveAction.Publish.SetExtendedAction\
2 Flag(ExtendedSaveAction.ClearVersions);

Compared to the two previous options, this option is more flexible. You can apply it on certain catalog content which match specific criteria.

Access right settings

Catalog content has basic access right settings. Compared to CMS which can set access rights to each individual content, Commerce only allows you to set access rights on Catalog and categories. Entries will inherit access rights from their “true” parents. As we learned before, that means the category which they have the NodeEntryRelation with IsPrimary set to true. If an entry has no parent node, it will inherit access right settings from the catalog.

You can either set the access rights using Catalog UI

How to set access rights for catalog content
How to set access rights for catalog content

or using the APIs, specifically IContentSecurityRepository:

1 var descriptor = _contentSecurityRepository.Get(catalogLi\
2 nk);
3 //make changes to the settings.
4 _contentSecurityRepository.Save(catalogLink, descriptor, \
5 SecuritySaveType.Replace);

Import and export catalogs

It’s fairly common that sites use an external system to manage catalog information. One of the examples is to use Product Information Management - PIM - system, then export the changes and import to Optimizely B2C Commerce site. You might have heard about PIM, like inRiver, and their connectors to export the catalog to a format that Episerver Commerce understands.

CatalogImportExport

This is the most common way to import/export catalog.

When using CatalogImportExport - exporting is always full export - so everything in the catalog will be exported. However, import can be partly, you might have catalog which only contains part of the catalog. That make senses because you don’t always update all catalog information, and importing only the changes will help you save both time and resource.

The heart of import/export catalog is CatalogImportExport, but you will mostly work with its wrapper, ImportJob. The reason we need ImportJob was because importing can be a long task and ImportJob has an async implementation with feedback - i.e. the caller can know what is the current progress - how many entries were imported etc. Unlike many other classes where you create a new instance by the inversion of control framework, like structuremap, you create a new instance of ImportJob by the constructors, using one of these constructors:

1 var importJob = new ImportJob(sourceZipFile, overwriteDup\
2 licates);

or

1 var importJob = new ImportJob(sourceZipFile, sourceXmlInZ\
2 ip, overwriteDuplicates);

or

1 var importJob = new ImportJob(sourceZipFile, sourceXmlInZ\
2 ip, overwriteDuplicates, isModelsAvailable);

Here you are passing the path to the catalog zip file, the name of the catalog file inside that zip (default is “catalog.xml”), and if you want to overwrite catalog items if existing ones found, and if the strongly typed content models are available (which will affect if the asset links will be imported or not). The next step is just to run it:

1 importJob.Execute(addMessageAction, cancellationToken);

addMessageAction is an Action<IBackgroundTaskMessage> so you can have a callback to your method to decide what to do with the messages the importer sends to you (either display them to the UI, log them, or just ignore), and cancellationToken is a CancellationToken so you can cancel the job (only has effect if the import job is extracting the zip file).

To export catalog, it’s even simpler:

1 var importExport = new CatalogImportExport();
2 using(FileStream fs = new FileStream(filePath, FileMode.C\
3 reate, FileAccess.ReadWrite))
4 {
5     importExport.Export(CatalogName, fs, folderPath); 
6 }

This will export your selected catalog (via CatalogName) to the filePath in folderPath. Normally, you would want filePath to have file name of Catalog.xml. You can then zip this file to make it easier to store, or to transfer, something like this

1 ZipFile.CreateFromDirectory(folderPath, String.Concat(fol\
2 derPath, ".zip"));
3 var stream = new FileStream(String.Concat(folderPath, ".z\
4 ip"), FileMode.Open);
5 return File(stream, "application/octet-stream", catalogOu\
6 tputName + ".zip");

CSV Import

CatalogImportExport is not the only way to import the changes from an external source - you can also use CSV for such task. However while a Catalog.xml is very self-contained and has everything you need for a catalog, CSV files are very simple. So you would need an extra part - a mapping file to let Commerce knows which column in CSV maps to which field in catalog.

For example here’s a mapping rule:

1 <RuleItem><SourColumnName>Code</SourColumnName><SourColum\
2 nType>System.String</SourColumnType>
3 <DestColumnName>Code</DestColumnName><DestColumnType>NVar\
4 Char</DestColumnType>
5 <FillType>CopyValue</FillType><CustomValue />
6 <DestColumnSystem>True</DestColumnSystem></RuleItem>

You can define and change these rules by Commerce Manager:

Update mapping rules
Update mapping rules

Creating mapping rules might not be the most interesting part of your job, but fortunately you only have to do it once - as long as your CSV format does not change.

 1 var dataContext = CatalogContext.MetaDataContext.Clone();
 2 dataContext.UseCurrentThreadCulture = false;
 3 dataContext.Language = "en";
 4 var mappingMetaClass = new EntryMappingMetaClass(dataCont\
 5 ext, metaClassName, 1);
 6 Rule mappingRule = Rule.XmlDeserialize(dataContext, mappi\
 7 ngFilePath);
 8 char textQualifier = '\0';
 9 if (mappingRule.Attribute["TextQualifier"].ToString() != \
10 "")
11 {
12     textQualifier = char.Parse(mappingRule.Attribute["Tex\
13 tQualifier"]);
14 }
15 IIncomingDataParser parser = null;
16 System.Data.DataSet rawData = null;
17 parser = new CsvIncomingDataParser(sourceFolder, true, ch\
18 ar.Parse(mappingRule.Attribute["Delimiter"].ToString()), \
19 textQualifier, true, Encoding.Default);
20 rawData = parser.Parse(Path.GetFileName(csvFilePath), nul\
21 l);
22 System.Data.DataTable dtSource = rawData.Tables[0];
23 string userName = Thread.CurrentPrincipal.Identity != nul\
24 l ? Thread.CurrentPrincipal.Identity.Name : String.Empty;
25 return mappingMetaClass.FillData(FillDataMode.All, dtSour\
26 ce, mappingRule, userName, DateTime.UtcNow);

CSV can only be used for import, there is no built in way to export data to a CSV. But that would be easy to do so, as CSV is an universal, well known format.

Batch API

As discussed in previous section, CatalogImportExport is the most common way to import catalog changes into Optimizely Commerce Cloud, mostly due to its simplicity and performance. If you have a PIM system with connector to export the changes as a catalog (and you don’t care about versions), it’s probably the most performant way you can get. But if you don’t, and if you want to have more control of the “importing” process, writing code to publish catalog content using content API is the most common way. But then you quickly realize it’s not the best way to do it, the process is usually slow. Updating a few hundred products a day is usually OK, but if you need to make changes to few thousands ones every house, the system can’t seem to keep up with it. In most cases you will get about 1 product/second which is too slow.

Then the batch saving API comes to rescue. Introduced in Commerce 13.10 as an extension of IContentRepository, the batch APIs fill the gap of performance. It is a very simple one, you take a list of CatalogContentBase, and a flag that indicates if you want to sync the changes to the draft versions or not, and that it

1 public static void Publish(this IContentRepository conten\
2 tRepository, IEnumerable<IContent> contents, PublishActio\
3 n publishAction)

The performance gain varies case by case, but it’s reasonable to expect a 5-10x gain.

How could it be fast? To understand that, we need to understand why IContentRepository.Save is slow. If you profile your code with that API (which you really should, it’s important to understand your code’s characteristics), you will see that a lot of time is actually spending in validation. Combining with multiple database roundtrips, there is a performance wall that IContentRepository.Save can’t break even if you optimize everything else.

Publish avoids those issues by doing … none. It bypasses the publishing pipeline almost entirely.

While it’s not the complete replacement of Save, it’s the best thing you can try if you do not care about versions, and performance is your priority.

Performance considerations

A frequently asked question I have received is that “Can Optimizely Commerce Cloud support our catalog”. While the number of entries (products/SKUs) in your catalog is definitely a major factor, it is so much more than that. Catalog size is more than just the number of entries, but also the number of languages, the number of versions, number of metafields, how you structure your catalog, and how do you CRUD them. It’s pretty complicated topic to begin with.

Optimize your catalog structure

As the most frequently hit entities your system, Catalogs have a big impact of performance for your website - so put some thoughts into organizing it properly will definitely help.

There are a couple of things to consider:

  • Do not put too many entries in one node. This has been greatly improved in Commerce 9, but still, Catalog UI will still struggle to manage that many items. It was simply not designed to handle a lot of entries in a same category. Performance, both backend and frontend will suffer.
This warning will appear if your node has more than 2000 entries
This warning will appear if your node has more than 2000 entries

By default, Catalog UI will display a node in a product - variations structure, variations will appear as children of products. However if the number of entries in a node reaches 2000, then it will only display flat structure. The threshold can be changed by SimplifiedCatalogListingThreshold value in AppSettings.

There is no definitive value for the optimal number of entries per node, it totally depends on your catalog, but speaking from experience, you might want to try to limit it to less than 2000. Anything bigger than that and you should be thinking about adding sub-categories.

  • Think about which properties to put in a product, and what to put in its variations. Sometimes it’s crystal clear to do so, sometimes it’s not, so take some considerations for this, with priority given to product. Having multiple variations to share same metafields will reduce the number of rows we have to load from CatalogContentProperty table. Less rows mean better performance. That might not sound a lot, but imagine you have 100.000 variations, in 10 languages. Each metafield less means 1.000.000 less in database. It can be some thing.
  • Same goes to culture-specific and non culture-specific metafields. Culture-specific fields will be shared across all languages, so check if you need a field to be culture-specific. (Of course, if your site is going to have many languages. If it has only 2-3 languages then the difference is very small, thought.)

Optimize your catalog operations

In previous section we talked about how to structure your catalog for the best performance. But that’s only the half of the picture - what even more important is how you work with your catalog.

As we learned before, by default, the catalog content is cached - which is a good thing, because the next time you need it, Episerver Framework will happily load it from memory, save you both precious time and disk I/O. However, if your catalog content is only loaded for one use, cache is not helping here. And remember your server’s memory is limited - and one instance of catalog content is not that small, depends on your modeling, it can be easily 10MB or 20MB. No cache can live forever. It will, sooner or later, be removed for new catalog content. The more that happens, the worse it is for performance - even worse if Garbage Collector needs to kick in often. If you are continuously loading and discarding catalog contents from cache, then you are probably doing something wrong.

As a rule of thumb, only load the content when you absolutely need it. I’ve seen more than often when the developers feel generous to load more than they need - and in the end, the site struggles when it has to serve multiple concurrent users.

Here’s something to keep in mind. If you can - do a review of your projects and fix the following problems. The performance gain might surprise you:

  • You don’t have to load an entire content just to get its code. That’s what ReferenceConverter is for - a new method GetCode(ContentReference) was added recently and was even improved in Commerce 10+. It’s much more faster and memory-efficient than a content. This is also true when you want to get, for example, ContentReference of all variations of a product. You don’t have to get all the VariationContent - IRelationRepository) can provide the lightweight solution for you. The general rule is - if you are not using the entire content, but just a small part of it, then look around. The framework might already provide something for you, with (much) better performance.
  • Keep in mind that UrlResolver.GetUrl also load contents, and not just one - it loads content recursively until it finds the configured root (which we will discuss more in part 3 - Advanced Stuffs). Sometimes, call to UrlResolver.GetUrl is inevitable, but in many cases, you can cache it …somewhere. If you are using Episerver Find (or even better, Find.Commerce), then the content url can be included in the index and reused for later calls.
  • If you are using IContentLoader.GetChildren for the product listing page, you are probably doing it wrong. Thing is, when a customer visits a product listing page, they unlikely click on every product - just one or two out of ten. Loading all of the children, even with paging, is not a good idea. A common best practice is to avoid loading the content until you needs to - and avoid loading multiple content in product listing page. That’s what the search provider (or Find/Find.Commerce) is for. We will talk more about this is part 5 - Searching.
  • Last but definitely not least, using batch APIs is arguably the best way to improve your catalog operation performance. If you are loading catalog content one by one inside a foreach loop, chances are you are doing it wrong. Ask yourself if the catalog content could be loaded by IContentRepository.GetItems instead. You might be surprised with the performance improvement! Also, as discussed in previous section, using IContentRepository.Publish can dramatically improve performance.