Wednesday, January 18, 2012

Mapping-by-Code - dynamic component

Dynamic component is a feature that allows to map non-generic key-value collection as a part of our entity, like ordinary properties.

Dynamic component was the hardest feature to map using mapping-by-code up to date. I was cursing at the lack of documentation or examples and struggling with NHibernate's source code for few hours and I was ready to show a white flag this time or assume that this feature is broken in mapping-by-code. Finally I've figured it out - it does work and it's quite neat, actually.

There's no separate method for defining dynamic components in mapping-by-code - overloads of the Component method are used. Here is the overload to be used with accessible IDictionary-typed property:

public void Component<TComponent>(
Expression<Func<TEntity, IDictionary>> property,
TComponent dynamicComponentTemplate,
Action<IDynamicComponentMapper<TComponent>> mapping)
where TComponent : class
{
this.RegisterDynamicComponentMapping<TComponent>(property, mapping);
}

I've pasted the whole code of this method to share my initial confusion. Second parameter, dynamicComponentTemplate, is not used at all and it didn't make a hint what TComponent should be. But it turned out to be very useful. Instead of specifying name and type for each key-value pair, we use this parameter to pass an anonymous class instance being a template. Each key from IDictionary should be defined as a property in the template and any value that allows to determine the proper type should be passed.

The third parameter are the mapping options. Thanks to the template and its anonymous but concrete type, we are now defining the properties mapping as if these are ordinary entity properties. Quite neat, isn't it?

My mapped entity has an IDictionary-typed property. I'm going to persist values for "Founded" and "Municipality" string-typed keys.

entity.Properties["Founded"] = 1497;
entity.Properties["Municipality"] = session.Load<Municipality>(1);

Here's how to map it:

Component(x => x.Properties, new
{
Founded = 0,
Municipality = default(Municipality)
}, dc =>
{
// dynamic component members mappings
dc.Property(x => x.Founded);
dc.ManyToOne(x => x.Municipality);
// etc.

// dynamic component options
dc.Access(Accessor.Property);
dc.Insert(true);
dc.Update(true);
dc.OptimisticLock(true);
dc.Unique(true);
});

My IDictionary-typed property called Properties is specified in the first parameter. "Founded" and "Municipality" keys are defined as properties in the template class (second parameter). Corresponding values from the dictionary will be persisted correctly, thanks to the members mappings defined in the third parameter. Properties dictionary can contain anything - collections, other components etc. - all the entity-level mappings are available here, too.

There are also a few options that define behaviors for the component as a whole, like Access or OptimisticLock. These can be redefined at members mappings options, as usual.

Fluent NHibernate's equivalent

Mapping dynamic components in FNH is easier to figure out, but not so clever - key strings and types needs to be defined explicitly.

DynamicComponent(x => x.Properties, dc =>
{
// dynamic component members mappings
dc.Map<int>("Founded");
dc.References<Municipality>(x => x["Municipality"]);

// dynamic component options
dc.Access.Property()
.Insert()
.Update()
.OptimisticLock()
.Unique()
.ParentReference(x => x["Parent"])
.ReadOnly();

dc.SqlDelete("SQL command");
dc.SqlDeleteAll("SQL command");
dc.SqlInsert("SQL command");
dc.SqlUpdate("SQL command");
});

ReadOnly is just a FNH's shortcut for setting both .Not.Insert() and .Not.Update().

This FNH mapping suffers for the same bug as in case of FNH's Component mapping. It is not possible to map ParentReference or SQL queries for dynamic component and FNH should not expose these options as they have no way to work correctly. In fact, SQL queries are ignored and defining ParentReference causes XML validation error.

3 comments:

  1. Micha Schopman (famschopman at gmail.com)January 18, 2012 at 10:20 PM

    Adam,

    Yup, documentation really really sucks. I've collected a working set of various implementations by trial & error and alot of googling. :)

    In this case you modified the mapping to reflect your domain object (the properties founded and municipality) but what if you don't know each property in advance and want to apply a generic mapping.

    So let's say I just want to persist an object with custom properties which inherits from parentobject and parentobject has a generic mapping?

    Would that be possible in the way you describe?

    ReplyDelete
  2. I think that dynamic component can't be really dynamic at both object and database level. Note that the component parts are stored as ordinary columns, so we need to know its list. There was similiar discussion in Ayende's post I've linked to, and he suggests to use Map mapping for that (which is just more generic key-value).

    ReplyDelete
  3. Today I can't say there isn't any documentation . You wrote an excelent documentation in your posts. Thanks a lot.

    ReplyDelete