Show Blogger Panel Hide Blogger Panel
Alex Yakunin

September 23, 2009

Object-to-object (O2O) mapper is our upcoming solution for POCO and DTO

I write this post mainly because I'm tired to listed complains related to necessity to support POCO and DTOs in ORM. Earlier I wrote this is not a real problem at all, if your ORM is capable of populating some objects. So here I'll simply prove this on examples.

So what object-to-object mapper (OOM) is? In the simplest case this is an API allowing to transform objects of types T1, T2, ... TN to objects of type T1`, T2`, ... TN` using pre-defined transformation rules (mappings). An example of such a simple API is e.g. AutoMapper (I recommend you to study its description before reading further).

On the other hand, I don't believe into such a simplicity ;) AutoMapper, as well as many similar mappers resolve just a single listed problem: forward-only mapping. I'd like such a tool handles few more cases:
  • It must be able to compare two T` graphs, identify the changes made there, and apply them to T graph. It must understand object keys, versions and removal flags while doing this.
  • It must be able to transform IQueryable<T> to IQueryable<T`>.
Let's think how we could work:
// Creating mapper. Let's think it is already configured.
var mapper = ...; 

// Transform a single object
var personDto = (PersonDto) 
  mapper.Transform(person); 

var personDtoClone = Cloner.Clone(personDto);
personDto.Name = "New name";

// Applying changes to the original object
mapper.Update(personDtoClone, personDto);

// Transforming the queryable
var personDtos = (IQueryable<PersonDto>)
  mapper.Transform(Query<Person>.All);

// Nothing has happened yet: we just provided a queryable
// that can be invoked later

// Here we're actually transforming the queryable to 
// the original one, executing it, transforming its
// result back to PersonDto objects and returning them.
var selectedPersons = (
  from p in personDtos
  where p.Name == "Alex"
  select p).ToList();

var selectedPersons2 = (
  from p in personDtos
  where p.Name == "Sergey"
  select p).ToList();

// Merging two lists of objects!
// This is possible, because we aware about keys.
// Conflicts are detected, because we aware about versions :)
selectedPersons = mapper.Merge(selectedPersons, selectedPersons2)

var selectedPersonsClone = Cloner.Clone(selectedPersons);
selectedPersons[0].Name = "Ivan";

// Applying changes to the original objects
mapper.Update(selectedPersonsClone, selectedPersons);
As you see, this solution allows us to solve the whole bunch of problems:
  • You can deal with POCO objects, your own DTOs - anything you want. There are no any special requirements.
  • This is simply ideal for SOA: Astoria (ADO.NET Data Services), .NET RIA Services, WCF, etc.
  • You may have as many of such mappings as you want. E.g. one per each particular client-side API ;)
  • You can use LINQ for your DTOs - that's simply a dream ;) Btw, writing such a translator must be really a peace of cake (there are always one-to-one mappings).
  • You shouldn't sacrifice all the benefits our Entity\Structure\EntitySet objects provide - I mean change tracking, lazy loading, auto transactions, validation, etc.!
Let's think how typical SOA context could look like:
public sealed class SoaContext : MappedStorageContext
{
  [MapTo(typeof(Person)]
  public IQueryable<PersonDto> Customers { get; }
  // We can implement it by standard way using PostSharp ;)

  [MapTo(typeof(Order)]
  public IQueryable<OrderDto> Orders { get; }

  public override void Initialize() 
  {
    // Executed just once per each type!
    
    // Only complex mappings are here.
    // 1-to-1 field mappings are defined automatically.
    // Type mappings are recognized from above properties.
    Map<Customer, CustomerDto>(
      c => c.Order.Total,
      dc => dc.OrderTotal);
    Map<Customer, CustomerDto>(
      c => c.Orders,
      dc => dc.Orders.Where(
        o => o.Date.Year==DateTime.Now.Year)
  }

  public SoaContext(Session session)
    : base(Session)
  {
  }
}

Note that this is EF-like context, that can be shared e.g. via ADO.NET Data Services API. Moreover, it provides Update & Merge methods allowing to update original objects or merge state changes - recursively.

I used MappedStorageContext here, which is "pre-tuned" for dealing with our own objects - e.g. it returns Query<T>.All for any auto property of mapped IQueryable<T> type and it is aware about Session. But it should be inherited from general MappedContext allowing you to map any objects as you like in similar fashion.

I hope this fully explains why I believe ORM must not care too much about supporting POCO and quite flexible mapping. These problems are solved by described OOM layer much better. Moreover, if they're resolved at different layer, the code of your Entities becomes more convenient and simple, because ORM can provide standard infrastructure for them.
  • In many cases (e.g. in web applications or simple services) you don't need POCO/DTO at all. All you need here is ability to deal with persistent entities using fast, simple and convenient API. Moreover, this API must be ideal for describing BLL rules. That's exactly what DO is designed for.
  • If you must maintain disconnected state for long-running transactions, upcoming DisconnectedState (and later - sync) will handle this gracefully. This problem significantly differs from DTOs - e.g. having on-demand downloading capability is quite desirable here.
  • Everything else (SOA, WCF serialization, etc.) is covered by this hard and fast solution.
Any comments are welcome. Especially if you see any problems here ;)

P.S. When this mapper will appear in DO4? Quite likely, we'll start working on it right after upcoming v4.1 update.