Home > Software design >  How to create generic mapper like this?
How to create generic mapper like this?

Time:07-06

I have many classes, that have similar operations of mapping objects. To keep code dry I want to create base abstract class for all these classes. base abstract class would have generic mapping function like this:

public TEntity GetEntity(Result<TServiceClientEntity> res) 
{
    entity = MappingProfiles.TryMap(res.Value);
    //some logic with result here
    return entity;
}

Result class:

public class Result<T>
{
    public T Value{ get; set; }
    //some more properties..
}

But the problem is that I can't think a way of how to map generic classes like that:

public static class MappingProfiles
{
    public static T2 TryMap<T,T2>(T t) 
    {
        return (T2)Map((Real_T_type)t); //f.e.: the type is ExampleFrom
    }

    public static ExampleTo Map(ExampleFrom from)
    {
        return new ExampleTo
        {
            exapleValue = from.exapleValue
        };
    }
}

EDIT:

I also want that TryMap generic method use my predefined Map manual methods for mapping.

CodePudding user response:

You can use Reflection (C#) to accomplish such things:

public static TOut Map<TIn, TOut>(TIn source)
    where TOut : new()
{
    var inPropDict = typeof(TIn).GetProperties()
        .Where(p => p.CanRead)
        .ToDictionary(p => p.Name);
    var outProps = typeof(TOut).GetProperties()
        .Where(p => p.CanWrite);
    var destination = new TOut();
    foreach (var outProp in outProps) {
        if (inPropDict.TryGetValue(outProp.Name, out var inProp)) {
            object sourceValue = inProp.GetValue(source);
            if (inProp.PropertyType != outProp.PropertyType) {
                sourceValue = Convert.ChangeType(sourceValue, outProp.PropertyType);
            }
            outProp.SetValue(destination, sourceValue);
        }
    }
    return destination;
}

Reflection enables you to inspect a type and to get its properties, fields, etc.

Type.GetProperties() returns an array of PropertyInfo with name, type, and other information about a property. It also allows you to read from or to write to a property of an object.

The code above is just a quick and dirty example without exception handling. It does only a flat mapping and does not map collections or nested objects. It also could be improved by allowing you to declare mappings for properties not having the same name etc.

There is a tool doing all these things and more called AutoMapper.


Solution with manual mapping methods

I suggest defining an interface like this

public interface IMapper<T1, T2>
{
    T2 Map(T1 input);
}

Example of a concrete implementation:

public class ExampleFromToMapper : IMapper<ExampleFrom, ExampleTo>
{
    public ExampleTo Map(ExampleFrom input)
    {
        return new ExampleTo {
            ExampleValue = input.ExampleValue
        };
    }
}

The idea is to use dependency injection to do the job of selecting the right mapper.

You can use the NuGet package Microsoft.Extensions.DependencyInjection as an example. But many other dependency injection frameworks exist.

Write a method configuring the mappers (as extension method in this example):

public static IServiceCollection AddMappers(this IServiceCollection services)
{
    return services
        .AddSingleton<IMapper<ExampleFrom, ExampleTo>, ExampleFromToMapper>()
        .AddSingleton<IMapper<OtherFrom, OtherTo>, OtherFromToMapper>();
}

Define the container somewhere:

public static class Config
{
    public static ServiceProvider Container { get; set; }
}

And at startup of your application configure the container

var services = new ServiceCollection();
services
    .AddMappers()
    .AddTransient<MyForm>(); // See below
Config.Container = services.BuildServiceProvider();

As an example, let us assume that you have a WinForms app with a form defined like this (it uses a mapper directly, but instead it could use other services that do use mappers. The DI container resolves the dependencies recursively and injects them in the constructors automatically):

public partial class MyForm : Form
{
    private readonly IMapper<ExampleFrom, ExampleTo> _mapper;

    public MyForm(IMapper<ExampleFrom, ExampleTo> mapper)
    {
        _mapper = mapper;
        InitializeComponent();
    }
}

Now, you can start the application like this:

var frm = Config.Container.GetRequiredService<MyForm>();
Application.Run(frm);

Okay, at the beginning it looks complicated, but once you have set up the basics it becomes easy to add new services. Every class offering some functionality is considered a service.

  • Related