Automapper for the poor

After the first acquaintance with the library, many experienced a wow effect. Cool, you can map objects, you can write queries on top of DTOs (projections) and everything magically works (well, or you have to believe that it works). Isn’t this a miracle?

However, with experience, the disadvantages of using this library became obvious, and there are a sufficient number of them:

  • Implicit Code – auto-matic mapping, mapping by convention. Ironically, this is the recommended way. In this scenario, mapping is trite by property or field names, and conventions work additionally. If it is not very successful to name properties or give them erroneous types, no built-in validation will help. In fact, each mapping needs to be tested, which in itself will be another adventure. You can, yes, you can write a binding that can validate many things, but all this is associated with other disadvantages of this library, see below.

  • Support. The programmers who wrote this library do not tolerate dissent and are the truth in the first and last instance, yes, yes! API breaks systematically. Just think, thousands of projects are new and not very dependent on this library, yet the authors are unable to even apply the deprecation strategy. A typical example is one of the latest releases, which, in addition to fixing a critical bug on the .NET 7 platform, also contained backward-incompatible changes. The last straw that prompted me to think in general on this topic was this bug: https://github.com/AutoMapper/AutoMapper/issues/4205. For the sake of “cosmetics” compatibility with EF6 was broken (the practical value of these changes is about zero). It was said `Well, we target EF Core now.`, but try to find it, say, in some log or guide (when, and why, why?). Of course, well, who am I to tell them what to do, and I won’t.

  • Watching the development of this library, I notice how randomly added and disappeared methods, how fields change the scope. If you wish, you can find mutually exclusive advice from the authors on stackoverflow. At the same time, from the point of view of architecture, there is no need for this, one could simply allow some things that the automapper already uses internally. Specifically, hundreds of mappings are used in my rather big monolith (as it happened, the project is about 7 years old) and almost any change in the automapper breaks something. And the more you try to customize it, the more you will suffer with updates.

  • The API is another drawback. Over the years, it was possible to bring this part to perfection, but … By itself, mapping can be used simply from code or inside LINQ when querying the database, or both, obviously? But no, the authors give only two options out of three: only LINQ and only code (and maybe LINQ, if it does not fall). That is, there is no way at the API level to even really determine what we want and, accordingly, this is not checked in any way, but something simply cannot be done.

  • I guarantee that if you haven’t covered everything with tests, then your mappings contain error(s). It could be the unexpected work of conventions, incorrect types that only miraculously still work, maybe you’re mapping too much. As soon as you start explicitly prescribing your mappings, then you will definitely make at least a couple of discoveries.

Mapping can be divided into two parts: projections and their inverse actions (which I would call mapping itself). Further, I will write only about projections, in principle, it seems to me that it makes no sense to use an automapper without projections at all.

The first way for the poorest is to use extension methods:

public static Dto ToDto(this Entity entity)
{
    return new Dto
    {
        Id = entity.Id, 
        Name = entity.Name, 
        AnotherDto = new AnotherDto
        {
            Id = entity.Another.Id
        }
    };
}

OK, but what about requests? Well, you can make an extension method for IQueryable<>only reuse it, for example, for IEnumerable<> will be problematic. In the example above, the `AnotherDto` mapping is written right in the body of the method, and if it is used somewhere else, then you need to look for a way to declare this logic in one place. In the case of a regular method, this part can be moved to another extension method, but this number will not work with expression trees (just as the example itself will not work), the provider knows nothing about our methods and will not be able to convert the query into SQL. In other words, you need the ability to compose.

Let’s get straight to the point:

public interface IProjection
{
    LambdaExpression GetProjectToExpression();
}

public readonly struct Projection<TSource, TResult> : IProjection
{
    public Expression<Func<TSource, TResult>> ProjectToExpression => LazyExpression.Value;

    public Func<TSource, TResult> ProjectTo => LazyDelegate.Value;

    private Lazy<Func<TSource, TResult>> LazyDelegate { get; }

    private Lazy<Expression<Func<TSource, TResult>>> LazyExpression { get; }

    public Projection(Expression<Func<TSource, TResult>> expression)
    {
        // visitor и остальное приведу в гисте пожалуй
        LazyExpression = new Lazy<Expression<Func<TSource, TResult>>>(() => (Expression<Func<TSource, TResult>>) new ProjectionSingleVisitor().Visit(expression), LazyThreadSafetyMode.None);

        var lazyExpression = LazyExpression;

        // тут можно использовать на свой страх и риск FastExpressionCompiler
        LazyDelegate = new Lazy<Func<TSource, TResult>>(() => lazyExpression.Value.Compile(), LazyThreadSafetyMode.None);
    }

    internal Projection(Expression<Func<TSource, TResult>> expressionFunc, Func<TSource, TResult> delegateFunc)
    {
        LazyExpression = new Lazy<Expression<Func<TSource, TResult>>>(() => expressionFunc);
        LazyDelegate = new Lazy<Func<TSource, TResult>>(() => delegateFunc);
    }

    LambdaExpression IProjection.GetProjectToExpression()
    {
        return ProjectToExpression;
    }

    public static implicit operator Func<TSource, TResult>(Projection<TSource, TResult> f)
    {
        return f.ProjectTo;
    }

    public static implicit operator Expression<Func<TSource, TResult>>(Projection<TSource, TResult> f)
    {
        return f.ProjectToExpression;
    }
}

public static class ProjectionExtensions
{
    public static IQueryable<TDestination> Projection<TSource, TDestination>(this IQueryable<TSource> queryable, Projection<TSource, TDestination> projection)
    {
        return queryable.Select(projection.ProjectToExpression);
    }

    public static IEnumerable<TDestination> Projection<TSource, TDestination>(this IEnumerable<TSource> enumerable, Projection<TSource, TDestination> projection)
    {
        return enumerable.Select(projection.ProjectTo);
    }
}

You can declare projections like this:

    public static readonly Projection<Category, LookupDetails> CategoryLookupDetails = new(x => new LookupDetails
    {
        Id = x.Id,
        Name = x.Name,
    });
    
    public static readonly Projection<SubCategory, SubCategoryDetails> SubCategoryDetails = new(x => new SubCategoryDetails
    {
        Id = x.Id,
        Active = x.Active,
        Category = CategoryLookupDetails.ProjectTo(x.Category),
        Name = x.Name,
        Description = x.Description,
        CreatedDate = x.CreatedDate,
        ModificationDate = x.ModificationDate
    });

And use in queries:

    [Retryable]
    public virtual Task<Option<SubCategoryDetails>> GetAsync(long id)
    {
        return Repository
            .Queryable()
            .Projection(SubCategoryDetails)
            .SingleOptionalAsync(x => x.Id == id);
    }

No one bothers and just call SubCategoryDetails.ProjectTo(entity), if there is an entity in the hands. As you can see, the composition works, it’s funny, but even the generated SQL is almost identical compared to the automapper, only the order is different.

The idea is quite simple, the expressions are rewritten and instead of ProjectTo body substitution occurs ProjectToExpression.

Can this code replace the entire automapper? Of course not, but it is simple, it is yours and, if desired, lends itself to full customization and adding features.

Important: This version is not optimized or even fully tested.

Link to the rest of the code: gist.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *