Custom Products and Products Entries using Metadata Plus configured by an XML file Part II – EPiServer Commerce

In Part I we explained how to use Metadata Plus using class inheritance and we also defined an XML file which will allow us to do the same using another Commerce API which is more flexible. So in this part we will define several classes to parse the xml and create the classes and fields based on the XML file. So once again without more hustle lets code !!!

First, we will create an interface IParser in order to allow in the future to create other kind of XML parsers if needed.

    /// <summary>
    /// Parser interface for any new XML parser.
    /// </summary>
    /// 
    /// Generic Type T which can be any type with XML attributes
    /// 
    public interface IParser
    {
        /// <summary>
        /// Gets or sets the xml file path.
        /// If there is a need to change, it must be set before invoking
        /// the Parse method
        /// </summary>
        string XmlFilePath { get; set; }

        /// <summary>
        /// Parse method which is in charge to transform the file into a class.
        /// </summary>
        /// 
        /// The  class. It can be any class with XML attributes. 
        /// 
        T Parse();
    }

Then, we will create the meta data parser. The class uses a web.config key (MetadataXMLFilePath) that sets where the XML file is located. In my specific scenario the file is at Configs\metadata.config

    using System.Configuration;
    using System.Xml;
    using System.Xml.Serialization;
    using Business.Metadata;

    /// <summary>
    /// The metadata parser class which is in charge to transform the xml config file
    /// to a MetaClass class.
    /// </summary>
    public class MetadataParser : IParser
    {
        /// <summary>
        /// Initializes a new instance of the  class.
        /// Sets the default xml file path from the web.config app settings section
        /// </summary>
        public MetadataParser()
        {
            XmlFilePath = ConfigurationManager.AppSettings["MetadataXMLFilePath"];
        }

        /// <summary>
        /// Gets or sets the xml file path.
        /// If needed must be modified before calling the Parse method
        /// </summary>
        public string XmlFilePath { get; set; }

        /// <summary>
        /// Parse the xml file into a MetaClasses and return it.
        /// </summary>
        /// 
        /// The  parsed from the xml file.
        /// 
        public Meta Parse()
        {
            var serializer = new XmlSerializer(typeof(Meta));
            return (Meta)serializer.Deserialize(new XmlTextReader(XmlFilePath));
        }
    }

Now that we have the XML serializer, we will create a initialization module which will call this parser and process its content to create the meta classes and fields. Pay special attention to the comments in the code

    using System.Web;
    using EPiServer.Framework;
    using EPiServer.Framework.Initialization;
    using Mediachase.Commerce.Catalog;
    using Mediachase.Commerce.Initialization;
    using Metadata;
    using Util.Parsers;

    /// <summary>
    /// The metadata initialization class.
    /// It runs when the application starts and verifies that the metadata 
    /// classes and fields are created
    /// </summary>
    [InitializableModule]
    [ModuleDependency(typeof(CommerceInitialization))]
    public class MetadataInitialization : IInitializableModule
    {
        /// <summary>
        /// Initialize metadata classes and fields.
        /// </summary>
        /// 
        /// The current initialization engine context.
        /// 
        public void Initialize(InitializationEngine context)
        {
            // Catalog context - Metadata
            var metadataContext = CatalogContext.MetaDataContext;

            // Generate all meta classes and fields from metadata.config file
            var metaParser = new MetadataParser();
            metaParser.XmlFilePath = HttpContext.Current.Server.MapPath(metaParser.XmlFilePath);
            var metaClasses = metaParser.Parse();

            foreach (var metaClass in metaClasses.MetaClasses.MetaClass)
            {
                // Create a meta class
                MetadataInitializationTools.CreateMetaClass(
                     metadataContext,
                     metaClass.ParentClassName,
                     metaClass.ClassName,
                     metaClass.FriendlyClassName,
                     metaClass.TableClassName,
                     metaClass.Description);
            }

            foreach (var metaField in metaClasses.MetaFields.MetaField)
            {
                // Create a meta field for this meta class
                var metaFieldDb = MetadataInitializationTools.CreateMetaField(
                    metadataContext,
                    metaField.Namespace,
                    metaField.FieldName,
                    metaField.FriendlyFieldName,
                    metaField.FieldType,
                    metaField.Length,
                    metaField.AllowNulls,
                    metaField.CultureSpecific,
                    metaField.AllowSearch);

                foreach (var extraAttribute in metaField.ExtraAttribute)
                {
                    metaFieldDb.Attributes[extraAttribute.AttributeName] = extraAttribute.AttributeValue;
                }

                // Join the meta field to the meta classes if necessary
                if (metaField.Joins?.Join == null)
                {
                    continue;
                }

                foreach (var join in metaField.Joins.Join)
                {
                    MetadataInitializationTools.JoinField(metadataContext, metaFieldDb, join.To);
                }
            }
        }

        /// <summary>
        /// UnInitialize something that is required
        /// In this case no implementation has been done
        /// </summary>
        /// 
        /// The current initialization engine context.
        /// 
        public void Uninitialize(InitializationEngine context)
        {
            // Add uninitialization logic
        }
    }

The class MetadataInitializationTools is the responsible to create the meta classes and fields using the Commerce API. Once again pay special attention to the comments

    using System;
    using Mediachase.MetaDataPlus;
    using Mediachase.MetaDataPlus.Configurator;
    using Util;

    /// <summary>
    /// The metadata initialization tools class.
    /// Contains several methods to create and 
    /// join meta classes and fields
    /// </summary>
    public class MetadataInitializationTools
    {
        /// <summary>
        /// The create meta class method allows to
        /// create a new meta class if it does not 
        /// exists, otherwise it returns the first one
        /// found in the database
        /// </summary>
        /// 
        /// The context on this class is going to be created
        /// For example: Catalog context.
        /// 
        /// 
        /// The parent class name which the new meta class
        /// inherit.
        /// 
        /// 
        /// The class name of the new meta class.
        /// 
        /// 
        /// The friendly class name of the new meta class.
        /// 
        /// 
        /// The table name on the database where the meta class is going to be saved.
        /// 
        /// 
        /// The description of the meta class (Optional).
        /// It is empty by default
        /// 
        /// 
        /// The  which contains
        /// all the information related to the new or found meta class.
        /// 
        /// 
        /// When some of the required parameters were not properly set
        /// 
        public static Mediachase.MetaDataPlus.Configurator.MetaClass CreateMetaClass(
            MetaDataContext context,
            string parentClassName,
            string className,
            string friendlyClassName,
            string tableName,
            string description = "")
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
            if (parentClassName == null)
            {
                throw new ArgumentNullException(nameof(className));
            }
            if (className == null)
            {
                throw new ArgumentNullException(nameof(className));
            }
            if (friendlyClassName == null)
            {
                throw new ArgumentNullException(nameof(friendlyClassName));
            }
            if (tableName == null)
            {
                throw new ArgumentNullException(nameof(tableName));
            }

            var parentMetaClass = Mediachase.MetaDataPlus.Configurator.MetaClass.Load(context, parentClassName);
            var metaClass = Mediachase.MetaDataPlus.Configurator.MetaClass.Load(context, className);

            // If meta class has not been created, otherwise update some specific properties of the class
            if (metaClass == null)
            {
                metaClass = Mediachase.MetaDataPlus.Configurator.MetaClass.Create(
                    context, parentMetaClass.Namespace.Replace(Constants.Metadata.Base.System, Constants.Metadata.Base.User), // Workaround to get the correct namespace from the parent class
                    className,
                    friendlyClassName,
                    tableName,
                    parentMetaClass,
                    false,
                    description);
            }
            else
            {
                metaClass.Name = className;
                metaClass.FriendlyName = friendlyClassName;
                metaClass.Description = description;
            }

            return metaClass;
        }

        /// <summary>
        /// The create meta field method allows to
        /// create a new meta field if it does not 
        /// exists, otherwise it returns the first one
        /// found in the database
        /// </summary>
        /// 
        /// The context on this class is going to be created
        /// For example: Order context.
        /// 
        /// 
        /// The meta data namespace of the new field. 
        /// This allows a field to be treated as public or private
        /// 
        /// 
        /// The field name of the new field. Non always visible
        /// 
        /// 
        /// The friendly field name of the new field. Always visible
        /// 
        /// 
        /// The type of the meta field as a string. 
        /// It will be cast as a enumeration variable.
        /// 
        /// 
        /// The length in bytes of the meta field By default 0
        /// which means number of bytes based on the type.
        /// 
        /// 
        /// If the new meta field allow nulls.
        /// 
        /// 
        /// If the new meta field accept different cultures (languages).
        /// 
        /// 
        /// If the new meta field allows to be searched.
        /// 
        /// 
        /// If the new meta field encrypted (Optional)
        /// By default false.
        /// 
        /// 
        /// The description for the new meta field (Optional).
        /// By default empty
        /// 
        /// 
        /// The  which contains
        /// all the information related to the new or found meta field.
        /// 
        /// 
        /// When some of the required parameters were not properly set
        /// 
        public static Mediachase.MetaDataPlus.Configurator.MetaField CreateMetaField(
            MetaDataContext context,
            string metaDataNamespace,
            string fieldName,
            string friendlyFieldName,
            string type,
            int length,
            bool allowNulls,
            bool cultureSpecific,
            bool allowSearch,
            bool isEncrypted = false,
            string description = "")
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
            if (metaDataNamespace == null)
            {
                throw new ArgumentNullException(nameof(metaDataNamespace));
            }
            if (fieldName == null)
            {
                throw new ArgumentNullException(nameof(fieldName));
            }
            if (friendlyFieldName == null)
            {
                throw new ArgumentNullException(nameof(friendlyFieldName));
            }

            var enumType = (MetaDataType)Enum.Parse(typeof(MetaDataType), type);

            var metaField = Mediachase.MetaDataPlus.Configurator.MetaField.Load(context, fieldName);

            // If meta field has not been created, otherwise update some specific properties of the field
            if (metaField == null)
            {
                metaField = Mediachase.MetaDataPlus.Configurator.MetaField.Create(
                    context,
                    metaDataNamespace,
                    fieldName,
                    friendlyFieldName,
                    description,
                    enumType,
                    length,
                    allowNulls,
                    cultureSpecific,
                    allowSearch,
                    isEncrypted);
            }
            else
            {
                metaField.FriendlyName = friendlyFieldName;
                metaField.Description = description;
                metaField.SetAllowNulls(allowNulls);
                metaField.SetAllowSearch(allowSearch);
            }

            return metaField;
        }

        /// <summary>
        /// The join field method verifies if the meta class and the 
        /// meta field are already related and if not it joins them.
        /// </summary>
        /// 
        /// The context on this class is going to be created
        /// For example: Catalog context.
        /// 
        /// 
        /// The meta field to verify if is part of the meta class.
        /// 
        /// 
        /// The name of the meta class that needs this field to be part of it.
        /// 
        public static void JoinField(MetaDataContext context, Mediachase.MetaDataPlus.Configurator.MetaField field, string metaClassName)
        {
            if (string.IsNullOrEmpty(metaClassName))
            {
                return;
            }

            var cls = Mediachase.MetaDataPlus.Configurator.MetaClass.Load(context, metaClassName);

            if (MetaFieldIsNotConnected(field, cls))
            {
                cls.AddField(field);
            }
        }

        /// <summary>
        /// The meta field is not connected method verify
        /// if the meta field is part of the meta class by 
        /// returning a boolean
        /// </summary>
        /// 
        /// The field which is going to be verified if is 
        /// part of a meta class.
        /// 
        /// 
        /// The meta class that should contain the meta field.
        /// 
        /// 
        /// The  whether the meta field is 
        /// connected (part of) the meta class.
        /// 
        private static bool MetaFieldIsNotConnected(Mediachase.MetaDataPlus.Configurator.MetaField field, Mediachase.MetaDataPlus.Configurator.MetaClass cls)
        {
            return cls != null &amp;&amp; !cls.MetaFields.Contains(field);
        }
    }

And that is all. Every time the application starts, or a pool recycling is executed, the initialization module will parse the XML file and add the new classes and fields to the commerce database. If there are some classes or fields that are already in the database it will update some of the allowed properties.

Note: This code does not allow to delete and recreate classes or fields, if they are already created the code will only update some properties. In order to remove a field or class you must remove it from the commerce manager interface.

I hope it will help someone and as always keep learning !!!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s