Morphing UCommerce Products

Books I read
See all recommendations

The problem

Today I had a new but fun challenge with UCommerce. Turns out, as usual, it's a great fit for my whims with architecture. I was stuck between a rock and a hard place when I was looking at adding a custom pricing algoritm. I can't go into details, but there's custom client pricing involved of course. To add to the fun, we're mapping UCommerce products to DTOs for wire transfer. We could aslo have been mapping to view models or something else. To map we're using AutoMapper with quite a few configurations and jumps-through-hoops.

I had this code (ish):

var products = productRepository.Select().Where(SomePredicate);
var mapped = products.Select(Mapper.Map<ProductDto>);
return mapped;

I immediately thought of a few options:

  • Iterate over the products and change prices here
  • Create a Product adapter with additional logic and map from that
  • Execute the pricing logic from AutoMapper configuration

They all seemed weird and out of place though. None seemed like they would be easy to find for the next developer. Not even with unit tests. It just didn't seem right. Changing data on the entities would mean I'd have to go out of my way to ensure nobody went and saved those products later in the request. Creating an adapter would mean loads of new instances, bloated wrapper classes and weird names. And finally executing business logic from AutoMapper configuration means I'd been mixing responsibilities en mass.

UCommerce & NHibernate to the rescue

Luckily I've been using EntityFramework a lot and tried to force it into my Domain Driven Design patterns since it's infancy. I've been through the lot (and I enjoy it). So I kind of know what to expect from an ORM. When using UCommerce I'm stuck with NHibernate, but I haven't really been doing it justice by just leaving it in the background. (And fiddling with Entity Framework - which is just as good!) Together the two systems are extremely powerful. UCommerce have even documented the possibilities, though the documentation fails to point out the really juicy benefits.

We have ProductDefintion, right? It allows us to set up product types with different properties and variant options. It even supports inheritance. But we're still stuck with the Product class and its GetProperty() overrides. In my case, I'd like to have ProductWithFancyPricing so I could override that GetPrice() method. If I could have ProductWithFancyPricing and ProductWithEvenFancierPricing that would be totally awesome.

Turns out you can have your cake and eat it too. When properly using an ORM you can exploit OOP like it's supposed to and use polymorphism for varying behavior. It's possible to set up an inheritance tree so the mapper automatically handles creation of different types for you. You've basically got three options:

  • Table per concrete class (type)
    • All classes have a table of their own
    • Useful when base classes don't have (much) data
  • Table per hierarchy
    • One table per base class
    • Useful when all data is on the base class
  • Table per subclass
    • One common table for base data
    • Individual tables per derived class with only additional data
    • Useful when there are some data in both classes. (Think umbracoNode)

In my case, I don't need any new data on the derived classes. It's all there in GetProperty() anyway. (I will add some getters though. ModelsBuilder, anyone? )
So for me it's going to be Table per hierarchy. The rest of the options are all viable for this technique if you have other requirements. You can read a bit about it in the UCommerce docs.

Mapping some product types

(I inadvertently wrote document types there. ModelsBuilder, anyone?)

In order to have NHibernate treat products as subclasses with the Table per hierarchy strategy it needs a way to pick the right class for each record. That way is known as discriminator columns. I first thought I could just discriminate by the ProductDefinitionId, but it turns out NHibernate doesn't support discriminating on a column already in use for associations (foreign keys) or other means.
We have to add a column. I just call it "Discriminator" and make it a varchar.

alter table uCommerce_Product add Discriminator nvarchar(max)

Then we need some entities. I added a couple of docu... product types:

public class ProductWithFancyPricing : UCommerce.EntitiesV2.Product
{
    public override Money GetPrice(PriceGroup priceGroup)
    {
        var price = base.GetPrice(priceGroup);
        if (IsChristmas())
        {
            price = new Money(price.Value * 2, price.Culture, price.Currency);
        }
        return base.GetPrice(priceGroup);
    }
}

public class ProductWithEvenFancierPricing : UCommerce.EntitiesV2.Product
{
    public override Money GetPrice(PriceGroup priceGroup)
    {
        var blackMarket = ObjectFactory.Instance.Resolve<IBlackMarketService>();
        var priceValue = blackMarket.GetPrice(Sku);
        return new Money(priceValue, priceGroup.Currency);
    }
}

The next thing you need is to tell NHibernate that these are our new product classes:

public class ProductWithFancyPricingMapping : FluentNHibernate.Mapping.SubclassMap<ProductWithFancyPricing>
{
    public ProductWithFancyPricingMapping()
    {
        DiscriminatorValue("Product with fancy pricing");
    }
}

public class ProductWithEvenFancierPricingMapping : FluentNHibernate.Mapping.SubclassMap<ProductWithEvenFancierPricing>
{
    public ProductWithEvenFancierPricingMapping()
    {
        DiscriminatorValue("Product with naughty pricing");
    }
}

We also need to subclass UCommerce's mapping for Product in order to tell UCommerce which column to use as the discriminator:

public class ProductMap : global::UCommerce.EntitiesV2.Maps.ProductMap
{
    public ProductMap()
    {
        DiscriminateSubClassesOnColumn("Discriminator");
    }
}

Finally we need a class in the same assembly with a tag on it. More on that in the UCommerce docs.

public class MappingMarker : IContainsNHibernateMappingsTag
{
}

To have NHibernate pick the right classes now, we just need to fix the existing products if we have any. I have called my discriminator values the same as my document types, so I can easily construct a query as such:

update
    ucommerce_product
set 
    discriminator = case productdefinitionid
        when 10 then 'Product with fancy pricing'
        when 11 then 'Product with naughty pricing'
        else null
    end

Now if we go...

var products = productRepository.Select();

...we'll get a bounch of ProductWithFancyPricings and ProductWithEvenFancierPricing. If you have more types, you might get into trouble though. You need to have a discriminator on them all.

The final hurdle

So that's cool. That's really cool. But there's one hurdle we have to jump over before we can cross the goal line. From very nasty experiences I knew I had to test everything manually and integrated. So I went and tried to see what happened if I added a product through the UCommerce Admin UI.

boom :)

'Course it didn't work. It actually did, and didn't. Several weird things happened ranging from strange NHibernate mapping exceptions to products getting the discriminator "UCommerce.EntitiesV2.Product". (Which makes a lot of sense if you think about it)

The @#¤%& CreateCategoryOrProduct.as[p|c]x WebForms control is in our way. It instantiates a Product and saves it. It's completely sealed and unconfigurable. We could overwrite it with a custom one, but that would open another can of worms with regards to upgrading, source control and what-not. Luckily it's going away very very soon in UCommerce V8. (2018?)

After hacking at it a bit my final resolve was to add a step right after save in the product saving pipeline. Again, UCommerce is so versatile that even when it sucks, it's got a golden workaround right up its arm. If you're not familiar with UCommerce Pipelines, go read about it here.

Here's the extra configuration. (In a .config file included from UCommerce's custom.config)

<!-- PRODUCT CLASS FIX -->
<component id="SaveProduct"
           service="UCommerce.Pipelines.IPipeline`1[[UCommerce.EntitiesV2.Product, UCommerce]], UCommerce"
           type="UCommerce.Pipelines.Catalog.ProductPipeline, UCommerce.Pipelines">
  <parameters>
    <tasks>
      <array>
        <value>${Product.UpdateRevision}</value>
        <value>${Product.Save}</value>
        <value>${FixProductClass}</value>
        <value>${Product.IndexAsync}</value>
      </array>
    </tasks>
  </parameters>
</component>

<component id="FixProductClass"
           service="UCommerce.Pipelines.IPipelineTask`1[[UCommerce.EntitiesV2.Product, UCommerce]], UCommerce"
           type="My.Awesome.Site.Persistence.FixProductClassTask, My.Awesome.Site.UCommerce"/>

A pipeline task gets a reference to the entity being handled, so we can't just go and replace the entire product with an instance of the right type. But we can fake it and force the database value to be correct after saving. UCommerce uses NHibernate level 2 cache, so we need to flush that as well, but we'll get to that.

Forcing the database is fairly easy. We have to resort to good old ADO code, which was actually a joyful little deja-vu experience (although I'm glad it was brief):

public class FixProductClassTask : IPipelineTask<Product>
{
    private readonly IStatelessSessionProvider sessionProvider;

    public FixProductClassTask(IStatelessSessionProvider sessionProvider)
    {
        this.sessionProvider = sessionProvider;
    }

    public PipelineExecutionResult Execute(Product subject)
    {
        var command = sessionProvider.GetStatelessSession().Connection.CreateCommand();
        command.CommandText = "UPDATE uCommerce_Product SET Discriminator = @discriminator WHERE ProductId = @productId";
        command.CommandType = CommandType.Text;
        var discriminatorParam = command.CreateParameter();
        discriminatorParam.ParameterName = "discriminator";
        discriminatorParam.Value = subject.ProductDefinition.Name;
        var idParam = command.CreateParameter();
        idParam.ParameterName = "productId";
        idParam.Value = subject.Id;
        command.Parameters.Add(discriminatorParam);
        command.Parameters.Add(idParam);
        command.ExecuteNonQuery();

        // TODO: Clear cache

        return PipelineExecutionResult.Success;
    }
}

I'm sure a lot of sazzy devs out there could prettify this a bit, but it does the job. Insert a switch/case (please don't) or whatever you fancy if the product definition name isn't what you discriminate by. I'll leave it up to you to choose between strings, ints or even enums for performance vs. readability.

If you've turned off the level 2 cache, you might be fine with this. Otherwise we'd better "evict" the entity from the cache. We need to do that in order for the cached instance to change type from Product to ProductWithFancyPricing. Sadly the NHibernate SessionFactory in charge of doing this is hidden in an internal static field in UCommerce, so we need to resort to some nasty reflection to do it:

// ...
command.ExecuteNonQuery();

var fieldInfo = typeof(SessionProvider).GetField("_factory", BindingFlags.Static | BindingFlags.NonPublic);
if (fieldInfo == null) throw new Exception("SessionFactory instance has moved in this UCommerce version. %(");
var sessionFactory = (ISessionFactory)fieldInfo.GetValue(null);
sessionFactory.Evict(typeof(Product), subject.Id);

return PipelineExecutionResult.Success;
// ...

Et voilá! We can now save new products, and they immediately morph into the correct derived type. (Except for when being saved to RavenDB for the first time, ref. the config).

I'm now free to go back into the instances and implement however naughty pricing I fancy. \o/

Added bonuses

I already have a custom entity in the database and NHibernate model. It has two associations to Product. Had I realized what I had under my fingertips it would already have been collections on my new shiny subclasses.

I recon you noticed I referenced ModelsBuilder a couple of times. How 'bout having all your properties statically typed on your product instances. How about some interfaces?

I'm sure you're getting the drift.

I for one am quite embarrased I didn't think of this before. I've had the knowledge and tools for years. But there you go. We learn something every day. And I love doing it with Umbraco, UCommerce, EntityFramework and apparently now also... NHibernate. :)

Author

comments powered by Disqus