-
Notifications
You must be signed in to change notification settings - Fork 1
Why mappings?
Why do we need mappings at all?
Well, you may not need them if you have some small simple project that doesn't require a lot of maintenance and ability to make your code as much reusable and extendable as it's possible to improve development efficiency.
But what if you have a lot of tasks that require modifications inside of already existing code or database structure changes happen quite often? What if you need to do database optimizations and you are going to refactor your database structure, but your business logic shouldn't be changed? In small application it's not a big deal, but when application grows you need to have some architecture in place to provide the simplest and painless way to achieve it.
We want to minimize amount of code that needs to be changed during refactorings. It will save development time and reduce possibility of creating a few additional bugs during implementation. To achieve it you may want to separate your Data Access Layer and Business Layer as much as possible and use 2 different groups of models. Models that are mirroring database structure (let's call them Entities) and models that will be used on Business Layer (let's call them Business Models). With described separation, if your database structure has changed, you just need to rewrite logic how you accessing the data and probably change structure of your Entities. Your business logic is located in a separate Business Layer and works with Business Models, neither of them require any modifications, so you just need to change how the new Entities structure is mapped on old Business Models. Both layers are only connected by intermediate layer of mappings.
You may ask yourself, why don't just write some simple methods like this:
public PersonModel ConvertPersonToPersonModel(Person entity)
{
return new PersonModel
{
// mapping logic goes here
}
}
public AddressModel ConvertAddressToAddressModel(Address entity)
{
return new AddressModel
{
// mapping logic goes here
}
}
Well, it will work. But there is one problem with this code: it's not generic. Why is it a problem? If you are not going to build some reusable or extendable functionality using your code then there is no actually any problem. But what if you have some repetitive logic in your business layer, some logic that is the same for most of the entities. Generic code provides reusability.
Let's say we have some table of data with pagination in it. When user press button we want to send data from the current page to some external API. This task contains two subtasks:
- get data from database for current page
- send this data to external API. API has its own format of input data.
Pagination is a quite generic task by its definition: you need to grab data from database, apply different filters and Skip / Take arguments to extract only one page of data:
public IQueryable<TEntity> Pagination<TEntity>(int skip, int take)
{
// pagination code goes here
}
Sending request to external API is a generic task too: we construct Business Model with the same format as requested by API and send this model using HttpClient:
public void SendRequest<TModel>(TModel model)
{
// sending request here
}
We can use both methods for every Entity and every Model in solution, because they are generic. Only one step that is left is how to convert our TEntity to TModel. In generic context we can't just use simple methods like ConvertPersonIntoPersonModel. We need some generic method like ConvertEntityToModel<TEntity, TModel>:
public TModel CovertEntityToModel<TEntity, TModel>(TEntity model)
{
// make magic
}
First way how we can achieve it is just to pass callback as an argument from external method, but it's a standard anti pattern that will create callbacks hell. What if we need 2 different mappings in this method? Or 5? And what if we want to build some another generic method that is going to use our current one, we will need to pass callbacks all the way down.
Another approach is to use some simple switch inside of generic method. But even average application usually has hundreds of mappings, so it's going to be a quite big switch statement. Plus switch statements aren't extendable...
Switch statements or standard methods like PersonModel ConvertPersonToPersonModel(Person entity) can't be extended from outside. Let's say that you want to move described logic into some separated external library, this library doesn't know anything about your entities and models.
So generic methods are required if we want to write easy extendable and reusable code. QueryMapping library provides the generic interface. Also it doesn't use any reflection.
Ok, that's fine, but the same API is provided by widely used AutoMapper library, isn't it? Yes, that's true. AutoMapper provides great generic interface, but AutoMapper also has a few drawbacks:
- Default AutoMapper behavior is killing one of the main goals of any mapping layer
Default AutoMapper behavior maps fields using name conventions: fields with the same names will be mapped together. If we use this behavior then we just kill one of the main advantages of mapping systems - it's not strongly typed mapping. If property type, format or name are changed, your code won't just work anymore and will fail in runtime.
- Overcomplicated API
Let's assume we are going to avoid above described drawback using non-default AutoMapper behavior and going to map properties between models explicitly.
cfg.CreateMap<CalendarEvent, CalendarEventForm>()
.ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.Date))
.ForMember(dest => dest.EventHour, opt => opt.MapFrom(src => src.Hour))
.ForMember(dest => dest.EventMinute, opt => opt.MapFrom(src => src.Minute))
We've only mapped 3 fields here, but the code looks really complex. API can be much simpler.
- Weakly typed conversions
Even when we use explicit mappings, it's not strongly typed and won't save us from errors in runtime: The next code will compile without problems even if YearString property can't be converted into Int type.
cfg.CreateMap<CalendarEvent, CalendarEventForm>()
.ForMember(dest => dest.YearInt, opt => opt.MapFrom(src => src.YearString))
- Reflection
AutoMapper is known for using a lot of reflection. The same interface can be achieved without using reflection.
- Is not optimized for IQueryable interface
AutoMapper is an object-object mapper. It's AutoMapper's advantage and disadvantage at the same time. On one had it's not tightly connected to IQueryable and can be used in projects that don't use Entity Framework at all, on another hand library that was written directly for usage with IQueryable interface will work better with IQueryable. QueryMapper also supports EF Context injections for the most complex cases when we need to write some custom joins or to do some optimization for generated by EntityFramework sql code (which sometimes can be really ugly).
- Doesn't support dynamic arguments inside of mapping
With QueryMapper you can pass arguments into mappings and use these arguments inside of expressions.
So is QueryMappings' goal to replace AutoMapper? No, it's definitely not. AutoMapper has other goal and project direction. AutoMapper's main goal is to reduce amount of code that developers have to write. QueryMappings' main goal is to provide easy to use and strongly-typed way to actually map models without reflection, even if you need to write more code with it. AutoMapper can be used with wide range of technologies, but it isn't optimized for usage with IQueryable. QueryMappings can be used only with Entity Framework, but has a full number of advantages when working with IQueryable interface.
In short - QueryMappings provides simple generic strongly typed interface optimized for work with IQueryable interface and written without using reflection in runtime.