Chapter 2: Catalog content modeling
The strongly typed models
Now you can have the basic ideas of how the catalog is structured - it’s time to start working with some real code. As I mentioned above, you’ll be working with IContentRepository (in some cases, with its base interface - IContentLoader), around IContent. Any content must implement IContent to be handled by the content providers - so the first thing to get the Catalog integrated into the content system (aka to get CatalogContentProvider to work) is modelling the catalog, node, and entry with IContent
Let’s see how the catalog contents are modeled
All catalog content types inherit from CatalogContentBase.
RootContent is a “virtual” content - it has no Catalog counterpart. You can only have one RootContent in the system, itsContentLink is fixed and it is always read-only. You can try to save it but nothing will be persisted. You will hardly work with RootContent directly, but with its ContentReference, via ReferenceConverter.GetRootLink().
Let’s us see a simple code of creating a Catalog, then editing its language, then deleting it. (We are just showing how the API:s work)
1 //Create a CatalogContent
2 var catalogContent = contentRepository.GetDefault<Cat\
3 alogContent>(referenceConverter.GetRootLink());
4 catalogContent.Name = "Pro Episerver Commerce";
5 catalogContent.DefaultCurrency = Currency.USD;
6 catalogContent.DefaultLanguage = "en";
7 catalogContent.WeightBase = "kgs";
8 var catalogContentLink = contentRepository.Save(catal\
9 ogContent, DataAccess.SaveAction.Publish, EPiServer.Secur\
10 ity.AccessLevel.NoAccess);
11
12 //Edit it to add Swedish
13 catalogContent = contentRepository.Get<CatalogContent\
14 >(catalogContentLink).CreateWritableClone<CatalogContent>\
15 ();
16 catalogContent.CatalogLanguages.Add("sv");
17 contentRepository.Save(catalogContent, DataAccess.Sav\
18 eAction.Publish, EPiServer.Security.AccessLevel.NoAccess)\
19 ;
20
21 //Delete it
22 contentRepository.Delete(catalogContentLink, false, E\
23 PiServer.Security.AccessLevel.NoAccess);
One thing to remember about CatalogContent is it’s different from NodeContent or EntryContentBase - it cannot be extended. While CatalogNode or CatalogEntry can be enriched by MetaClasses (as they implement IMetaClass interface), CatalogContent has no such abilities, you’ll always work with CatalogContent directly, never with its subclasses.
This will simply does not work:
1 [CatalogContentType(GUID = "1FD3E34D-6476-40E2-8CB5-8EFB0\
2 DB79E3E")]
3 public class MyCatalogContent : CatalogContent
4 {
5 public virtual string MyExtendedProperty { get; set; }
6 }
CatalogContentScannerExtension will throw an EPiServerException exception during the initialization if it detects any content types inherit from CatalogContent:
The content type ‘EPiServer.Commerce.Sample.MyCatalogContent’ extends ‘EPiServer.Commerce.Catalog.ContentTypes.CatalogContent’, which is not supported.
Working with Nodes and Entries is more or less the same. But you’ll have to model your metaclasses first.
Modeling catalog content types
It’s not required to model your metaclasses. You can use the defined classes such as NodeContent or ProductContent, and throw everything into Property properties, instead of content.VeryLongPropertyName, you can use content.Property["VeryLongPropretyName"]. (Remember? Any instance of IContent has Property property as a “property bag” to store its properties). It’s why Catalog UI (which relies on content types) will still work when you import a Catalog with metaclasses you didn’t model after. But that will defeat one of the most important benefit of content types: strongly typed types mean Intellisense and compile-time checking. Noticed the typo I intentionally made above? Visual Studio will catch that for you.
1 /// <summary>
2 /// FashionProduct class, map to Fashion_Product_Class me\
3 tadata class
4 /// </summary>
5 [CatalogContentType(GUID = "18EA436F-3B3B-464E-A526-564E9\
6 AC454C7", MetaClassName = "Fashion_Product", GroupName = \
7 "Fashion",
8 DisplayName = "Fashion product", Description = "fashi\
9 on product with Add to Cart button.")]
10 [ImageUrl("~/Templates/thumbnail-fashion.png")]
11 public class FashionProduct : ProductContent
12 {
13 [Display(Name = "Description", Order = -15)]
14 public virtual XhtmlString Info_Description { get; se\
15 t; }
16
17 [Display(Name = "Features", Order = -11)]
18 public virtual XhtmlString Info_Features { get; set; }
19
20 [Display(Name = "Facet Brand", Order = -1)]
21 [Searchable]
22 public virtual string Brand { get; set; }
23 }
Catalog Content modeling uses attributes intensively. As you might see, there are plenty to explain:
-
CatalogContentType: this is the most important attribute. It inherits from CMSContentTypeattribute, which makes this content type an option to select when you create new content in Catalog UI. GUID is something required by CMS to identify the content type, even its name and its namespace is changed. It hasMetaClassNameproperty indicate that your class is mapped to the corresponding metaclass. In case the metaclass does not exist GroupName groups your content type with other content types, so when you create a new content in CMS, they will in same group and be easier to select. - Other properties is to make the display of your content type in Catalog UI nicer and more recognizable.
-
ImageUrlthumbnail allow you to set a thumbnail to your content type.
When you define your strongly typed content, there are some rules to keep in mind:
- The property must match the name and the type of the the metafield. Casing is not important. So if you have a string metafield named
facet_color, your property must be stringFacet_Color.
- The property must be declared as virtual (An exception will be thrown if one or more properties are not declared virtual)
- As a metafield can be used across multiple metaclass, the property which maps to it also has to be identical across content types, including all of the attributes decorated. That means you cannot have a property named
Facet_Coloras string in one class and another namedFacet_Coloras double in another class. OrMaterialwhich hasCultureSpecificattribute in one content type, and without that property in another. If one of the properties has mismatched attributes across classes, an exception will be thrown during site initialization. - In most of the case, the underlying
MetaDataTypeis automatically determined, so you don’t have to do anything else. This is, however, not true with dictionary types. Both of them require you to specify a BackingType. In case ofMetaDataType.EnumSingleValueandMetaDataType.DictionarySingleValue, if you fail to do so, it’ll be created as a normal string, whileMetaDataType.EnumMultiValueandMetaDataType.DictionaryMultiValuewill throw exception during site startup.
Here’s how you create them:
1 [BackingType(typeof(PropertyDictionaryMultiple))]
2 public virtual IEnumerable<string> MultipleValue { get; s\
3 et; }
4
5 [BackingType(typeof(PropertyDictionarySingle))]
6 public virtual string SingleValue { get; set; }
These are attributes you can use to decorate the property, which also map to attribute of a metafield:
-
Required: the property cannot be null -
Searchable: the property will be indexed by the search provider -
Tokenize: the value of the property will be tokenized while indexing. So if its value is “Hello world”, whenTokenizeis set to true, then you can find this content with either “Hello” or “world”. Otherwise it will only be returned when you search for “Hello world” -
IncludeValuesInSearchResults: Same as “Include value in search results” setting for metafield. -
IncludeInDefaultSearch: same as “Include in default search” setting for metafield. -
Encrypted: Indicates that the property will be saved to database (as a metafield) encrypted. This attribute, however, does not take effect, if your database is set to be Azure-compatible. -
DecimalSettings: Allow you to set the precision and scale of a decimal type. Note that the highest precision ofdecimaltype stored in SQL Server is (38,9), so anything bigger than that will not be allowed.
When your site starts, CatalogContentScannerExtension will do all the leg works to scan the content types and synchronize them to the metadata system - content types to metaclasses and and properties to metafields. In case of any conflicts between those pairs, catalog content types win. For example, you have a metafield with decimal precision of (18,0), but the mapping property has DecimalSettings to be (33,9), CatalogContentScannerExtension will update the metafield to be (33,9), not the way around.
IDimensionalStockPlacement
Most of properties of the base content types provided by Optimizely Commerce (CatalogContent, NodeContent, ProductContent, etc.) are flat, which means you can access directly, for example:
1 nodeContent.Code = "This-is-a-code";
But the contents implement IDimensionalStockPlacement (by default VariationContent and PackageContent), are exceptions. They are accessible via a block - ShippingDimensions.
1 variationContent.ShippingDimensions.Height = 10m;
That’s why the shipping dimensions are displayed a bit differently in Catalog UI:
If you have worked with CMS, you might already know that you don’t have to initialize a block before using it - it’ll never be null for a content. So you don’t have to write code such as:
1 variationContent.ShippingDimensions = new ShippingDimensi\
2 ons();
Just use the block directly, as we showed above.
The MetaData system
It’s hard to fully understand the Catalog system without knowing about the MetaData system, with its MetaClass(es) and MetaField(s). The Catalog is just the barebone, it’s the MetaData system which bring it to life.
The idea of metaclasses and metafields is simple. To make it effective to editing your catalog, you’ll need pre-defined templates. So a shirt needs a style (tailor fit, slim fit,…), a material (silk, cotton, polyester,…), a color (red, white, blue), a texture,…, while a TV needs a size (40”, 50”, 55”,…), a resolution (HD ready, FullHD or 4k), Smart features (Netflix, Youtube, Browser), 3D or not, etc. As a framework, Episerver cannot predefine all of those things out of the box - it’s up to you to define your “templates” for your needs. So each template is a metaclass, and each attribute of the template, is a metafield.
And each instance of the template, is a MetaObject. MetaObject is, in the simplified view, just a HashSet of values.
At API:s level, there’s nothing really interesting about MetaClass(es) or MetaField(s) in particular. Today, you will be hardly working with them - you should be working with the strongly typed models instead, which will come up in next part. However, they are still the internals of the catalog system, so it’ll make sense for us to explore how they works.
Let’s take a closer looks at MetaClass. At its core, it’s a simple class with some attributes (Id, Name, TableName, Namespace, etc) and a list of MetaFields attach to it. If you take a look at the MetaClass management in Commerce Manager (Commerce Manager/Administration/Catalog System/MetaClass), it’s as close as bare metal you can get (without looking at database, of course)
The key feature of MetaData system is the extensibility. You can attach or remove a metafield from a metaclass on-the-fly. You can share metafields between metaclasses.
It’s metafield which is more interesting
What do these settings mean?
- Support multiple languages: That mean the metafield will be per-language, each language version of the MetaObject will have an unique value.
- Use in comparing: This is a legacy setting which no longer has any meaning. (It should have been removed)
- Allow Null Values: This metafield is not required.
- Encrypted: The metafield will be encrypted by the master key in database and be decrypted in the fly. This option, however, does not show up if your database is set to be Azure-compatible. (As Sql Azure Database does not support encryption at the time of this writing).
- Precision: This only set-able if your metafield is a decimal. This was important in the past because it will affect how the decimal is stored in the database, but with Commerce 9, the decimal will always be stored in the (38,9) form.
Search attributes: These attributes will affect how will this metafield be indexed in a Lucene-based search provider:
- Allow Search: This metafield will be indexed by the search providers.
- Enable Sorting Search results: The value of this metafield can be used for sort by the search providers.
- Include Value in search results:
- Tokenize: The value of this metafield will be tokenized. So if its value is “Hello World”, you can either search with “Hello” or “World”
- Include in Default Search: This metafield will be include in the default Lucene query.
If you can only choose between either CatalogEntry or CatalogNode for MetaClass, then it’s whole lot more thing to choose for metafield. The full list of MetaType which you can choose from: {lang=C#} public enum MetaDataType { Integer = 26, Boolean = 27, Date = 28, Email = 29, URL = 30, ShortString = 31, LongString = 32, LongHtmlString = 33,
1 DictionarySingleValue = 34,
2 DictionaryMultiValue = 35,
3 EnumSingleValue = 36,
4 EnumMultiValue = 37,
5 StringDictionary = 38,
6 File = 39,
7 ImageFile = 40,
8
9 MetaObject = 41
10 }
The only caveat of choosing a type is that you can’t change the type of a metafield after you saved it. So if you make a wrong move, it might be easiest to just delete the metafield and create again.
Then how MetaObjects are stored?
Here’s the map between PropertyData types and MetaField types:
| MetaDataType | PropertyData type |
|---|---|
| MetaDataType.Decimal | PropertyDecimal |
| MetaDataType.BigInt | |
| MetaDataType.Money | |
| MetaDataType.SmallMoney | |
| MetaDataType.DateTime | PropertyDate |
| MetaDataType.SmallDateTime | |
| MetaDataType.Date | |
| MetaDataType.Float | PropertyFloatNumber |
| MetaDataType.Real | |
| MetaDataType.Bit | PropertyNumber |
| MetaDataType.Char | |
| MetaDataType.Int | |
| MetaDataType.Integer | |
| MetaDataType.SmallInt | |
| MetaDataType.TinyInt | PropertyNumber |
| MetaDataType.NChar | PropertyLongString |
| MetaDataType.VarChar | |
| MetaDataType.NText | |
| MetaDataType.Text | |
| MetaDataType.NVarChar | |
| MetaDataType.LongString | |
| MetaDataType.Boolean | PropertyBoolean |
| MetaDataType.Email | PropertyEmailAddress |
| MetaDataType.URL | PropertyUrl |
| MetaDataType.ShortString | PropertyString |
| MetaDataType.UniqueIdentifier | |
| MetaDataType.LongHtmlString | PropertyXhtmlString |
| MetaDataType.EnumMultiValue | PropertyDictionaryMultiple |
| MetaDataType.DictionaryMultiValue | |
| MetaDataType.EnumSingleValue | PropertyDictionarySingle |
| MetaDataType.DictionarySingleValue | |
| MetaDataType.Timestamp | Unsupported |
| MetaDataType.Variant | |
| MetaDataType.Numeric | |
| MetaDataType.Sysname | |
| MetaDataType.Binary | |
| MetaDataType.VarBinary | |
| MetaDataType.StringDictionary | |
| MetaDataType.Image | |
| MetaDataType.File | |
| MetaDataType.ImageFile | |
| MetaDataType.MetaObject |
How properties are stored
It might not be particularly useful to look into how properties are stored in database, but it can be interesting to know the detail (You might someday look into your databases to investigate, when a problem arises.)
In Commerce 9, the way properties are stored is fundamentally changed.
If you have worked with CMS content properties before, you might know about tblContentProperties. Commerce 9 adapts the same idea.
Commerce 9 has a new table - CatalogContentProperty to store property. Besides the identity columns, there are 10 columns for the datatypes MetaDataPlus supports: Boolean, Number (Integer), FloatNumber, Money, Decimal [^foo151], Date, Binary, String (for strings shorter than 256 characters length), LongString (any strings long than that) and Guid. A metafield, for each content and for each language (in case it’s culture specific) will be stored in one row matches with it value. So a string property value will be in String column, while an Integer property value will be in Number column, and so on and so forth, while the other columns remain null for that row.
It’s hard to say if the old or new approach is definitely better than the other. The new one might be more flexible, but it means more rows and is not as “natural” as the old one. However, the new one has simply access approach - you only read or update or delete from one table, while the old one has to rely on the automatically generated stored procedures (which are updated every time you add or remove metafields to metaclasses). In long run, the new approach might be simpler to maintain. Let’s hope for that.
When you upgrade your site to Commerce 9, all old metaclass tables are dropped. Of course, it does not really make sense to keep the tables you no longer update. However, you might still have custom queries to work on that tables (such as to report something on your catalog). To replace those tables, new views are created. They are created by querying the data from CatalogContentProperty table, and are updated every time you update your metaclasses (just like the way old metaclass tables did).
Those views are, as you can expect, slower than the original tables, some tests shows that they can be 30% slower than the original tables. However, you’ll not use them frequently enough for the difference to really make senses.
Dictionary types.
Previously we discussed on how properties work with catalog content. However - if you have dictionary types in your MetaClasses, they will work differently. In this section we will examine these special data types - this applies to Order system metaclasses as well.
As we all know - there are three types of dictionary in Episerver Commerce:
- Single value dictionary: editor can select a value from defined ones.
Single value dictionary type is supported in the strongly typed content types - you’ll need to define a property of type string, with backing type of typeof(PropertyDictionarySingle)
1 [BackingType(typeof(PropertyDictionarySingle))]
2 public virtual string Color { get; set; }
- Multi value Dictionary: editor can select multiple values from defined ones. The only different from Single value dictionary is it has the “Multiline” option enabled.
You can define a property for Multi value dictionary in content type by IEnumeable<string> and typeof(PropertyDictionaryMultiple) backing type
1 [BackingType(typeof(PropertyDictionaryMultiple))]
2 public virtual IEnumerable<string> Colors { get; set;\
3 }
Both single and multi value dictionary types are fully supported in Catalog UI, including editing:
and administration:
- String dictionary: this is the true “dictionary type”: you can define pairs of key and value (the previous types are actually “list”).
String dictionary is not supported by the strongly typed content types, nor Catalog UI. To manage this metafield, we’ll have to use Commerce Manager, or use MetaDataPlus API:s
In later chapters, we will see how to add the support for it in Catalog UI.
How dictionary works
We’ve learned in previous chapters how properties are stored and loaded. Technically, dictionaries are “just another properties”. However they are stored differently. If you look at CatalogContentProperty table (or ecfVersionProperty, for draft versions), you’ll see the dictionary properties are stored as numbers. What do those numbers mean?
- For single value dictionary type, that number is the MetaDictionaryId of the selected value in
MetaDictionarytable. - For multi value dictionary type, things are a bit more complicated. That number is the
MetaKeyvalue inMetaKeytable. ThisMetaKey, is, however, connected toMetaMultiValueDictionary, which itself points back toMetaDictionary. An “usual” design for 1-n relation in database, right? - For string dictionary type, it’s more or less the same as multi value dictionary. However, the
MetaKeywill point to pairs of key and value inMetaStringDictionaryValuetable.