AutoMapper / AutoMapper.Extensions.Microsoft.DependencyInjection

MIT License
258 stars 79 forks source link

Adds support for dependencies to be injected into auto discovered Profiles #123

Closed MattMinke closed 4 years ago

MattMinke commented 4 years ago

Enhances The AddAutoMapper extension methods with the ability to resolve the profiles if they have dependencies.

lbargaoanu commented 4 years ago

Search the previous issues on the subject.

MattMinke commented 4 years ago

So I took a look at a couple of different issues (#113 , #29 , #18 , https://github.com/AutoMapper/AutoMapper/issues/2208) related to this and also did some more digging trying to find a different way to solve my use case and I am coming up with nothing. Do you have any guidance on a way to solve the below use case that does not require dependencies to be injected into the Profile. Maybe I am using IMappingAction incorrectly or possible there a different Auto-mapper feature that better fits my need.

public class DataAccessMappingProfile : Profile
{
    public DataAccessMappingProfile(IEnumerable<IMappingAction<ModelDto, Model>> mappingActions)
    {
        CreateMap<ModelDto, Model>()
            .ForMember(m => m.Status, x => x.ConvertUsing(new EnumMemberValueConverter<ModelStatus>()))
        // .. other member mappings

        // How do I solve the need to be able to pass in a list of mapping actions here?
        .AfterMap((source, destination, context) =>
        {
            foreach (var action in mappingActions)
            {
                action.Process(source, destination, context);
            }
        });
        // More CreateMap Calls
        // .....
    }
}

A little detail about how I am using the IMappingAction<,> interface. The ModelDto class has a collection of "attributes" that have been normalized to oblivion and back. There is not a one to one mapping between the attributes and the Model's "specifications". There are some cases where multiple attributes are collapsed to a single specification, and other cases where attributes are skipped/ignored, and others where a specification is not derived from an attribute at all. There is a decent amount of business logic that creates specifications. Take for example the below mapping actions. The first mapping action must check for specific attributes and conditions to determine if the Capacity specification should be added to the destination Model. The second mapping action requires data from three "attributes" to create a single specifications.

public class CapacitySpecificaitonMappingAction : IMappingAction<ModelDto, Model>
{
    public void Process(ModelDto source, Model destination, ResolutionContext context)
    {
        if (source.Classification == Classification.CC10B)
        {
            var attribute = source.Attributes.FirstOrDefault(attr => attr.AttributeKey.AttributeKeyId == 187);
            if (attribute != null && attribute.NumericValue.HasValue && attribute.NumericValue.Value > 0M)
            {
                return;
            }
        }

        if (!string.IsNullOrWhiteSpace(source.LoadCapacity))
        {
            destination.Description.Specifications.Add(new Specification()
            {
                Label = "Capacity",
                Type = SpecificationType.Capacity,
                Value = source.LoadCapacity
            });
        }
    }
}

public class ExternalDimensionsSpecificaitonMappingAction : IMappingAction<ModelDto, Model>
{
    const int _largeDimensionAttributeKeyId = 60;
    const int _mediumDimensionAttributeKeyId = 61;
    const int _smallDimensionAttributeKeyId = 62;

    public void Process(ModelDto source, Model destination, ResolutionContext context)
    {
        var value = BuildDimensionsSpecification(source.Attributes);
        if (value != null)
        {
            destination.Description.Specifications.Add(new Specification()
            {
                Label = "Exterior Dimensions",
                Type = SpecificationType.ExternalDimensions,
                Value = value.ToString()
            });
        }
    }
    private DimensionsSpecification BuildDimensionsSpecification(IEnumerable<AttributeValueDao> attributes)
    {
        IDictionary<int, IList<AttributeValueDao>> lookup = attributes.ToDictionaryList(o => o.AttributeKey.AttributeKeyId);
        var largeDimension = Get(lookup, _largeDimensionAttributeKeyId);
        var mediumDimension = Get(lookup, _mediumDimensionAttributeKeyId);
        var smallDimension = Get(lookup, _smallDimensionAttributeKeyId);

        if (largeDimension > 0 || mediumDimension > 0 || smallDimension > 0)
        {
            return new DimensionsSpecification(largeDimension, mediumDimension, smallDimension);
        }
        return null;
    }
    private decimal Get(IDictionary<int, IList<AttributeValueDao>> lookup, int id)
    {
        if (lookup.TryGetValue(id, out var data))
        {
            return data.First().NumericValue.GetValueOrDefault(0M);
        }
        return 0.0M;
    }
}
lbargaoanu commented 4 years ago

You can inject things in IMappingAction. And also pass parameters through the context. But it seems to me that you're writing business logic in the AM configuration. Your app should be about that business logic, you shouldn't hide it in AM. You should use AM for trivial, technical transformations, not for the core of your app.

MattMinke commented 4 years ago

Couple of follow up question to make sure I am understanding

  1. Are you considering the below segment of code to be business logic or are you referring to the logic in the concrete implementations of IMappingAction<,> ?

    .AfterMap((source, destination, context) => {
    foreach (var action in mappingActions)
    {
        action.Process(source, destination, context);
    }
    });
  2. I see IValueResolver, IMemberValueResolver, ITypeConverter, and IValueConverter as extension points in AutoMapper intended to support and encourage the encapsulation of complex mapping logic (not business logic) in AutoMapper. Am I misunderstanding the intended purpose of these extension points?

  3. I am surprised that you say AutoMapper should be used for trivial transformations. Its such a powerful tool I find it hard to think of using AutoMapper only for Property to Property mappings. I feel it's common for people to use IValueResolver, IMemberValueResolver, ITypeConverter, and IValueConverter as extension points to map complex values. In general am I the outlier in my desired use of AutoMapper for more than property to property mappings or is this a direction the AutoMapper team is trying to steer the community in because of documented issues?

A follow up though: I agree Automapper should not contain the business logic of my application and should only be used for mapping between models (in my case between my database model and domain model - I use the term domain model loosely and do not mean to confuse it with a domain model described in DDD). It just so happens that in my case my mapping logic (I would not consider any of this business logic. It is not solving a business problem, or providing business value) is having to deal with some idiosyncrasies caused by the way my data is stored in the database, and how I would like to work with it in my application.

jbogard commented 4 years ago

Still, don't do this. You're going to mess up the resolutions of mapping actions, those should be at map-time.

At MOST you should pass in an enumerable of types of mapping actions, not the mapping actions themselves. But really you should create your own IMappingAction that takes a dependency on an enumerable of your own mapping action interface that you define, and register, in the container.

lock[bot] commented 4 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.