Posts
12
Comments
5
Trackbacks
0
Building Product Business Entities From Commerce Server 2007 Products

Many developers write catalog code against the Commerce Server 2007 Product class, or against a single business entity class that represents all types of products in the catalog, and then use the product's DefinitionName to define behavior like so:

private static void DoSomething(ProductBusinessEntity product)

{

    if ((product.DefinitionName == "WhiteGood") || (product.DefinitionName == "BrownGood"))

    {

        // Do something for appliances, consumer electronics, etc.

    }

    else if (product.DefinitionName == "SoftGood")

    {

        // Do something else for software.

    }

    else if (product.DefinitionName == "Bundle")

    {

        // Do something else for bundles of products.

    }

    else

    {

        // Do something else for all other types of products.

    }

}

This makes their code fragile and harder to understand, test, or extend.  Fortunately, it's easy to define a business entity class for each type of product, place type-specific logic into these classes, and to build them up from products using abstract factory and dependency injection.  Here's an extremely simple example of how we do it.

(I've already described how we use dependency injection to get catalog context in Injecting Commerce Server 2007 Catalog Context.  I've also described how we extend the Product class to get property values in Extension Methods For Commerce Server 2007 Products.  This post builds on the concepts and code from these two posts.)

We start with our catalog schema.  For this example, I've defined WhiteGood, BrownGood, SoftGood, and Bundle products in the catalog schema.

We then define our business entity class hierarchy, creating a base class for all product types, one class for each product type, and additional base classes as needed where products have properties or methods in common.  For this example, I've defined ProductEntityBase (base for all products), HardGoodEntityBase (base for physical products), WhiteGoodEntity, BrownGoodEntity, SoftGoodEntity, and BundleEntity classes:

    1 using System;

    2 

    3 namespace MyCommerceApplication.Entities

    4 {

    5 

    6     /// <summary>

    7     /// <see langword="abstract"/> base class for product entities.

    8     /// </summary>

    9     public abstract class ProductEntityBase

   10     {

   11         #region Properties

   12 

   13         /// <summary>

   14         /// Gets or sets the catalog identifier <see cref="String"/> of the catalog that the

   15         /// product is from.

   16         /// </summary>

   17         public string CatalogId { get; set; }

   18 

   19         /// <summary>

   20         /// Gets or sets the product identifier.

   21         /// </summary>

   22         public string ProductId { get; set; }

   23 

   24         /// <summary>

   25         /// Gets or sets the product's web identifier.

   26         /// </summary>

   27         public string WebId { get; set; }

   28 

   29         /// <summary>

   30         /// Gets or sets the product's SKU.

   31         /// </summary>

   32         public string Sku { get; set; }

   33 

   34         /// <summary>

   35         /// Gets or sets the product's localized short name.

   36         /// </summary>

   37         public string ShortName { get; set; }

   38 

   39         /// <summary>

   40         /// Gets or sets the product's localized long name.

   41         /// </summary>

   42         public string LongName { get; set; }

   43 

   44         /// <summary>

   45         /// Gets or sets the product's localized short description.

   46         /// </summary>

   47         public string ShortDescription { get; set; }

   48 

   49         /// <summary>

   50         /// Gets or sets the product's localized long description.

   51         /// </summary>

   52         public string LongDescription { get; set; }

   53 

   54         /// <summary>

   55         /// Gets or sets whether the product is enabled.

   56         /// </summary>

   57         public bool IsEnabled { get; set; }

   58 

   59         #endregion // Properties

   60     }

   61 

   62 }

(I've trimmed these to make things easier to follow.)

public abstract class HardGoodEntityBase : ProductEntityBase

{

    public double? LengthInBox { get; set; }

    public double? WidthInBox { get; set; }

    public double? HeightInBox { get; set; }

    public double? LengthOutOfBox { get; set; }

    public double? WidthOutOfBox { get; set; }

    public double? HeightOutOfBox { get; set; }

    public double? WeightInBox { get; set; }

    public double? WeightOutOfBox { get; set; }

}

 

public class WhiteGoodEntity : HardGoodEntityBase { /*...*/ }

public class BrownGoodEntity : HardGoodEntityBase { /*...*/ }

public class SoftGoodEntity : ProductEntityBase { /*...*/ }

public class BundleEntity : ProductEntityBase { /*...*/ }

We then define our mapping interface to map from a Product to a business entity class:

    1 using Microsoft.CommerceServer.Catalog;

    2 using MyCommerceApplication.Entities;

    3 

    4 namespace MyCommerceApplication.Mappings

    5 {

    6 

    7     /// <summary>

    8     /// Mapping used to map Commerce Server products to product business entities.

    9     /// </summary>

   10     public interface IProductMapping

   11     {

   12         #region Methods

   13 

   14         /// <summary>

   15         /// Maps a Commerce Server product to a product business entity.

   16         /// </summary>

   17         /// <param name="product">The <see cref="Product"/> to map from.</param>

   18         /// <returns>The resulting <see cref="ProductEntityBase"/>.</returns>

   19         ProductEntityBase Map(Product product);

   20 

   21         #endregion // Methods

   22     }

   23 

   24 }

We then implement the mapping interface, creating a base class for all mappings and one class for each non-abstract business entity class.  We override the MapProperties method in the subclasses to map the properties specific to that type of product onto the corresponding business entity class:

    1 using Microsoft.CommerceServer.Catalog;

    2 using MyCommerceApplication.Entities;

    3 

    4 namespace MyCommerceApplication.Mappings

    5 {

    6 

    7     /// <summary>

    8     /// <see langword="abstract"/> base class for mappings used to map Commerce Server products to

    9     /// product business entities.

   10     /// </summary>

   11     /// <typeparam name="TProductEntity">The type of <see cref="ProductEntityBase"/> to map the

   12     /// Commerce Server product to.  <typeparamref name="TProductEntity"/> must have a

   13     /// <see langword="public"/> parameterless constructor.</typeparam>

   14     public abstract class ProductMappingBase<TProductEntity> : IProductMapping

   15         where TProductEntity : ProductEntityBase, new()

   16     {

   17         #region IProductMapping Members

   18 

   19         /// <summary>

   20         /// Maps the specified Commerce Server product to a product entity.

   21         /// </summary>

   22         /// <param name="product">The <see cref="Product"/> to map from.</param>

   23         /// <returns>The resulting <typeparamref name="ProductEntityBase"/>.</returns>

   24         public ProductEntityBase Map(Product product)

   25         {

   26             TProductEntity entity;

   27 

   28             entity = new TProductEntity();

   29             MapBaseProperties(product, entity);

   30             return entity;

   31         }

   32 

   33         #endregion // IProductMapping Members

   34 

   35         #region Methods

   36 

   37         /// <summary>

   38         /// Maps the base properties of a Commerce Server product to a product entity.

   39         /// </summary>

   40         /// <param name="product">The <see cref="Product"/> to map from.</param>

   41         /// <param name="entity">The <typeparamref name="TProductEntity"/> to map to.</param>

   42         protected virtual void MapBaseProperties(Product product, TProductEntity entity)

   43         {

   44             // Map built-in properties.

   45             entity.ProductId = product.ProductId;

   46             entity.CatalogId = product.CatalogName;

   47             entity.ShortName = product.DisplayName;

   48 

   49             // Map required custom properties.

   50             entity.ShortDescription = product.GetRequiredPropertyValue<string>("ShortDescription");

   51             entity.IsEnabled = product.GetRequiredPropertyValue<bool>("IsEnabled");

   52 

   53             // Map optional custom properties.

   54             entity.WebId = product.GetOptionalPropertyValue<string>("WebId");

   55             entity.LongName = product.GetOptionalPropertyValue<string>("LongName");

   56             entity.LongDescription = product.GetOptionalPropertyValue<string>("LongDescription");

   57 

   58             // ...

   59         }

   60 

   61         #endregion // Methods

   62     }

   63 

   64 }

(Trimmed again.)

public abstract class HardGoodMappingBase<TProductEntity> : ProductMappingBase<TProductEntity>

    where TProductEntity : HardGoodEntityBase, new()

{

    protected override void MapBaseProperties(Product product, TProductEntity entity)

    {

        base.MapBaseProperties(product, entity);

 

        // Map custom properties specific to hard goods.

        entity.LengthInBox = product.GetOptionalPropertyValue<double?>("LengthInBox");

        entity.WidthInBox = product.GetOptionalPropertyValue<double?>("WidthInBox");

        entity.HeightInBox = product.GetOptionalPropertyValue<double?>("HeightInBox");

        entity.LengthOutOfBox = product.GetOptionalPropertyValue<double?>("LengthOutOfBox");

        entity.WidthOutOfBox = product.GetOptionalPropertyValue<double?>("WidthOutOfBox");

        entity.HeightOutOfBox = product.GetOptionalPropertyValue<double?>("HeightOutOfBox");

        entity.WeightInBox = product.GetOptionalPropertyValue<double?>("WeightInBox");

        entity.WeightOutOfBox = product.GetOptionalPropertyValue<double?>("WeightOutOfBox");

    }

}

 

public class WhiteGoodMapping : HardGoodMappingBase<WhiteGoodEntity>

{

    protected override void MapBaseProperties(Product product, WhiteGoodEntity entity)

    {

        base.MapBaseProperties(product, entity);

        // Map custom properties specific to white goods...

    }

}

 

public class BrownGoodMapping : HardGoodMappingBase<BrownGoodEntity> { /*...*/ }

public class SoftGoodMapping : ProductMappingBase<SoftGoodEntity> { /*...*/ }

public class BundleMapping : ProductMappingBase<BundleEntity> { /*...*/ }

At this point in the example, we've got everything we need to map products to business entities.  However, we still need to use the product's DefinitionName to figure out which mapping to use to map the product to its business entity, and we still need to create an instance of that mapping.  To do this, we define the factory interface for creating a mapping:

    1 using MyCommerceApplication.Mappings;

    2 

    3 namespace MyCommerceApplication

    4 {

    5 

    6     /// <summary>

    7     /// Factory used to create Commerce Server product mappings.

    8     /// </summary>

    9     public interface IProductMappingFactory

   10     {

   11         #region Methods

   12 

   13         /// <summary>

   14         /// Gets the product mapping for the specified definition name.

   15         /// </summary>

   16         /// <param name="definitionName">The definition name of the product mapping that is to be

   17         /// created.</param>

   18         /// <returns>An <see cref="IProductMapping"/> object.</returns>

   19         IProductMapping CreateProductMapping(string definitionName);

   20 

   21         #endregion // Methods

   22     }

   23 

   24 }

We then implement the factory interface using dependency injection, using the supplied definition name to retrieve the correct implementation of IProductMapping from our dependency container:

    1 using System;

    2 using System.Globalization;

    3 using Microsoft.Practices.Unity;

    4 using MyCommerceApplication.Mappings;

    5 

    6 namespace MyCommerceApplication

    7 {

    8 

    9     /// <summary>

   10     /// An <see cref="IProductMappingFactory"/> implementation that resolves mappings using a

   11     /// dependency injection container.

   12     /// </summary>

   13     public class ProductMappingFactory : IProductMappingFactory

   14     {

   15         #region Properties

   16 

   17         /// <summary>

   18         /// Gets or sets the dependency injection container to use to resolve dependencies.

   19         /// </summary>

   20         /// <remarks>

   21         /// <para>

   22         /// <see cref="ProductMappingFactory"/> uses the <see cref="IUnityContainer"/> to resolve

   23         /// mappings by name.

   24         /// </para>

   25         /// </remarks>

   26         [Dependency]

   27         public IUnityContainer Container { get; set; }

   28 

   29         #endregion // Properties

   30 

   31         #region IProductMappingFactory Members

   32 

   33         /// <summary>

   34         /// Gets the product mapping for the specified definition name.

   35         /// </summary>

   36         /// <param name="definitionName">The definition name of the product mapping that is to be

   37         /// created.</param>

   38         /// <returns>An <see cref="IProductMapping"/> object.</returns>

   39         /// <exception cref="InvalidOperationException">Throw if <see cref="IProductMapping"/>

   40         /// is not registered with the specified definition name in the dependency injection

   41         /// container.</exception>

   42         public IProductMapping CreateProductMapping(string definitionName)

   43         {

   44             try

   45             {

   46                 return Container.Resolve<IProductMapping>(definitionName);

   47             }

   48             catch (ResolutionFailedException ex)

   49             {

   50                 throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Product definition name '{0}' is unknown.", definitionName), ex);

   51             }

   52         }

   53 

   54         #endregion // IProductMappingFactory Members

   55     }

   56 

   57 }

(Note that the factory implementation advertises that it needs an IUnityContainer.  In order for the factory to do its work, the UnityContainer that builds up the factory must be registered as the instance of IUnityContainer:

container = new UnityContainer();

container.RegisterInstance<IUnityContainer>(container);

We usually register a container with itself as soon as it's created so we can do this sort of thing.  However, we generally only use this technique in factories.  It's easy to abuse.)

We then configure our application's dependency injection framework to map the factory interface to the factory implementation, and to map the mapping interface to the various named mapping implementations:

    1 <?xml version="1.0" encoding="utf-8" ?>

    2 <configuration>

    3 

    4   <configSections>

    5     <section name="dependencyInjectionConfiguration" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration, Version=1.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />

    6     <!-- ... -->

    7   </configSections>

    8 

    9   <dependencyInjectionConfiguration>

   10     <typeAliases>

   11       <typeAlias alias="singleton" type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager, Microsoft.Practices.Unity" />

   12     </typeAliases>

   13     <containers>

   14       <container>

   15         <types>

   16 

   17           <!-- CatalogContextFactory, from my earlier post. -->

   18           <type type="MyCommerceApplication.ICatalogContextFactory, MyCommerceApplication" mapTo="MyCommerceApplication.WebTierCatalogContextFactory, MyCommerceApplication">

   19             <lifetime type="singleton" />

   20           </type>

   21 

   22           <!-- Map IProductMappingFactory to ProductMappingFactory. -->

   23           <type type="MyCommerceApplication.IProductMappingFactory, MyCommerceApplication" mapTo="MyCommerceApplication.ProductMappingFactory, MyCommerceApplication">

   24             <lifetime type="singleton" />

   25           </type>

   26 

   27           <!-- Map IProductMapping to the different mapping classes by definition name. -->

   28           <type type="MyCommerceApplication.Mappings.IProductMapping, MyCommerceApplication" mapTo="MyCommerceApplication.Mappings.WhiteGoodMapping, MyCommerceApplication" name="WhiteGood">

   29             <lifetime type="singleton" />

   30           </type>

   31           <type type="MyCommerceApplication.Mappings.IProductMapping, MyCommerceApplication" mapTo="MyCommerceApplication.Mappings.BrownGoodMapping, MyCommerceApplication" name="BrownGood">

   32             <lifetime type="singleton" />

   33           </type>

   34           <type type="MyCommerceApplication.Mappings.IProductMapping, MyCommerceApplication" mapTo="MyCommerceApplication.Mappings.SoftGoodMapping, MyCommerceApplication" name="SoftGood">

   35             <lifetime type="singleton" />

   36           </type>

   37           <type type="MyCommerceApplication.Mappings.IProductMapping, MyCommerceApplication" mapTo="MyCommerceApplication.Mappings.BundleMapping, MyCommerceApplication" name="Bundle">

   38             <lifetime type="singleton" />

   39           </type>

   40           <!-- ... -->

   41 

   42         </types>

   43       </container>

   44     </containers>

   45   </dependencyInjectionConfiguration>

   46 

   47   <!-- ... -->

   48 

   49 </configuration>

(Our factory and our mappings are stateless and thread-safe, so we use the ContainerControlledLifetimeManager to make them singletons, and inject the same instance over and over again.)

Finally, we with all this in place, we can write code that depends on the factory to get a mapping, and on the mapping to convert the Product to the corresponding business entity.  Here's an example of a product provider that uses the factories we've defined to get products from the catalog:

    1 using Microsoft.CommerceServer.Catalog;

    2 using Microsoft.Practices.Unity;

    3 using MyCommerceApplication.Entities;

    4 using MyCommerceApplication.Mappings;

    5 

    6 namespace MyCommerceApplication

    7 {

    8 

    9     /// <summary>

   10     /// An <see cref="IProductProvider"/> implementation that gets products from the Commerce

   11     /// Server catalog.

   12     /// </summary>

   13     public class ProductProvider : IProductProvider

   14     {

   15         #region Properties

   16 

   17         /// <summary>

   18         /// Gets or sets the <see cref="ICatalogContextFactory"/> to use to create the catalog

   19         /// context.

   20         /// </summary>

   21         [Dependency]

   22         public ICatalogContextFactory CatalogContextFactory { get; set; }

   23 

   24         /// <summary>

   25         /// Gets or sets the <see cref="ProductMappingFactory"/> to use to create product mappings.

   26         /// </summary>

   27         [Dependency]

   28         public IProductMappingFactory ProductMappingFactory { get; set; }

   29 

   30         #endregion // Properties

   31 

   32         #region Methods

   33 

   34         /// <summary>

   35         /// Gets the specified product from the specified catalog.

   36         /// </summary>

   37         /// <param name="catalogId">The catalog identifier.</param>

   38         /// <param name="productId">The product identifier.</param>

   39         /// <returns>A <see cref="ProductEntityBase"/> representing the product.</returns>

   40         public ProductEntityBase GetProduct(string catalogId, string productId)

   41         {

   42             CatalogContext context;

   43             Product product;

   44             IProductMapping mapping;

   45             ProductEntityBase entity;

   46 

   47             context = CatalogContextFactory.CreateCatalogContext(); // Defined in an earlier post.

   48             product = context.GetProduct(catalogId, productId);

   49             mapping = ProductMappingFactory.CreateProductMapping(product.DefinitionName);

   50             entity = mapping.Map(product);

   51             return entity;

   52         }

   53 

   54         #endregion // Methods

   55     }

   56 

   57 }

(In production you'd want error handling, caching, etc here.)

This might seem like a lot of code, but it's relatively simple, easy to understand, and easy to test.  It also has two key benefits:

1. The business logic in the original example can be placed in the appropriate business entity classes, inherited, overridden, etc.

public abstract class ProductEntityBase

{

    #region Methods

 

    /// <summary>

    /// Does something.

    /// </summary>

    public virtual void DoSomething()

    {

        // ...

    }

 

    #endregion // Methods

 

    // ...

 

}

 

public abstract class HardGoodEntityBase : ProductEntityBase

{

    public override void DoSomething()

    {

        // Do something different when the product is a hardgood.

    }

}

2. The code can be easily extended to support new product types.  Adding a new product type is as simple as adding a new business entity class, a new mapping class, and the necessary configuration to map the definition name to the mapping:

public class GiftCardEntity : HardGoodEntityBase { /*...*/ }

 

public class GiftCardMapping : ProductMappingBase<GiftCardEntity> { /*...*/ }

 

<type type="MyCommerceApplication.Mappings.IProductMapping, MyCommerceApplication" mapTo="MyCommerceApplication.Mappings.GiftCardMapping, MyCommerceApplication" name="GiftCard">

  <lifetime type="singleton" />

</type>

There's no need to crack open the existing code and change banks of hard-coded if/else or switch statements, as in the original example.

Again, this is an extremely simple technique, but it's one I haven't seen many developers use.

Cheers,

Colin

posted on Sunday, August 23, 2009 7:39 PM Print
Comments
Gravatar
# re: Building Product Business Entities From Commerce Server 2007 Products
Andy
1/27/2010 8:48 AM
  
thanks

Post Comment

Title *
Name *
Email
Url
Comment *  
Please add 5 and 3 and type the answer here: