Optimizely Commerce Business Foundation custom entity model - To add on Quan Mai's blog :)

Posted on July 01, 2023

Quan Mai is right, there is another official way to load organizations and contacts under Optimizely Commerce using the famous static class BusinessManager. Though, there's more you can do about it. Have you ever wonder if you could personalize the data access on custom properties using your own class instead of using, e.g., Organization.Properties["MyCustomProperty"]? Yes, me too, and in this blog post, I'm going to show you it's possible!

For readability simplicity, I'll concentrate the discussion around the organizations, but keep in mind the same personalization principle applies for all other Business Foundation entities. Our journey will start under a configuration file; baf.data.manager.config. This file lists a couple of Business Foundation entities with their associated RequestHandler. You see here, the default configuration for an Organization is the following type: Mediachase.Commerce.Customers.Handlers.OrganizationRequestHandler, Mediachase.Commerce. If you have Jetbains Rider, you can easily find the class by searching everywhere, usually CTRL + T with the Visual Studio key binding and include non solution items. For those who are using dotPeek by example, you need to decompile the dll Mediachase.Commerce.dll which is under the NuGet package EPiServer.Commerce.Core.

The OrganizationRequestHandler inherits CustomerRequestHandlerBase and this is where we'll be finding what we're looking for. There's a method CreateEntityObject, which has the following one liner:

return new CustomerEntityFactory().Create<EntityObject>((object) new KeyValuePair<string, PrimaryKeyId?>(metaClassName, primaryKeyId));

If you look closely, inside the class CustomerEntityFactory, you can find how Business Foundation is creating its instance of classes that it will be returning to you after pulling the data. This is also why you can use "OfType" when using BusinessManager. The class is quite basic, for any known entity, use the following new instance of class to build my entity:

EntityObject IFactoryMethod<EntityObject>.Create(object obj)
    {
      KeyValuePair<string, PrimaryKeyId?> keyValuePair = obj != null ? (KeyValuePair<string, PrimaryKeyId?>) obj : throw new ArgumentNullException(nameof (obj));
      EntityObject entityObject = (EntityObject) null;
      switch (keyValuePair.Key)
      {
        case "Contact":
          entityObject = (EntityObject) CustomerContact.CreateInstance();
          break;
        case "Address":
          entityObject = (EntityObject) CustomerAddress.CreateInstance();
          break;
        case "Organization":
          entityObject = (EntityObject) Organization.CreateInstance();
          break;
        case "CreditCard":
          entityObject = (EntityObject) CreditCard.CreateInstance();
          break;
      }
      if (entityObject != null && keyValuePair.Value.HasValue)
        entityObject.PrimaryKeyId = new PrimaryKeyId?(keyValuePair.Value.Value);
      return entityObject;
    }

Naturally, this was my entry point to personalize how the organizations could be loaded. There are three things you need to do:

  1. Create your own entity.
  2. Create a new entity factory.
  3. Create a new request handler.

1. Create your own entity

We're personalizing our Organization entity with additional properties, we want them to be accessible without having to call Properties["MyProperty"]. So here, we'll create a new model, say, MyOrganization, that will be inheriting from Mediachase.Commerce.Customers.Organization:

using System;
using System.Collections.Generic;
using Mediachase.BusinessFoundation.Data;
using Mediachase.Commerce.Customers;
using Mediachase.Commerce.Customers.Request;

namespace MyNamespace
{
    public class MyOrganization : Organization
    {
        public string MyInternalOrgNumber
        {
            get => (string) this[nameof(MyInternalOrgNumber)];
            set => this[nameof(MyInternalOrgNumber)] = value;
        }
    }
}

In the previous example, I want developers to be able to access our custom property "MyInternalOrgNumber" from this object. This is basically the same as calling Organization.Properties["MyInternalOrgNumber"]. Don't forget from on now, to use your custom class, you need to cast it to "MyOrganization". You can do that in the application layer responsible to obtain data from BusinessManager. What we did in our project, was to create a service that was the data layer abstraction between our application and Business Foundation. Any calls on the CustomerContext, e.g., CustomerContext.Current.GetOrganizations() can be also casted to "MyOrganization" (as far as I remember™️).

2. Create entity factory

You still have the class CustomerEntityFactory decompiled? Go back there and copy it, because it will almost be the same in your implementation:

using System;
using System.Collections.Generic;
using MyEntityModelsNamespace;
using Mediachase.BusinessFoundation.Common;
using Mediachase.BusinessFoundation.Data;
using Mediachase.BusinessFoundation.Data.Business;
using Mediachase.Commerce.Customers;

namespace MyNamespace
{
    public class MyEntityFactory : AbstractFactory, IFactoryMethod<EntityObject>
    {
        /// <summary>Creates the specified obj.</summary>
        /// <param name="obj">The obj.</param>
        /// <returns></returns>
        EntityObject IFactoryMethod<EntityObject>.Create(object obj)
        {
            var keyValuePair = (KeyValuePair<string, PrimaryKeyId?>?) obj ?? throw new ArgumentNullException(nameof(obj));
            var key = keyValuePair.Key;
            EntityObject entityObject = key switch
            {
                ContactEntity.ClassName => CustomerContact.CreateInstance(),
                AddressEntity.ClassName => CustomerAddress.CreateInstance(),
                OrganizationEntity.ClassName => new MyOrganization(),
                CreditCardEntity.ClassName => CreditCard.CreateInstance()
                _ => default
            };

            if (entityObject != null && keyValuePair.Value.HasValue)
                entityObject.PrimaryKeyId = keyValuePair.Value.Value;

            return entityObject;
        }
    }
}

3. Create request handler

For an organization, as I've previously mentioned, by default it's using Mediachase.Commerce.Customers.Handlers.OrganizationRequestHandler, Mediachase.Commerce as the type. Our class then, will look like that:

using System.Collections.Generic;
using Mediachase.BusinessFoundation.Data;
using Mediachase.BusinessFoundation.Data.Business;
using Mediachase.Commerce.Customers.Handlers;

namespace MyNamespace
{
    public class MyOrganizationRequestHandler : OrganizationRequestHandler
    {
        protected override EntityObject CreateEntityObject(string metaClassName, PrimaryKeyId? primaryKeyId)
        {
            return new MyEntityFactory().Create<EntityObject>(new KeyValuePair<string, PrimaryKeyId?>(metaClassName, primaryKeyId));
        }
    }
}

The final touch; Change the configuration under the file baf.data.manager.config to point it to your new request handler. In our example, the file should have the following line instead of the default one for the organization:

<add metaClass="Organization" method="*" type="MyNamespace.MyOrganizationRequestHandler, MyAssemblyName" />

Final thoughts

Maybe it might be a bit overkill to do that just to be able to use your own class for the entities, but in the end, it helped a lot. Looking for a reference is easier; we can easily find who is using a certain custom property and whatnot. Also, it enhances a bit the readability. One other potential advantage of the approach would be for new developers, especially junior, usually not accustomed with the certain level of complexity Business Foundation entails.