The purpose of enmap is to provide a mapping framework that allows for easy composibility between entity types and flexible translation of database properties into domain properties. In particular there are several key areas that are difficult to implement with other mapping frameworks and/or awkward to implement without a mapping framework in the first place:
- Translating database values to model values with a step that requires unencumbered leveraging of normal C# syntax without having to worry about expression tree limitations.
- Batching up relationships into separate queries. Unfortunately, when you project container properties into your entity
framework projections, the default implementation forces an implementation that only requires one SQL query. While this
notion is admirable in the abstract, in practice it produces queries with an enormous amount of wasted replication. If
a
person
has 10 addresses, then each row in the result set will include all the properties ofperson
and all the properties ofaddress
. This produces gigantic queries that are both difficult to parse and more expensive to ultimately run.
All mappers must be contained in a registry. There are two ways to do this:
- Subclass
MapperRegistry
and overrideRegister
- Instantiate
MapperRegistry
and pass to the constructor a delegateAction<MapperRegistry<TContext>>
Either way you are responsible for registering new mappers using this protocol. For simplicity the following examples will
assume you are subclassing and overriding Register
, but everything that follows applies to either technique. To register
a mapper without defining any properties, you'd use:
Map<TEntityFrameworkType, TDomainType>();
But unlike some mappers such as Automapper, we deliberately do not offer any conventinon-based implementations. Enmap -- by default -- requires you to specify each translation explicitly. This is because it's a better long-term solution as it forces you to declare each translation in a way that can be caught by refactoring tools. If your rename one property or another, proper refactoring tools will take care of this for you; but if you try to refactor by hand in these scenarios, you'll get compiler errors in these mapping definitions.
Therefore, to map any property, you have to explicitly define the mapping. Say you want to say the Id
property of the
entity type maps to the Id
property of the domain type. To do so, you must declare the translation:
Map<TEntityFrameworkType, TDomainType>()
.For(x => x.Id).From(x => x.Id);
Here we define a simple mapping translation that copies the value Id
from the database type to the property Id
of the domain
type. These translations are extremely typical. And if this were the only sort of translation offered by the mapping framework
it would not be terribly useful.
The key aspect in which a framework such as this is useful is that it can handle sub-relationship in a simple and concise way that encourages you to define mappings using this framework, rather than making you feel this is just a chore. Aside from the aforementioned simple mapping, there are five other types:
- Inline single entity relationships (i.e. a FK to another table)
- Inline container entity relationships (you have a collection property representing a relationship)
- Fetch single entity relationships, meaning that a separate query will be run to pull all the entities of that particular type that were referenced from the containing query.
- Fetch container entity relationships, with the same separate-query meaning as defined above.
- Ad-hoc fetching of batches of objects of a particular type based on an id. This is similar to the fetch based techniques outlined above except may apply to getting objects from any data source at all.
By default, a fetch based approach is used for containers, since inlining container relationships usually produces wildly inefficient queries, since all the columns from the parent must be duplicated for each row of the child. Similarly, single entity relationships are by default mapped using the inline technique, since reducing the number of queries is beneficial and inlining the relationship usually has little extra cost.
Finally, let's look at some examples of each of those techniques.
For an inline single entity relationship, consider a DbPerson
entity with a reference to a DbAddress
entity:
public class DbPerson
{
public int Id { get; set; }
public int AddressId { get; set; }
public DbAddress Address { get; set; }
}
public class DbAddress
{
public int Id { get; set; }
public string Street { get; set; }
}
And with corresponding model types:
public class Person
{
public int Id { get; set; }
public Address Address { get; set; }
}
public class Address
{
public int Id { get; set; }
public string Street { get; set; }
}
The mapping for this would look like:
Map<DbPerson, Person>()
.For(x => x.Id).From(x => x.Id)
.For(x => x.Address).From(x => x.Address);
Map<DbAddress, Address>()
.For(x => x.Id).From(x => x.Id)
.For(x => x.Street).From(x => x.Street);
The actual Entity Framework projection might look a bit like:
dbContext.Persons.Select(x => new Person
{
Id = x.Id,
Address = new Address
{
Id = x.Address.Id,
Street = x.Address.Street
}
});
Importantly, the mapping for Address
is composable in a way the raw Entity Framework projection is not. Without dynamically
generating these expression trees, it's not possible to reuse projections for relationships like Address
.
To continue with our previous example, let's change the mapping for DbPerson
to:
Map<DbPerson, Person>()
.For(x => x.Id).From(x => x.Id)
.For(x => x.Address).From(x => x.Address).Fetch();
Since the default behavior is inline for this type of relationship, we need to explicitly indicate we want to use the fetch-based behavior. Doing this changes the projection above to something like:
var persons = dbContext.Persons.Select(x => new Person
{
Id = x.Id,
AddressId = x.AddressId
});
var addresses = dbContext.Addresses.Select(x => new Address
{
Id = x.Id,
Street = x.Street
});
Without going into too much detail, the addresses are interleaved back into the Person
model type after both queries
have been executed. There are a variety of situations -- particularly with especially complicated mappings -- where
using a fetch based approach (and therefore multiple queries) will actually improve performance.
The two container-based approaches are very similar to the single entity approaches. However, as mentioned earlier,
inlining container relationships is almost always very inefficient. Therefore, the default is to use a fetch-based
approach. To force it to use the inline behavior, you must override the mapping with .Inline()
in an analogous
way that you had forced it to use .Fetch()
for the single entity scenario.
Sometimes you may have an id in your tables that represent an entity or object that does not actually exist in the
database. Perhaps it's a key to a key/value store and all you have in the database is a GUID string. To facilitate
these scenarios, you can provide your own batch processing that works in a similar fashion to the fetch-based approaches
outlined above. To accomplish this, you must create a class that is responsible for fetching the external data. This
should be an implementation of IBatchProcessor
and implement its one method:
Task Apply(IEnumerable<IBatchFetcherItem> items, MapperContext context);
The items
parameter contains a sequence of IBatchFetcherItem
, which is just a contract for an object that specifies:
- The object implementing
IBatchProcessor
. (i.e. the class you are in the process of implementing) - The id representing the object you are trying to fetch (the value in the key/value store, say)
- A callback named
ApplyFetchedValue
that is invoked when the fetched item has been obtained.
So to implement this method, you would grab all the entity ids (items.Select(x => x.EntityId)
) and make a call
to your key/value store to obtain the values for all those keys. Then for each value returned, you need to find the
corresponding IBatchFetcherItem
and call its ApplyFetchedValue
method with the value.
Once you have set up the your batch processor, you can use it in your mapping configurations. For example, suppose you have an entity such as:
public class DbNotification
{
public int Id { get; set; }
public string PostId { get; set; }
}
And model types such as:
public class Notification
{
public int Id { get; set; }
public Post Post { get; set; }
}
public class Post
{
public string Id { get; set; }
public string Title { get; set; }
}
To set up a mapper for this using your batch processor, you'd have something like:
var batchProcessor = new PostsBatchProcessor();
Map<DbNotification, Notification>()
.For(x => x.Id).From(x => x.Id)
.For(x => x.Post).Batch(batchProcessor).Collect(x => x.PostId);
This will set up a mapper that collects all the PostId
s and then, via your implementation of IBatchProcessor
,
fetches all the posts at once from the key/value store and subsequently interleaves the results into the Post
property
of Notification
.
One of the most common problems with other mappers is they don't provide any straightforward way to further transform the
database value into the type expected in the model. For example, you might store a TimeSpan
value in the database as
an int that represents seconds. However, your model type might choose to surface this value as a TimeSpan
. If you want
to do this, you need to ensure that the TimeSpan
transformation only happens after the projected value has been resolved.
With Enmap, this is trivial. Supposing you have the following types:
public class DbJob
{
public int Id { get; set; }
public int Period { get; set; }
}
public class Job
{
public int Id { get; set; }
public TimeSpan Period { get; set; }
}
Here it's not straightforward to build a SQL projection that honors this mapping. For example, this is illegal:
var jobs = db.Jobs.Select(x => new Job
{
Id = x.Id,
Period = TimeSpan.FromSeconds(x.Period)
});
The reason is that TimeSpan.FromSeconds
is not "translatable to SQL". However, all you really want from the
database is the Period
in seconds. Once the query is complete, you just want to perform a translation that converts
the int value in seconds to a TimeSpan
value. With Enmap, you simply need to use the .To(...)
operator. The
contents of the function passed to .To
is executed after the query has been executed -- meaning no SQL translation
errors will happen. Here's an example of what that mapping might look like:
Map<DbJob, Job>()
.For(x => x.Id).From(x => x.Id)
.For(x => x.Period).From(x => x.Period).To(x => TimeSpan.FromSeconds(x));
The .From(...)
clause is responsible for managing the SQL projection, and thus must be fairly simple, such as the
aforementioned property reference. In contrast, the .To(...)
method is run in a normal C# context, and thus calling
TimeSpan.FromSeconds(...)
is perfectly valid.
The final result is that you get to define all these translations in a clean way that fully encapsulates various translations at the precise place where the mapping is declared in the first place.
Limitations:
- You are guaranteed that the your
.After
operator is invoked after the normal SQL projection has occurred. However, there is no guarantee that your particular.After
operator has been invoked before or after any others. Furthermore, any fetch-based results are only reified after all the.After
operators for every entity has been executed. So bear in mind this is only useful for immediate values -- those values that are immediately obtained from the result of the EF query.