Automapper not mapped inside nested collection is not working properly - c#

All. For example we have such simple classes
public class SimpleModel
{
public int PropertyId { get; set; }
public ICollection<SimpleModelCollectionItem> SimpleCollection { get; set; }
}
public class SimpleModelCollectionItem
{
public int PropertyId { get; set; }
}
public class SimpleEntity
{
public int Id { get; set; }
public int PropertyId { get; set; }
public virtual ICollection<SimpleEntityCollectionItem> SimpleCollection { get; set; }
}
public class SimpleEntityCollectionItem
{
public int Id { get; set; }
public int PropertyId { get; set; }
}
and we have some configuration code
AutoMapper.Mapper.CreateMap<SimpleModel, SimpleEntity>()
.ForMember(dest => dest.Id, src => src.Ignore())
.ForMember(dest => dest.SimpleCollection, src => src.UseDestinationValue());
AutoMapper.Mapper.CreateMap<SimpleModelCollectionItem, SimpleEntityCollectionItem>()
.ForMember(dest => dest.Id, src => src.Ignore());
AutoMapper.Mapper.AssertConfigurationIsValid();
and test data initialization
var model = new SimpleModel
{
PropertyId = 2,
SimpleCollection =
new List<SimpleModelCollectionItem>
{
new SimpleModelCollectionItem
{
PropertyId = 7
}
}
};
var entity = new SimpleEntity
{
Id = 1,
PropertyId = 3,
SimpleCollection =
new List<SimpleEntityCollectionItem>
{
new SimpleEntityCollectionItem
{
Id = 5,
PropertyId = 4
}
}
};
AutoMapper.Mapper.Map(model, entity);
and I expect to see
Console.WriteLine(entity.Id); // 1
Console.WriteLine(entity.PropertyId); // 2
Console.WriteLine(entity.SimpleCollection.First().Id); // 5 but was 0
Console.WriteLine(entity.SimpleCollection.First().PropertyId); // 7
Is it possible to set Id for inner collection equals to 5 as it was initially?

So, when AutoMapper was mapping your collection, it actually removed the old item from the destination collection and then added a new item.
You can verify this by using this code:
var item_before = entity.SimpleCollection.First();
AutoMapper.Mapper.Map(model, entity);
var item_after = entity.SimpleCollection.First();
bool item_same_references = object.ReferenceEquals(item_before, item_after);
The value of item_same_references will be false.
This happens even if you are using src => src.UseDestinationValue(). Using this only means that the collection object it self should be reused, but not the items in that collection.
I am not sure if there is a way to tell AutoMapper to also use the same collection items.
Also, thinking about it, what happens if the destination collection contains more of fewer items than the source collection?
So, the zero you are getting is because when AutoMapper creates the new item to add to the collection, the default value of the Id property is default(int) which is zero.
I suggest the following:
I am assuming that the number of items in the source and destination collections are equal.
First, modify your mapping configuration to instruct AutoMapper to ignore the collection like this:
AutoMapper.Mapper.CreateMap<SimpleModel, SimpleEntity>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.SimpleCollection, opt => opt.Ignore());
Then, create a method like this that maps collection items in place (without removing and then adding new items) like this:
public void MapCollectionsInPlace<TSource, TDestination>(IEnumerable<TSource> source_collection,
IEnumerable<TDestination> destination_collection)
{
var source_enumerator = source_collection.GetEnumerator();
var destination_enumerator = destination_collection.GetEnumerator();
while (source_enumerator.MoveNext())
{
if(!destination_enumerator.MoveNext())
throw new Exception("Source collection has more items than destination collection");
AutoMapper.Mapper.Map(source_enumerator.Current, destination_enumerator.Current);
}
}
Now, you can use the following code:
AutoMapper.Mapper.Map(model, entity);
MapCollectionsInPlace(model.SimpleCollection, entity.SimpleCollection);

Related

Automapper. How to map custom type property of inner class (source) to string and string array?

I want to map source inner classes to string and in other case string array but in both cases they are mapped to null. I want MapNoteFromInnerSourceEntity1 to hold a value of InnerSourceEntity1 Id property and MapValueFromInnerSourceEntity2 to hold values of InnerSourceEntity2 value properties. So far automapper is quite difficult for me to understand.
Code:
internal class Program
{
public class InnerSourceEntity1
{
public string Id { get; set; }
public string Note { get; set; }
}
public class InnerSourceEntity2
{
public string Id { get; set; }
public string Value { get; set; }
}
public class SourceEntity
{
public InnerSourceEntity1 A { get; set; }
public IList<InnerSourceEntity2> B { get; set; }
}
public class DestinationEntity
{
public string MapNoteFromInnerSourceEntity1 { get; set; }
public string[] MapValueFromInnerSourceEntity2 { get; set; }
}
static void Main()
{
var source = new SourceEntity
{
A = new InnerSourceEntity1 { Note = "Note", Id = "Id"},
B = new List<InnerSourceEntity2> { new InnerSourceEntity2 { Id = "Id", Value = "Value" }, new InnerSourceEntity2 { Id = "Id", Value = "Value" } }
};
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<SourceEntity, DestinationEntity>();
cfg.CreateMap<InnerSourceEntity1, string>().ConvertUsing(s => s.Note);
cfg.CreateMap<IList<InnerSourceEntity2>, string[]>();
});
var mapper = new Mapper(config);
DestinationEntity destination = mapper.Map<SourceEntity, DestinationEntity>(source);
Console.ReadLine();
}
}
Since the names of properties between source type SourceEntity and destination type DestinationEntity don't match, you'll have to explicitly indicate them, otherwise AutoMapper will not know how to fill the properties:
cfg.CreateMap<SourceEntity, DestinationEntity>()
.ForMember(
dst => dst.MapNoteFromInnerSourceEntity1,
opts => opts.MapFrom(src => src.A))
.ForMember(
dst => dst.MapValueFromInnerSourceEntity2,
opts => opts.MapFrom(src => src.B));
Also, don't map between concrete collection types:
cfg.CreateMap<IList<InnerSourceEntity2>, string[]>(); // <== Don't do that.
Instead, see what the docs say about mapping collections:
(...) it’s not necessary to explicitly configure list types, only their member types. ~ AutoMapper Docs
So, you only need to specify map like this:
cfg.CreateMap<InnerSourceEntity2, string>();
And since we are mapping to string we also need to instruct the AutoMapper from where it can get the string value. So, we'll use ConvertUsing() again:
cfg.CreateMap<InnerSourceEntity2, string>().ConvertUsing(s => s.Value);
Final configuration:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<SourceEntity, DestinationEntity>()
.ForMember(
dst => dst.MapNoteFromInnerSourceEntity1,
opts => opts.MapFrom(src => src.A))
.ForMember(
dst => dst.MapValueFromInnerSourceEntity2,
opts => opts.MapFrom(src => src.B));
cfg.CreateMap<InnerSourceEntity1, string>().ConvertUsing(s => s.Note);
cfg.CreateMap<InnerSourceEntity2, string>().ConvertUsing(s => s.Value);
});

C# Automapper how to create a list with nested mapping?

I am trying to map into the list, which supposes to have two other lists inside. Is it doable? I looked at the documentation but couldn't find what I needed unless I misunderstood something.
CompanyActivityReport.cs
public int OrganisationID { get; set; }
public string OrganisationName { get; set; }
public Nullable<int> OrganisationSubTypeID { get; set; }
public CompanyActivityReportTask ReportTask{get; set;}
public CompanyActivityReportNote ReportNotes{get; set;}
My mapping:
var config = new MapperConfiguration(c =>
{
c.CreateMap<OrganisationMain,CompanyActivityReport>();
c.CreateMap<TaskMain, CompanyActivityReportTask>();
c.CreateMap<NoteMain, CompanyActivityReportNote>();
});
var mapper = new Mapper(config);
List<CompanyActivityReport> TestList = mapper.Map<List<CompanyActivityReport>>(OrganisationMainsList).ToList();
Guess you problably need to map each item individually like:
List<CompanyActivityReport> TestList = OrganisationMainsList.Select(x => mapper.Map<CompanyActivityReport>(x))
Edit: Select return a Enumerable so if you wish to mantain the behaviour a .ToList() is required:
List<CompanyActivityReport> TestList = OrganisationMainsList.Select(x => mapper.Map<CompanyActivityReport>(x)).ToList()

Can AutoMapper mappings be composed?

The models I'm working with include an entry object which I'd like to map as if its child object were the entire object.
Here is a simplified version of the problem. I'd like an instance of OurWrappedSource to map directly to OurTarget.
class OurTarget
{
public Guid Id { get; set; }
public string Notes { get; set; }
public int Quantity { get; set; }
}
class OurSource
{
public Guid Id { get; set; }
public string Notes { get; set; }
public int Quantity { get; set; }
}
class OurWrappedSource
{
public OurSource Source { get; set; }
}
private static void TestUnwrapUsingConfig(MapperConfiguration config)
{
config.AssertConfigurationIsValid();
IMapper mapper = new Mapper(config);
var wrappedSource = new OurWrappedSource
{
Source = new OurSource
{
Id = new Guid("123e4567-e89b-12d3-a456-426655440000"),
Notes = "Why?",
Quantity = 27
}
};
var target = mapper.Map<OurTarget>(wrappedSource);
Assert.Equal(wrappedSource.Source.Id, target.Id);
Assert.Equal(wrappedSource.Source.Notes, target.Notes);
Assert.Equal(wrappedSource.Source.Quantity, target.Quantity);
}
The following configuration works, but is unwieldy for more than a couple of members:
// Works, but isn't *auto* enough
TestUnwrapUsingConfig(new MapperConfiguration(cfg =>
{
cfg.CreateMap<OurWrappedSource, OurTarget>()
.ForMember(src => src.Id, opts => opts.MapFrom(wrappedSource => wrappedSource.Source.Id))
.ForMember(src => src.Notes, opts => opts.MapFrom(wrappedSource => wrappedSource.Source.Notes))
.ForMember(src => src.Quantity, opts => opts.MapFrom(wrappedSource => wrappedSource.Source.Quantity));
}));
What I'd like to be able to do is define two intermediate mappings an then compose them:
Map OurWrappedSource directly to OurSource
Map OurSource directly to OurTarget
Map OurWrappedSource to OurTarget by composing mapping 1 with mapping 2
After some hammering, I have this configuration:
// Works, but #3 probably isn't ProjectTo-friendly
TestUnwrapUsingConfig(new MapperConfiguration(cfg =>
{
// 1
cfg.CreateMap<OurWrappedSource, OurSource>()
.ConvertUsing(wrappedSource => wrappedSource.Source);
// 2
cfg.CreateMap<OurSource, OurTarget>();
// 3
cfg.CreateMap<OurWrappedSource, OurTarget>()
.ConstructUsing((wrappedSource, ctx) =>
ctx.Mapper.Map<OurTarget>(ctx.Mapper.Map<OurSource>(wrappedSource))
)
.ForAllOtherMembers(opts => opts.Ignore());
}));
This works exactly as specified, but mapping 3 seems perhaps a little more explicit and/or kludgey than it should. It involves code in a Func (rather than an expression), which makes me think it probably won't optimize well when used with ProjectTo(). Is there a way to rewrite mapping 3 to address these issues?

How to order Nested Collections in Linq and EF

i would like to make a treelistview for my Data.
Tree should look like this
Accounts
-> Providers
-> Accounts
public sealed class AccountRoot
{
public AccountRoot()
{
Providers = new Collection<Hoster>();
}
public long AccountRootId { get; set; }
public ICollection<Hoster> Providers { get; set; }
}
public sealed class Hoster
{
public Hoster()
{
Accounts = new Collection<Account>();
}
[Key]
public long HosterId { get; set; }
public long AccountRootId { get; set; }
public string Name { get; set; }
public ICollection<Account> Accounts { get; set; }
}
public sealed class Account
{
[Key]
public long AccountId { get; set; }
public long HosterId { get; set; }
public Hoster Hoster { get; set; }
public string Name { get; set; }
}
I would like to order my query.
should be sth like
Accounts
Providers A-Z
Accounts A-Z
what i got until now is..
var query = _entity.AccountRoot.Local
.Select(x => new AccountRoot()
{
AccountRootId = x.AccountRootId,
Providers = x.Providers.OrderBy(y => y.Name).ToList()
}).ToList();
What is missing is the orderby for the next nested collection.
Thank you for your help ! :-)
It can be a bit different approaches depending on if you already have a result set, and want to just sort it in code, or if you want to construct IQueryable<> for EF which will be successfully compiled to SQL and executed with actual sorting in database.
First, assume you already have the collection in code. In this case, you have object AccountRoot, which contains collection of Providers, each of which has collection of Accounts. Obviously, you cannot return the same objects, as you need to reorder collection properties, so all you need is to just construct new ones. I would just sort the collections, but you could construct completely new entities, if you need:
var query = ...
.Select(x => new AccountRoot
{
// add copy properties here
// ....
Providers = x.Providers
.Select(y =>
{
// Here we can construct completely new entity,
// with copying all properties one by one,
// or just reorder existing collection as I do here
var result = y;
result.Accounts = y.Accounts.OrderBy(z => z.Name).ToArray();
return result;
})
.OrderBy(y => y.Name)
.ToArray()
})
.ToArray();
Second case, if you need to get it directly from SQL, is a bit different, as you cannot use all that var result = ...; ... return result stuff in lambda - it won't compile to SQL. But idea is the same - you need to construct projection from data sets. It should be something like this:
var query = ...
.Select(x => new AccountRoot
{
AccountRootId = x.AccountRootId,
// Other properties to copy
// ...
Providers = x.Providers
.Select(y => new Hoster
{
HosterId = y.HosterId,
// Other properties to copy
// ...
Accounts = y.Accounts.OrderBy(z => z.Name).ToArray(),
})
.OrderBy(y => y.Name)
.ToArray()
})
.ToArray();

Mapping "LinkedList" with AutoMapper

I have linked list kind of situation. My DTO looks like this -
public class DTOItem
{
public string ID
{
get;
set;
}
public int? UniqueId
{
get;
set;
}
public string Payload
{
get;
set;
}
//How do I map this guy? It is list of same type.
public List<DTOItem> RelatedItems
{
get;
set;
}
}
How do I map this guy using AutoMapper? I am able to map other members of the class. Data is mapped from another class' collection object that has a different set of member not identical to this class.
public List<DTOItem> RelatedItems
{
get;
set;
}
Thanks in advance.
UPDATE: Here is the code -
Raphael, here is the code:
The Source Objects:
public class ResultsSet
{
public int? ResultId
{
get;
set;
}
public int UID
{
get;
set;
}
//Returns large XML string
public string ResultBlob
{
get;
set;
}
public RelatedItems[] RelatedSet
{
get;
set;
}
}
public class RelatedItems
{
public int Item_ID
{
get;
set;
}
public int Relationship_ID
{
get;
set;
}
public string Description
{
get;
set;
}
}
To map here is the code:
Mapper.CreateMap<ResultSet, DTOItem>()
.ForMember(dest => dest.ID, opt => opt.MapFrom(src => src.ResultID.GetValueOrDefault(0)))
.ForMember(dest => dest.UniqueId, opt => opt.MapFrom(src => src.UID))
.ForMember(dest => dest.Payload, opt => opt.MapFrom(src => src.ResultBlob));
/*
How do I map RelatedSet to RelatedItems here?
*/
Mapper.Map(result, returnResult);
Thanks again.
No need to use AutoMapper for this.
For non-cyclic, relatively flat data, this should do:
static Func<RelatedItems, DTOItem> MapRelated(IEnumerable<ResultsSet> all) {
var map = MapResultSet(all);
return relatedItem => map(all.First(x => x.UID == relatedItem.Item_ID));
}
static Func<ResultsSet, DTOItem> MapResultSet(IEnumerable<ResultsSet> all) {
return s =>
new DTOItem {
ID = s.ResultId.GetOrElse(0).ToString(),
UniqueId = s.UID,
Payload = s.ResultBlob,
RelatedItems = (s.RelatedSet ?? new RelatedItems[0]).Select(MapRelated(all)).ToList()
};
}
Sample usage:
var data = new[] {
new ResultsSet {
UID = 1,
RelatedSet = new[] {
new RelatedItems { Item_ID = 2 },
new RelatedItems { Item_ID = 3 },
},
},
new ResultsSet {
UID = 2,
},
new ResultsSet {
UID = 3,
},
};
var items = data.Select(MapResultSet(data)).ToList();
Debug.Assert(items.Count == 3);
Debug.Assert(items[0].UniqueId == 1);
Debug.Assert(items[1].UniqueId == 2);
Debug.Assert(items[2].UniqueId == 3);
Debug.Assert(items[0].RelatedItems.Count == 2);
Debug.Assert(items[0].RelatedItems[0].UniqueId == items[1].UniqueId);
Debug.Assert(items[0].RelatedItems[1].UniqueId == items[2].UniqueId);
I assumed Item_ID is the 'key' to UID, otherwise simply adjust MapRelated.
Generally speaking, I think AutoMapper is only useful if you have to map untyped data into typed data, and even in that case I'd think really hard before using it. Otherwise, some LINQ code is simpler and more type safe.

Categories