AutoMapper ResolutionContext does not contain a definition for engine anymore - automapper

After migration from an old version of AutoMapper (before 5) to version 9 there is one spot which causes headache. Old implementation:
.ForMember(a => a.Definition, o =>
{
o.Condition(s => s.TypeId == DocumentationType.Medication);
o.ResolveUsing((d, ctx) => ctx.Engine.Map<MedicationDefinitionContent>(d.Content.MedicationContentData));
})
which uses this extension method:
public static class MappingExtensions
{
public static void ResolveUsing<TType>(this IMemberConfigurationExpression<TType> expression, Func<TType, ResolutionContext, object> map)
{
expression.ResolveUsing(result => map((TType)result.Value, result.Context));
}
}
I fixed the first error that that IMemberConfigurationExpression needs 3 arguments, but then I learned that ResolutionContext does not contain a definition for engine anymore. I looked in the upgrade guide of version 5 and found that the ResolutionContext has been changed, but I do not understand how to fix this. The code seems to be pretty tricky. Can someone help, please?

#Lucian Bargaoanu
Ok, but the member "Definition" is the member wie map with MapFrom(s => s.Content.MedicationContentData). So different to the exception there is already a mapping. The member "Definition" is of type SerialisationHelper a helper class for Json stuff. It also has a mapping.
CreateMap<MedicationDefinitionContent, SerialisationHelper>()
.IgnoreAllUnmapped()
.AfterMap((s, t) => t.Write = s);
And MedicationDefinitionContent has a separate mapping.
CreateMap<MedicationContentData, MedicationDefinitionContent>()
MedicationDefinitionContent is annotated with [JsonObject(MemberSerialization.OptIn)]
so, a direct mapping from MedicationDefinitionContent to "Definition" does not work.
How you see I try to understand it, but maybe it needs more time.

Related

AutoMapper.Map overload with IMappingOperations missing from ResolutionContext.Mapper

I have a TypeConverter in which I'm using context.Mapper.Map() to map two subproperties.
The properties are the same Type and use the same (another) TypeConverter. However in one the properties I need to pass some IMappingOperationsOptions.
It looks like this (simplified):
public class MyTypeConverter : ITypeConverter<A, B>
{
public B Convert(A, B, ResolutionContext context)
{
var subProp1 = context.Mapper.Map<C>(B.SomeProp);
var subProp2 = context.Mapper.Map<C>(B.SomeOtherProp, ops => ops.Items["someOption"] = "someValue");
return new B
{
SubProp1 = subProp1,
SubProp2 = subProp2
};
}
}
This was working fine in AutoMapper 8.0.0 but I'm upgrading to AutoMapper 10.1.1 (last version with .NET framework support).
In this newer version of AutoMapper the overload method to pass IMappingOperationsOptions does not exist anymore.
I could (theoretically) solve this by injecting IMapper in the constructor of the TypeResolver and use that instead of the ResolutionContext's Mapper but that doesn't feel right.
At the moment I solved the issue by temporarily updating the ResolutionContext options, but that also doesn't really feel right.
var subProp1 = context.Mapper.Map<C>(B.SomeProp);
context.Options.Items["someOption"] = "someValue";
var subProp2 = context.Mapper.Map<C>(B.SomeOtherProp);
context.Options.Remove("someOption");
Casting ((IMapper)context.Mapper).Map() crashes so that's not an option either. Is there a more elegant way to achieve this?

Map or Ignore Indexer Property in Automapper 11

I'm having some issues mapping two classes using the AutoMapper in version 11. The destination class has an indexer-property, which causes the issue.
Since Automapper 11, the indexer property is no longer automatically ignored.
For testing purposes I used three classes:
public class Source {}
public class Target {
public float this[int key]
{
get
{
return 0;
}
set
{
}
}
}
public class MapperProfile: Profile
{
public MapperProfile()
{
CreateMap<Source, Target>();
}
}
During startup I'm calling mapper.ConfigurationProvider.AssertConfigurationIsValid() to validate the configuration. This fails with an unmapped Item property.
While it is possible to ignore all properties starting with Item using
this.AddGlobalIgnore("Item")
inside the Profile, I'd rather not use such a general way to ignore it, especially since the first parameter is labeled propertyNameStartingWith - this would suggest to me, that other properties such as ItemWithSuffix might be ignored as well.
Another strategy I tried to employ is to use an explicit ignore on a property. Using the expression notation fails, due to compiler errors:
CreateMap<Source, Target>()
.ForMember(dest => dest[], opt => opt.Ignore())
.ReverseMap();
Adding an arbitrary index to the expression fails with another error, so that does not seem to be a viable solution as well:
CreateMap<Source, Target>()
.ForMember(dest => dest[0], opt => opt.Ignore())
.ReverseMap();
In this case the error notes, that we may not map to child property.
When using the member name syntax, there are some different errors.
CreateMap<Source, Target>()
.ForMember("Item", opt => opt.Ignore())
.ReverseMap();
In this case it fails with the following message:
Incorrect number of arguments supplied for call to method 'Double get_Item(Int32)' (Parameter 'property')
Using [] or Item[] fails with a missing property notification.
The last strategy I employed was using the ForAllMembers call. This succeeds, however, I'm wondering if there is a better solution to handle this logic which allows using a specific mapping logic for a single member.
CreateMap<Source, Target>()
.ForAllMembers(x =>
{
if (x.DestinationMember.Name == "Item")
{
x.Ignore();
}
});

Entity Framework Core 5 tries to insert value for computed column

Using Entity Framework 5 and EF Core Power Tools (whith the setting to generate ef 5 code enabled) we get something like the following code generated for each computed column:
...HasComputedColumnSql("([LastName]+isnull(' '+[FirstName],''))", true);
which is perfectly right.
Unfortunately, trying to create a new entity that has computed columns and trying to save we get the following exception from ef core 5:
The column '...' cannot be modified because it is either a computed column or is the result of a UNION operator
When manualy appending the ValueGeneratedOnAddOrUpdate after the HasComputedColumnSql like so:
...HasComputedColumnSql("([LastName]+isnull(' '+[FirstName],''))", true).ValueGeneratedOnAddOrUpdate();
everything works fine.
According to the docs this should be automatically be called by HasComputedColumnSql.
What can be done to overcome this issue ?
We are using Microsoft.EntityFrameworkCore v5.0.5 package and also we are using an Azure Managed Sql Server Instance
The OP already pointed out the solution: in EF core 5, when scaffolding is used, you have to manually add ValueGeneratedOnAddOrUpdate(). This problem has been fixed in EF core 6.
The patch has been mentioned in earlier answers.
This answer elaborates on how to remove manual code from generated code.
Let's say the scaffolded file GenDbContext.cs contains:
modelBuilder.Entity<MyEntity>(entity =>
{
entity.Property(e => e.ComputedName).HasMaxLength(100).IsUnicode(false)
.HasComputedColumnSql("([LastName]+isnull(' '+[FirstName],''))", true);
});
There are two options: partial class or inheritance.
A) Partial class in GenDbContext.Extension.cs - requires EF core 3.1+
public partial class GenDbContext : DbContext
{
partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MyEntity>(entity =>
entity.Property(e => e.ComputedName).ValueGeneratedOnAddOrUpdate()
);
}
}
B) Subclass in InheritedDbContext.cs.
public class InheritedDbContext : GenDbContext
{
public InheritedDbContext(DbContextOptions<GenDbContext> options) : base(options) {}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<MyEntity>(entity =>
entity.Property(e => e.ComputedName).ValueGeneratedOnAddOrUpdate()
);
}
}
Then in Startup.cs:
// The first line is still necessary as it declares DbContextOptions<GenDbContext>.
services.AddDbContext<GenDbContext>(...options...);
services.AddDbContext<InheritedDbContext>(...same options...);
There is no need to repeat the rest of the column settings such as HasMaxLength, IsUnicode and HasComputedColumnSql.

Using string.Split() in AutoMapper issue

I have an ASP .Net core application. I am simply trying to have my AutoMapper configure to convert a string comma delimited into a list of strings as per this configuration:
configuration.CreateMap<Job, JobDto>()
.ForMember(dto => dto.Keywords, options => options.MapFrom(entity => entity.Keywords.Split(',').ToList()))
For some reason it does not get compiled and give me the following error:
An expression tree may not contain a call or invocation that uses
optional argument
I can't see why I am getting this error. I am pretty sure that I have done that in my other projects before without any such error.
As error says, Split function has an optional parameter. The full signature of it is as this (options is optional)
public string[] Split(string separator, StringSplitOptions options = StringSplitOptions.None)
As you are trying to use a function with default value inside an expression tree, it gives you the error.
To Fix it, easy, just pass on optional parameters by yourself. ( StringSplitOptions.None )
So, simply change it to this:
entity.Keywords.Split(',' , StringSplitOptions.None).ToList()
This is completely true.
Error is raised because expression tree being created is about to contain some more complex logic, like .Split(',').ToList(), which is not an accessible property or method, only top-level reflected object properties and methods are supported (like in class MemberInfo).
Property chaining, deep-calls (.obj1property.obj2property), extension methods are not supported by the expression trees, like in this .ToList() call.
My solution was like this:
// Execute a custom function to the source and/or destination types after member mapping
configuration.CreateMap<Job, JobDto>()
.AfterMap((dto,jobDto)=>jobDto.Keywords = dto.Keywords.Split(',').ToList());
I had the same problem. I do not know if it is an issue or not. Anyway, I found a workaround.
CreateMap<Category, GetCategoryRest>()
.ForMember(dest => dest.Words,
opt => opt.MapFrom(src => ToWordsList(src.Words)));
private static List<string> ToWordsList(string words)
{
return string.IsNullOrWhiteSpace(words) ? new List<string>() : words.Split(",").ToList();
}
It is guaranteed that AutoMapper has always a List. Still, I'm confused. In my Startup.cs I define that AutoMapper allows null values for list.
Mapper.Initialize(cfg => {
cfg.AllowNullCollections = true;
}
Category.Words is a string.
GetCategoryRest.Words is a List<string>
AutoMapper Version: 8.1.1,
AutoMapper.Microsoft.DependencyInjection: 6.1.1
Use .AfterMap
CreateMap<src, dto>()
.ForMember(src =>src.Categories,options=> options.Ignore())
.AfterMap((src, dto) => { dto.Categories.AddRange(src.Categories.Split(",").ToList()); })
.ReverseMap()
.ForMember(src => src.Categories, option => option.MapFrom(dto => string.Join(",", dto.Categories)));

How to map an int to a boolean

I'm using AutoMapper 5.2. I currently have a mapping statement that looks as follows:
CreateMap<JeffreysOnline.Data.Customer, JeffreysOnline.Entities.Customer>()
.ForMember(s => s.CustomerWant, t => t.Ignore());
Both the Customer table and Customer entity have a field named BadChecks. In the database it's an int. I recently changed the type to a bool in my entity. AutoMapper is now giving me the following error:
Unable to create a map expression from Customer.BadChecks (System.Int16) to Customer.BadChecks (System.Boolean) Mapping types: Customer -> Customer JeffreysOnline.Data.Customer -> JeffreysOnline.Entities.Customer Type Map configuration: Customer -> Customer JeffreysOnline.Data.Customer -> JeffreysOnline.Entities.Customer Property: BadChecks
It seems AutoMapper doesn't know how to map from an int to a boolean. Is it possible for me to help AutoMapper with this?
It may be helpful to know that in my DAL, I'm using ProjectTo() to pass an IQueryable to another method that is attempting to access the data, and therefore the mapping is occurring (an error being generated). My DAL code looks like this:
return entityList.OrderBy(row => row.LastName).ProjectTo<Entities.Customer>();
Automapper 6.0.2 - works without any ForMember... null, 0 = false, values >= 1 are mapped to true.
In Automapper 6.0.2 - other way:
class nnnProfile : Profile
{
CreateMap<src, dst>()
.ForMember(d => d.Decision, opt => opt.ResolveUsing<CustomBoolResolver>());
}
Resolver:
public class CustomBoolResolver : IValueResolver<src, dst, bool>
{
public bool Resolve(src source, dst destination, bool destMember,
ResolutionContext context)
{
return source.Decision == 1;
}
}
but this is per Destination, so not much flexible.
According to this page:
http://taswar.zeytinsoft.com/automapper-mapping-objects-part-5-of-7-customresolver/
In past you could write a custom resolver with just Source and target type.
I don't think I would know how to map from int to a boolean.
If you do figure out how that should happen, you'll need to create a mapping from int to boolean.:
CreateMap<int, bool>().ProjectUsing(src => src != 0);
Completely guessing there. But since you're using ProjectTo, you'll need to use ProjectUsing so that the expression makes it allllll the way down to your DAL.
Remember, when using ProjectUsing, AutoMapper isn't actually executing the mapping. It's creating a LINQ "Select" expression that it passes down to your query provider (EF maybe?). So you'll need to make sure that whatever you use in your projection expression, EF can support translating that eventually into SQL.

Resources