1

I created a .Net Core 5 API having 2 types of models:

  • Entities (used by Entity Framework Core)
  • DTOs (Data Transfer Objects for requests and responses, replacing "{Property}Id" properties from Entity with "{Property}Code" in DTO)

I have a service responsible of mapping Entities types to Dtos types added as singleton in ConfigureServices:

services.AddSingleton(typeof(IEntityDtoMappingProvider), typeof(EntityDtoMappingProvider));

The service EntityDtoMappingProvider has a method which returns the mapping between Entities and Dtos for an assembly through reflection described by this interface:

public interface IEntityDtoMappingProvider
{
    Dictionary<Type,Type> GetEntityDtoMapping(Assembly assembly);
}

I have an AutoMapper Profile requiring Entities and DTOs mapped, returned by the first service IEntityDtoMappingProvider:

public class EntitiesToDtosProfile : Profile
{
    public EntitiesToDtosProfile(Dictionary<Type,Type> mapping)
    {
        if (mapping == null || mapping.Count == 0)
        {
            throw new ArgumentException( $"Empty mapping argument passed to {nameof(EntitiesToDtosProfile)} profile", nameof(mapping));
        }

        foreach(var item in mapping)
        {
            // Create AutoMapper mapping both ways based on those types
            CreateMap(item.Key, item.Value); // Entity-DTO
            CreateMap(item.Value, item.Key); // DTO-Entity
        }
    }
}

I need to create the AutoMapper profile in Startup.cs in ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddSingleton(typeof(IEntityDtoMappingProvider), typeof(EntityDtoMappingProvider));

    // Note: Using services.BuildServiceProvider() is a bad practice because an additional copy of singleton services being created
    using (var serviceProvider = services.BuildServiceProvider())
    {
        var mappingService = serviceProvider.GetRequiredService<IEntityDtoMappingProvider>();
        var mappings = mappingService.GetEntityDtoMapping(typeof(Workflow).Assembly);

        // Add AutoMapper IMapper to services
        var mappingConfig = new MapperConfiguration(mc =>
        {
            mc.AddProfile(new EntitiesToDtosProfile(mappings));
        });
        var mapper = mappingConfig.CreateMapper();
        services.AddSingleton(mapper);
        
        // Here I should call other IServiceCollection extensions like:

        // Database-related services: GenericRepository<TEntity, TDbContext> : IGenericRepository<TEntity>
        services.AddDatabaseGenericRepositories<ApplicationDbContext>(mappings, Log.Logger);

        // Mapping-related services: MappingHelper<TEntity, TDto> : IMappingHelper<TEntity, TDto>
        services.AddMappingHelpers(mappings, Log.Logger);

        // ...
    }
    // ...
}

As I was saying in the code, using services.BuildServiceProvider() is a bad practice because an additional copy of singleton services being created, creates a second container, which can create torn singletons and cause references to object graphs across multiple containers. Microsoft .Net Core 5 documentation backing those statements.

Please give an answer about how I should create the Entity-DTO mapping of type Dictionary<Type,Type> in CreateServices using IEntityDtoMappingProvider in order to build the AutoMapper profile EntitiesToDtosProfile and create other services through reflection without calling services.BuildServiceProvider taking into considerations the following:

  • I have many services created through reflection using extension methods for IServiceCollection requiring Entity-DTO mapping in ConfigureServices
  • I cannot use IOptions having a property of type Dictionary<Type,Type> because IOptions shouldn't be used in ConfigureServices: "An inconsistent options state may exist due to the ordering of service registrations." Source: IOptions Microsoft Documentation.
  • I looked through a lot of questions (some might be a little unrelated), but all solved their issues using services.BuildServiceProvider() or IOptions which is not ok.
  • If you are only registering `IEntityDtoMappingProvider` so that you use it to build your mapping component then maybe you shouldn't register it. – satnhak Jan 11 '22 at 13:20
  • Good point, I only use it to create an AutoMapper profile (EntitiesToDtosProfile) and a create lot of services through reflection looping through mappings. I might lose the interface and just create the instance, without registering it at all. You can create it as an answer and I'll accept it in case nothing better shows up. – Alexandru Chirita Jan 11 '22 at 14:08
  • 1
    done. sometimes the simplest solution is the best. – satnhak Jan 12 '22 at 12:02

2 Answers2

1

If you are only registering IEntityDtoMappingProvider so that you use it to build your mapping component then maybe you shouldn't register it. This sort of one time configuration is often best done outside the scope of the container itself. As you suggested you can probably just remove the interface entirely and use the concrete class directly.

Same goes for things like logger configuration.

satnhak
  • 9,407
  • 5
  • 63
  • 81
0

You can register a service factory which accepts the service provider instance and uses that to resolve other services. For example:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddSingleton(typeof(IEntityDtoMappingProvider), typeof(EntityDtoMappingProvider));
    
    services.AddSingleton(sp =>
    {
        var mappingService = sp.GetRequiredService<IEntityDtoMappingProvider>();
        var mappings = mappingService.GetEntityDtoMapping(typeof(Workflow).Assembly);

        var mappingConfig = new MapperConfiguration(mc =>
        {
            mc.AddProfile(new EntitiesToDtosProfile(mappings));
        });
        
        return mappingConfig.CreateMapper();
    });
    // ...
}

You would need to modify your AddDatabaseGenericRepositories and AddMappingHelpers methods to do something similar.

Richard Deeming
  • 29,830
  • 10
  • 79
  • 151
  • Your answer only works for a single service registration (in your case a singleton). The methods AddDatabaseGenericRepositories and AddMappingHelpers add multiple services looping through the mappings and creating generic types like: "var interfaceType = typeof(IMappingHelper<,>).MakeGenericType(mapping.Key, mapping.Value)", "var implementationType = typeof(MappingHelper<,>).MakeGenericType(mapping.Key, mapping.Value))" and afterwards registering all of them one by one like "services.AddSingleton(interfaceType, implementationType);" (or AddTransient, and so on). – Alexandru Chirita Jan 11 '22 at 13:43
  • In that case, I suspect you'll need to follow satnhak's advice, and create the `EntityDtoMappingProvider` instance manually rather than registering it in the DI container. – Richard Deeming Jan 11 '22 at 13:56