Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use generic Profile with Automapper and Asp.Net Core Dependency Injection

I would like to create the .Net Core class library that will be contains following extension method:

public static class MyServiceExtensions
    {
        public static IServiceCollection AddMyService<TUserDto, TUserDtoKey, TUser, TUserKey>(this IServiceCollection services)
            where TUserDto : UserDto<TUserDtoKey>
            where TUser : User<TUserKey>
        {
            services.AddAutoMapper(config =>
            {
                config.AddProfile<UserMappingProfile<TUserDto, TUserDtoKey, TUser, TUserKey>>();
            });

            return services;
        }
    }

I have following Automapper Profile:

public class UserMappingProfile<TUserDto, TUserDtoKey, TUser, TUserKey> : Profile 
        where TUserDto : UserDto<TUserDtoKey>
        where TUser : User<TUserKey>
    {
        public UserMappingProfile()
        {
            CreateMap<TUserDto, TUser>(MemberList.Destination)
                .ForMember(x => x.Id, opts => opts.MapFrom(x => x.UserId));

            CreateMap<TUser, TUserDto > (MemberList.Source)
                .ForMember(x => x.UserId, opts => opts.MapFrom(x => x.Id));
        }
    }

These entities:

public class UserDto<TKey>
    {
        public TKey UserId { get; set; }

        public string UserName { get; set; }
    }

public class User<TKey>
    {
        public TKey Id { get; set; }

        public string UserName { get; set; }
    }

public class MyUser : User<int>
    {
        public string Email { get; set; }
    }

public class MyUserDto : UserDto<int>
    {
        public string Email { get; set; }
    }

If I try to use it like this:

services.AddMyService<MyUserDto, int, MyUser, int>();

I get this error:

{System.ArgumentException: Cannot create an instance of GenericMapping.Services.Mapping.UserMappingProfile4[TUserDto,TUserDtoKey,TUser,TUserKey] because Type.ContainsGenericParameters is true. at System.RuntimeType.CreateInstanceCheckThis() at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean wrapExceptions, Boolean skipCheckThis, Boolean fillCache) at System.Activator.CreateInstance(Type type, Boolean nonPublic, Boolean wrapExceptions) at AutoMapper.Configuration.MapperConfigurationExpression.AddProfile(Type profileType) in C:\projects\automapper\src\AutoMapper\Configuration\MapperConfigurationExpression.cs:line 44 at AutoMapper.ServiceCollectionExtensions.<>c__DisplayClass10_0.<AddAutoMapperClasses>g__ConfigAction|4(IMapperConfigurationExpression cfg) in C:\projects\automapper-extensions-microsoft-dependencyinjectio\src\AutoMapper.Extensions.Microsoft.DependencyInjection\ServiceCollectionExtensions.cs:line 83 at AutoMapper.MapperConfiguration.Build(Action1 configure) in C:\projects\automapper\src\AutoMapper\MapperConfiguration.cs:line 307 at AutoMapper.ServiceCollectionExtensions.AddAutoMapperClasses(IServiceCollection services, Action1 additionalInitAction, IEnumerable1 assembliesToScan) in C:\projects\automapper-extensions-microsoft-dependencyinjectio\src\AutoMapper.Extensions.Microsoft.DependencyInjection\ServiceCollectionExtensions.cs:line 89 at GenericMapping.Services.Extensions.MyServiceExtensions.AddMyService[TUserDto,TUserDtoKey,TUser,TUserKey](IServiceCollection services) in C:\Projects\GenericMapping\GenericMapping.Services\Extensions\MyServiceExtensions.cs:line 14 at GenericMapping.Startup.ConfigureServices(IServiceCollection services) in C:\Projects\GenericMapping\GenericMapping\Startup.cs:line 33}

How can I fix this issue?

like image 940
Jenan Avatar asked Aug 20 '18 09:08

Jenan


People also ask

Does AutoMapper work with .NET Core?

AutoMapper is a ubiquitous, simple, convention-based object-to-object mapping library compatible with.NET Core. It is adept at converting an input object of one kind into an output object of a different type. You can use it to map objects of incompatible types.

Is AutoMapper a singleton?

Your configuration (e.g. Automapper Profiles) are singletons. That is, they are only ever loaded once when your project runs.

What is AutoMapper in asp net core?

What is AutoMapper? AutoMapper is a simple library that helps us to transform one object type into another. It is a convention-based object-to-object mapper that requires very little configuration. The object-to-object mapping works by transforming an input object of one type into an output object of a different type.


1 Answers

The root cause of your issue is the incorrect usage of the AddAutoMapper extension method. This method scans assemblies for profiles (and other AutoMapper components) and registers an IMapper component in the DI container using the configuration found. (I suggest you taking a look at its sources to understand what's going on exactly under the hood.)

You get the exception because AddAutoMapper finds the UserMappingProfile class but has no clue how to instantiate it as it has 4 open type arguments.

The easiest way of resolving the issue is to make your generic profile class abstract and to subclass it with the desired type arguments:

public abstract class UserMappingProfile<TUserDto, TUserDtoKey, TUser, TUserKey> : Profile
    where TUserDto : UserDto<TUserDtoKey>
    where TUser : User<TUserKey>
{
    public UserMappingProfile()
    {
        CreateMap<TUserDto, TUser>(MemberList.Destination)
            .ForMember(x => x.Id, opts => opts.MapFrom(x => x.UserId));

        CreateMap<TUser, TUserDto>(MemberList.Source)
            .ForMember(x => x.UserId, opts => opts.MapFrom(x => x.Id));
    }
}

public class UserMappingProfile : UserMappingProfile<MyUserDto, int, MyUser, int> { }

Now you don't need MyServiceExtensions at all, just a services.AddAutoMapper() call and your configuration will be picked up automatically.

However, if you insist on doing the configuration using your own extension methods, you have to avoid AddAutoMapper as it's intended to be called only once. Instead of scanning assemblies for Profile classes, you can provide your own registration logic. An example using the builder pattern:

public class UserMappingProfile<TUserDto, TUserDtoKey, TUser, TUserKey> : Profile
    where TUserDto : UserDto<TUserDtoKey>
    where TUser : User<TUserKey>
{
    public UserMappingProfile()
    {
        CreateMap<TUserDto, TUser>(MemberList.Destination)
            .ForMember(x => x.Id, opts => opts.MapFrom(x => x.UserId));

        CreateMap<TUser, TUserDto>(MemberList.Source)
            .ForMember(x => x.UserId, opts => opts.MapFrom(x => x.Id));
    }
}

public interface IMapperConfigurationBuilder
{
    IMapperConfigurationBuilder UseProfile<TUserDto, TUserDtoKey, TUser, TUserKey>()
        where TUserDto : UserDto<TUserDtoKey>
        where TUser : User<TUserKey>;
}

public static class MyServiceExtensions
{
    private class MapperConfigurationBuilder : IMapperConfigurationBuilder
    {
        public HashSet<Type> ProfileTypes { get; } = new HashSet<Type>();

        public IMapperConfigurationBuilder UseProfile<TUserDto, TUserDtoKey, TUser, TUserKey>()
            where TUserDto : UserDto<TUserDtoKey>
            where TUser : User<TUserKey>
        {
            ProfileTypes.Add(typeof(UserMappingProfile<TUserDto, TUserDtoKey, TUser, TUserKey>));
            return this;
        }
    }

    public static IMapperConfigurationBuilder AddMyMapper(this IServiceCollection services)
    {
        var builder = new MapperConfigurationBuilder();

        services.AddSingleton<IConfigurationProvider>(sp => new MapperConfiguration(cfg =>
        {
            foreach (var profileType in builder.ProfileTypes)
                cfg.AddProfile(profileType);
        }));

        services.AddScoped<IMapper>(sp => new Mapper(sp.GetRequiredService<IConfigurationProvider>(), sp.GetService));
        return builder;
    }
}

Then the mapping profile registration would look like this:

services.AddMyMapper()
    .UseProfile<MyUserDto, int, MyUser, int>();
like image 106
Adam Simon Avatar answered Oct 30 '22 03:10

Adam Simon