When mapping a source instance to a destination instance, how are null source values handled? It appears that with built in types, a null source value will overwrite a destination value (set it to null). However with a navigation property, a destination value will not be set to null by a null source value, e.g. OuterDest.Innter:
`
public class OuterSource
{
public int Value { get; set; }
public InnerSource? Inner { get; set; }
}
public class InnerSource
{
public int OtherValue { get; set; }
}
public class OuterDest
{
public int Value { get; set; }
public InnerDest? Inner { get; set; }
}
public class InnerDest
{
public int OtherValue { get; set; }
}
`
This test will fail
[TestMethod]
public void NestedTestNullSourceValue()
{
var mappingConfig = new MapperConfiguration(cfg =>
{
cfg.CreateMap<OuterSource, OuterDest>();
cfg.CreateMap<InnerSource, InnerDest>();
});
var mapper = mappingConfig.CreateMapper();
OuterSource source = new()
{
Value = 123
};
OuterDest dest = new()
{
Value = 888,
Inner = new()
{
OtherValue = 999
}
};
mapper.Map(source, dest);
Assert.AreEqual(123, dest.Value);
Assert.IsNull(dest.Inner);
}
Had the same issue (AutoMapper - Map(source, destination) overwrite destination child object with null value from source via configuration), updating to AutoMapper 12.0.1 pre-release solved my issue
Related
Given I have 2 classes, Foo and Bar:
public class Foo
{
private readonly List<Bar> _bars = new List<Bar>();
public int Id { get; private set; }
public string Name { get; private set; }
public IEnumerable<Bar> Bars => _bars;
public void AddBar(Bar bar)
{
_bars.Add(bar);
}
public static Foo Create(string name)
{
return new Foo { Name = name };
}
private Foo() { }
}
public class Bar
{
public int Id { get; private set; }
public string Description { get; private set; }
public static Bar Create(string description)
{
return new Bar { Description = description };
}
}
With 2 corresponding DTOs,
public class BarDto
{
public int Id { get; set; }
public string Description { get; set; }
}
public class FooDto
{
public int Id { get; set; }
public string Name { get; set; }
public List<BarDto> Bars { get; set; }
public FooDto()
{
Bars = new List<BarDto>();
}
}
And an AutoMapper/AutoMapper.Collection.EntityFrameworkCore setup of
var config = new MapperConfiguration(cfg =>
{
cfg.AddCollectionMappers();
cfg.UseEntityFrameworkCoreModel<DemoContext>();
cfg.CreateMap<BarDto, Bar>().EqualityComparison((src, dest) => src.Id == dest.Id);
cfg.CreateMap<FooDto, Foo>().ForMember(dest => dest.Bars, opt =>
{
opt.MapFrom(s => s.Bars);
opt.UseDestinationValue();
}).EqualityComparison((src, dest) => src.Id == dest.Id);
});
I have a use case whereby the incoming FooDto may contain inserted, appended, updated and deleted items in the Bars collection which I am attempting to handle by:
Looking up the existing entity from the database
Mapping changes from the DTO to the entity
Saving the changes to the database
However the following code produces an InvalidOperationException exception stating that "The instance of entity type 'Bar' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached"
var fooToUpdate = db.Foos.Include(_ => _.Bars).FirstOrDefault(_ => _.Id == fooDto.Id);
mapper.Map(fooDto, fooToUpdate);
db.SaveChanges();
My understanding was that becuase I am setting EqualityComparison for the BarDto -> Bar mapping it should update the tracked entity and the save operation should succeed becuase it was referencing the same object?
I am not sure if I'm going about this the wrong way or simply missing somthing in the configuration.
Update
It seems the problem I am facing may be related to this issue on github.
There are two classes in my Model:
public class Operation
{
public string Name { get; set; }
public Calculation Details { get; set; }
}
public class Calculation
{
public long Value { get; set; }
public List<decimal> Points { get; set; }
}
Mapping into this DTO:
public class OperationDto
{
public string Name { get; set; }
public CalculationDto Details { get; set; }
}
public class CalculationDto
{
public long Value { get; set; }
}
public class CalculationDetailedDto: CalculationDto
{
public List<decimal> Points { get; set; }
}
And sometimes the Client can request detailed information about the calculation. For example, depending of the command-line options:
class Program
{
static void Main(string[] args)
{
Mapper.CreateMap<Operation, OperationDto>();
Mapper.CreateMap<Calculation, CalculationDto>();
Mapper.CreateMap<Calculation, CalculationDetailedDto>();
var operation = new Operation
{
Name = "Very complicated opertion.",
Details =
new Calculation
{
Value = 1002,
Points = new List<decimal> {1.2m, 2.4m, 3.7m}
}
};
var operationDto = Mapper.Map<OperationDto>(operation);
Debug.WriteLine("Operation name: '{0}' value: '{1}'", operationDto.Name, operationDto.Details.Value);
if (args.Length > 0)
{
Debug.WriteLine("Details:");
foreach (var point in ((CalculationDetailedDto) operationDto.Details).Points)
{
Debug.WriteLine("{0}", point);
}
}
}
How do i tell Automapper at runtime map the calculation into CalculationDetailedDto?
You need to create another OperationDto that uses the detailed calculationDto, and create a map from Operation to OperationDetailedDto:
public class OperationDetailedDto
{
public string Name { get; set; }
public CalculationDetailedDto Details { get; set; }
}
Mapper.CreateMap<Operation, OperationDetailedDto>();
if (detailed) operationDto = Mapper.Map<Operation, OperationDto>(operation);
else operationDto = Mapper.Map<Operation, OperationDetailedDto(operation);
For those who do not fit solution proposed #fiskeboss can use the following.
You can re-map Calculation to CalculationDetailedDto after checking the command line arguments.
if (args.Length > 0)
{
operationDto.Details = Mapper.Map<CalculationDetailedDto>(operation.Details);
Debug.WriteLine("Details:");
foreach (var point in ((CalculationDetailedDto) operationDto.Details).Points)
{
Debug.WriteLine("{0}", point);
}
}
i want register custo generic type like default generic type that register in autoMaper (like List, Array) in AutoMappper.
i have one generic type in project this Code :
class PagedResult<T>
{
public List<T> PageOfResults { get; set; }
public int TotalItems { get; set; }
}
and Dto Class is:
class StudentDto
{
public int ID { get; set; }
public string Name { get; set; }
}
and VM Model is :
class StudentVM
{
public int ID { get; set; }
public string Name { get; set; }
}
and service class:
class MyServie
{
public PagedResult<StudentDto> Swap()
{
var test2 = new PagedResult<StudentDto>();
test2.PageOfResults = new List<StudentDto>();
test2.PageOfResults.Add(new StudentDto() { ID = 10, Name = "Ten" });
test2.TotalItems = 10;
return test2;
}
}
i want use from AutoMapper Object Manager for register PagedResult<> in automapper but i can not do this
var allMappers = MapperRegistry.AllMappers();
MapperRegistry.AllMappers = () => new IObjectMapper[]{
new IdentifiableMapper(),
}.Concat(allMappers);
var service = new MyServie();
PagedResult<StudentDto> pageableStudentDto = service.Swap();
Mapper.CreateMap<StudentDto, StudentVM>();;
PagedResult<StudentVM> vm = Mapper.Map<PagedResult<StudentDto>, PagedResult<StudentVM>>(pageableStudentDto);
and implement of
public class PageOfMapper : IObjectMapper
{
public bool IsMatch(ResolutionContext context)
{
return typeof(PagedResult<>).IsAssignableFrom(context.SourceType.GetGenericTypeDefinition()) &&
typeof(PagedResult<>).IsAssignableFrom(context.DestinationType.GetGenericTypeDefinition());
//return true;
}
public object Map(ResolutionContext context, IMappingEngineRunner mapper)
{
// please help me in this code for map ******************
return null;
}
}
Hello. I have a list that looks like this one:
public class PagedList<T> : List<T>
{
public PagedList(IEnumerable<T> collection) : base(collection)
{ }
public int TotalItems { get; set; }
public int CurrentPage { get; set; }
public int PageSize { get; set; }
//some other properties
}
and used in repository for paging
public PagedList<TEntity> GetPaged(int page)
{
var pagedEntities = some_query;
return pagedEntities.AsPagedList(totalResults, page, pageSize);
}
The same PagedList is also used in asp mvc view models for paging.
Is it possible to map this collections using Automapper with all the properties TotalItems/CurrentPage/... ?
PagedList<DbItem> dbItems = _repository.GetPages(page);
var viewItems = new PagedList<SomeItemView>();
Mapper.Map(dbItems , viewItems);
Tahnk You !
This worked for me. Are you looking for something more generic?
public class DbItem
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ViewItem
{
public int Id { get; set; }
public string Name { get; set; }
}
public class PagedList<T>: List<T>
{
public int TotalItems { get; set; }
public int CurrentPage { get; set; }
public int PageSize { get; set; }
}
class Program
{
static void Main(string[] args)
{
MapItems();
}
public static void MapItems()
{
Mapper.CreateMap<DbItem, ViewItem>();
Mapper.CreateMap<PagedList<DbItem>, PagedList<ViewItem>>()
.AfterMap((s, d) => Mapper.Map<List<DbItem>, List<ViewItem>>(s, d));
var dbList = new PagedList<DbItem>
{
new DbItem {Id = 1, Name = "a"},
new DbItem {Id = 2, Name = "b"}
};
dbList.TotalItems = 2;
dbList.CurrentPage = 1;
dbList.PageSize = 10;
var viewList = Mapper.Map<PagedList<DbItem>, PagedList<ViewItem>>(dbList);
Console.WriteLine(viewList.TotalItems);
Console.WriteLine(viewList.CurrentPage);
Console.WriteLine(viewList.PageSize);
Console.WriteLine(viewList[0].Id + " " + viewList[0].Name);
Console.WriteLine(viewList[1].Id + " " + viewList[1].Name);
Console.ReadLine();
}
}
What you need is a custom type converter
public class PagedListConverter<TIn, TOut> : ITypeConverter<IPagedList<TIn>, IPagedList<TOut>>
{
public IPagedList<TOut> Convert(AutoMapper.ResolutionContext context)
{
var source = (IPagedList<TIn>)context.SourceValue;
var mapped = Mapper.Map<IList<TOut>>(source);
return new StaticPagedList<TOut>(mapped, source.GetMetaData());
}
}
Usage:
Mapper.CreateMap<IPagedList<Company>, IPagedList<CompanyViewModel>>().ConvertUsing<PagedListConverter<Company, CompanyViewModel>>();
For those who have faced the similar problem recently, and as an update to NoPyGod's answer, you can achieve the general mapping using ITypeConverter. According to the official documentation:
AutoMapper also supports open generic type converters with any number of generic arguments:
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap(typeof(Source<>), typeof(Destination<>)).ConvertUsing(typeof(Converter<,>)));
The closed type from Source will be the first generic argument, and the closed type of Destination will be the second argument to close Converter<,>.
So the custom type converter would be:
private class Converter<TSource, TDestination>
: ITypeConverter<PagedList<TSource>, PagedList<TDestination>>
{
public PagedList<TDestination> Convert(
PagedList<TSource> source,
PagedList<TDestination> destination,
ResolutionContext context) =>
new PagedList<TDestination>(
context.Mapper.Map<List<TSource>, List<TDestination>>(source));
/* Additional settings comes here. */
}
And then register it:
this.CreateMap(typeof(PagedList<>), typeof(PagedList<>)).ConvertUsing(typeof(Converter<,>));
Here is what I have where I hope someone can help us out:
class Source
{
string name { get; set; }
Inner { get; set; }
}
class Inner
{
Col A { get; set; }
Col B { get; set; }
}
class Col : IList<ClassX>, IEnunmerable<ClassX>
I need to map class Source to a destination type which has:
class Dest
{
string name { get; set; }
IList<ClassY> A { get; set;}
IList<ClassY> B { get; set;}
}
Now, ClassX and class ClassY share the same properties. ClassY class has a subset of the ClassX primitive properties with the exact same names and types.
Tried all kinds of mappings. Just the ClassX to ClassY map, with the collections, without and with any mapping get no mapping found between or missing configuration between Source and Dest
AutoMapper.Mapper.Map<Source, Dest>(src);
Can someone help me out with the mapping? Thanks in advance.
This question is a few months old, but if you're still looking for an answer, this is what I tried that worked:
class Source
{
public string Name { get; set; }
public Inner Inner { get; set; }
}
class Inner
{
public Col A { get; set; }
public Col B { get; set; }
}
class Col : List<ClassX> { }
class ClassX
{
public int Index { get; set; }
public string Name { get; set; }
public ClassX() : this(0, "") { }
public ClassX(int index, string name)
{
this.Index = index;
this.Name = name;
}
}
class ClassY
{
public int Index { get; set; }
public string Name { get; set; }
public ClassY() : this(0, "") { }
public ClassY(int index, string name)
{
this.Index = index;
this.Name = name;
}
}
class Dest
{
public string Name { get; set; }
public List<ClassY> A { get; set; }
public List<ClassY> B { get; set; }
}
[TestMethod]
public void ComplexTest()
{
Mapper.CreateMap<Source, Dest>()
.ForMember(dest => dest.A, config => config.MapFrom(src => src.Inner.A))
.ForMember(dest => dest.B, config => config.MapFrom(src => src.Inner.B));
Mapper.CreateMap<ClassX, ClassY>();
Source source = new Source
{
Name = "Source",
Inner = new Inner
{
A = new Col
{
new ClassX(1, "First"),
new ClassX(2, "Second"),
new ClassX(3, "Third"),
new ClassX(4, "Fourth"),
},
B = new Col
{
new ClassX(5, "Fifth"),
new ClassX(6, "Sixth"),
new ClassX(7, "Seventh"),
new ClassX(8, "Eighth"),
},
}
};
Dest destination = Mapper.Map<Source, Dest>(source);
Assert.AreEqual(source.Name, destination.Name);
Assert.AreEqual(source.Inner.A.Count, destination.A.Count);
Assert.AreEqual(source.Inner.B.Count, destination.B.Count);
Assert.AreEqual(source.Inner.A[0].Name, destination.A[0].Name);
Assert.AreEqual(source.Inner.B[0].Name, destination.B[0].Name);
}
I didn't go too in-depth with my Asserts, so there may be something I missed, but they appear to be mapped properly.