DEV Community

jpeg729
jpeg729

Posted on • Edited on

AutoMapper: converting entities to views at runtime

I have put off writing this post for quite some time since I didn't have a viable alternative and others have written about the problems they have experienced with AutoMapper. For instance Why I don't use AutoMapper.

Now I do have an alternative, so here goes.

What AutoMapper does

When you use an ORM (object relational mapper) to load data from your database, you often end up loading data into objects (which I will call entities) containing way more data than you actually need at that moment. Automapper lets you transform those objects into smaller data transfer objects (DTOs) more adapted for your need at that moment.

For example, you could have Book and Author classes like this.

public class BookEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string ISBN { get; set; }

    public AuthorEntity Author { get; set; }
}

public class AuthorEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string PreferedGenre { get; set; }

    public List<AuthorEntity> Books { get; set; }
    // I put "List" for ease of understanding, but 
    // EntityFramework 6 actually requires an "ICollection".
}
Enter fullscreen mode Exit fullscreen mode

And you might want to send only the following

public class AuthorWithBooksDto
{
    public string Name { get; set; }
    public List<BookDto> Books { get; set; }
    public int BooksCount { get; set; }

    public class BookDto
    {
        public string Name { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

I put the BookDto class inside the AuthorWithBooksDto in order to avoid reusing it elsewhere in the codebase.

So you define a map using AutoMapper

public class AuthorProfile : Profile
{
    public AuthorProfile()
    {
        CreateMap<AuthorEntity, AuthorWithBooksDto>();
    }
}
Enter fullscreen mode Exit fullscreen mode

And you use it like this...

var authorDto = AutoMapper.Mapper.Map<AuthorWithBooksDto>(authorEntity);
Enter fullscreen mode Exit fullscreen mode

Or even better you can use AutoMapper's queryable extensions to only load the required properties.

var authorDtos = dbContext.Authors.ProjectTo<AuthorWithBooksDto>();
Enter fullscreen mode Exit fullscreen mode

The mapping will be translated into SQL in order to optimise the data transfer.

Upsides

  • It is super easy to use.
  • It works nicely with EntityFramework and other ORMs.
  • It works pretty well.
  • It is super easy to use (Did I already say that?)

Downsides

  • If your mappings aren't defined quite right you can get cryptic runtime errors, such as OutOfMemory, or NullReferenceException's that can't tell you exactly what was null.
  • Since you define the mappings without referring to all the properties that get mapped, your editor cannot tell you exactly which properties are used and where.

Alternatives

AutoMapper does a great job, but it does it at runtime and I think we can do better.

I don't want to have to write all my mappings by hand, but I do want my code editor to be able to tell me exactly where each property is used, and I do want an easy equivalent to AutoMapper's query projection extensions.

This means I need a tool that can generate code for the mappings when I am writing the code. It also has to be able to generate mappings as C# Expressions because that is what EntityFramework uses to generate its SQL queries.

The MappingGenerator extension

If you use Visual Studio 2017+ you can use the MappingGenerator which as its name suggests, generates mappings.

Here is how it works...

  1. Type out the map function's signature

    public AuthorWithBooksDto MapFrom(AuthorEntity author)
    
  2. Press Ctrl+. at the end of the line and choose the proposed code fix
    map generation at work

  3. Check the generated code

    public AuthorWithBooksDto MapFrom(AuthorEntity author)
    {
        return new AuthorWithBooksDto()
        {
            Name = author.Name,
            Books = author.Books.Select(authorBook => new AuthorWithBooksDto.BookDto()
            {
                Name = authorBook.Name
            }).ToList(),
            BooksCount = author.Books.Count
        };
    }
    

Of course, you are going to tell me that isn't an Expression mapping and it can't be used with EntityFramework, and you would be right, but watch this...

  1. Type out the signature, it is a little more complicated, I will grant you that

    public Expression<Func<AuthorEntity, AuthorWithBooksDto>> MapAuthorWithBooks =
        entity => new AuthorWithBooksDto { };
    
  2. Put the text cursor in between the curly braces, press Ctrl+., and choose "Initialize with lambda parameter".
    expression map generation at work

  3. Check the generated code

    public Expression<Func<AuthorEntity, AuthorWithBooksDto>> MapAuthorWithBooks =
        entity => new AuthorWithBooksDto
        {
            Name = entity.Name,
            Books = entity.Books.Select(entityBook => new AuthorWithBooksDto.BookDto()
            {
                Name = entityBook.Name
            }).ToList(),
            BooksCount = entity.Books.Count
        };
    
  4. Test its use

    var query = db.Authors.Select(MapAuthorWithBooks);
    Console.WriteLine(query.ToString()); // only the required columns are included
    var authorsWithBooks = query.ToList();
    

What do we gain?

  • Visual Studio no longer tells us that the properties of our entities have zero references.
  • No more memory overflows

What problems remain?

  • If a null reference exception happens during the mapping Visual Studio 2017 can't tell you which precise line caused it, so you then have to do things like commenting out most of the mapping until you find the culprit. I imagine nullable reference types will help resolve this problem.
  • If you want Expression maps and compiled maps you either have to generate code for both, or you have to compile the expression at runtime, in which case you loose the ability to set breakpoints inside the mapping code.
  • Organising your maps becomes a bigger problem than it was. Adding a static method MapFrom to each Dto class might be a great idea.

Conclusion

AutoMapper is great, and while I can reproduce much of what I can do with AutoMapper using generated mapping code, I am not certain that it is a huge advantage.

The main use case for mappings seems to be generating models to send over the wire, and there are many alternative approaches. gRPC and GraphQL are alternatives that may have their own advantages.

Even if you prefer using AutoMapper, do try out the MappingGenerator extension because it has many useful capabilities.

Top comments (0)