Mapping "LinkedList" with AutoMapper - c#

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.

Related

Error when using Select() instead of Include() in a query

I have the following query:
var catInclude = _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Include(x => x.CatItems)
.SingleOrDefault(p => p.Id == request.ProvId
cancellationToken: cancellationToken);
As I don't want to get all properties from CatItems with Include(), I have created the following query:
var catSelect = _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Select(p ==> new
{ Provider = p,
Items = p.CatItems.Select(x => new List<CatItems> { new CatItems
{ Id = x.Id, Name = x.Name, Price = x.Price } }
})})
SingleOrDefault(cancellationToken: cancellationToken);
But something is wrong in the 2nd query because here return _mapper.ProjectTo<CatDto>(cat) I get the following error:
Argument 1: cannot convert from '<anonymous type: Db.Entities.Cat Prov, System.Colletions.Generic.IEnumerable<System.Colletions.Generic.List<Models.CatItems> > Items>' to 'System.Linq.IQueryable'
Here is my CatDto:
public class CatDto
{
public int ProvId { get; set; }
public List<CatItems> CatItems { get; set; }
}
Here are my entities:
public class Prov
{
public int Id { get; set; }
public Cat Cat { get; set; }
}
public class Cat
{
public int Id { get; set; }
public int ProvId { get; set; }
public List<CatItems> CatItems { get; set; }
}
public class CatItems
{
public int Id { get; set; }
public int CatId { get; set; }
public DateTime CreatedOn { get; set; }
}
Is there a way to recreate the 2nd query and use it?
Main difference that instead of returning List of CatItems, your code returns IEnumerable<List<CatItems>> for property Items.
So, just correct your query to project to List:
var catSelect = await _db.Cat
.Where(x => x.ProvId == request.ProvId)
.Select(p => new CatDto
{
ProvId = p.ProvId,
Items = p.CatItems.Select(x => new CatItems
{
Id = x.Id,
Name = x.Name,
Price = x.Price
})
.ToList()
})
.SingleOrDefaultAsync(cancellationToken: cancellationToken);
I mean, even the exception is pretty self-explanatory. Nevertheless:
You are performing a .Select(...). It returns an Anonymous type. So, your catSelect is an anonymous type, thus the AutoMapper fails.
The quickest fix is to just cast (Cat)catSelect before mapping.
Or, you can dig deeper into how does AutoMapper play with anonymous types.
I feel like you can make most of the classes inherent Id and why is public cat CAT {get; set;} i thought you were supposed to initialize some kind of value

Let AutoMapper create destination objects only once and reuse them

In the following sample console program, AutoMapper generates 3 DestinationAuthor objects, even though the source hierarchy only uses 2 distinct SourceAuthor objects (but one of them twice).
What I am looking for is to let AutoMapper generate only one distinct destination object for every distinct source object, and then reference this one destination object as many times as necessary during mapping, instead of creating duplicates.
The result would be an object hierarchy, where all destination objects and references mimic exactly the ones from the source hierarchy.
using System.Diagnostics;
using System.Linq;
using AutoMapper;
namespace AutomapperMapOnce
{
public class SourceBlog
{
public int BlogId { get; set; }
public string Title { get; set; }
public SourceAuthor Author { get; set; }
}
public class SourceAuthor
{
public int AuthorId { get; set; }
public string Name { get; set; }
}
public class DestinationBlog
{
public int BlogId { get; set; }
public string Title { get; set; }
public DestinationAuthor Author { get; set; }
}
public class DestinationAuthor
{
public int AuthorId { get; set; }
public string Name { get; set; }
}
internal static class Program
{
private static SourceBlog[] GetSourceBlogs()
{
var sourceAuthors = new[]
{
new SourceAuthor {AuthorId = 1, Name = "John"},
new SourceAuthor {AuthorId = 2, Name = "Sam"}
};
var sourceBlogs = new[]
{
new SourceBlog
{
BlogId = 1,
Title = "First Blog",
Author = sourceAuthors.First(a => a.Name == "John")
},
new SourceBlog
{
BlogId = 2,
Title = "Second Blog",
Author = sourceAuthors.First(a => a.Name == "John")
},
new SourceBlog
{
BlogId = 3,
Title = "Another Blog",
Author = sourceAuthors.First(a => a.Name == "Sam")
}
};
Trace.Assert(sourceAuthors.Distinct().Count() == 2);
Trace.Assert(sourceBlogs.Select(b => b.Author).Distinct().Count() == 2);
return sourceBlogs;
}
private static void Main()
{
var mapper = new MapperConfiguration(
cfg =>
{
cfg.CreateMap<SourceBlog, DestinationBlog>();
cfg.CreateMap<SourceAuthor, DestinationAuthor>();
}).CreateMapper();
var sourceBlogs = GetSourceBlogs();
var destinationBlogs = mapper.Map<DestinationBlog[]>(sourceBlogs);
// Throws, because there are 3 distinct DestinationAuthor objects.
Trace.Assert(destinationBlogs.Select(b => b.Author).Distinct().Count() == 2);
}
}
}
(I could probably setup some kind of objects store in form of a dictionary, and then manually resolve instances through that, but this must be a very common AutoMapper scenario, so I assume this functionality already exists in the core or some extension package.)
Of course it is supported and pretty simple after all. Just use .PreserveReferences() (see 5.0 Upgrade Guide: Circular references):
var mapper = new MapperConfiguration(
cfg =>
{
cfg.CreateMap<SourceBlog, DestinationBlog>()
.PreserveReferences();
cfg.CreateMap<SourceAuthor, DestinationAuthor>()
.PreserveReferences();
}).CreateMapper();

Automapper Unflatten to List

I have flat tabular data that I need to turn into hierarchical data, and I'm trying to do this with AutoMapper. Here is a sketch of the tabular DTO source and the master/detail destination classes.
public class FlatDTO
{
public string Supplier { get; set; }
public string OrderNumber { get; set; }
public string ItemNumber { get; set; }
public string Amount { get; set; }
}
and the destination objects look like this:
public class Order
{
public string AccountName { get; set; }
public string OrderNumber { get; set; }
List<OrderLines> OrderLines { get; set; }
}
public class OrderLines
{
public string Item { get; set; }
public string Amount { get; set; }
}
My Automapper profile looks like this:
public class MyAutomapperProfile : Profile
{
public MyAutomaperProfile ()
{
CreateMap<FlatDto, Order>()
.ForMember(des => des.Account, opt => opt.MapFrom(src => src.Supplier))
.ReverseMap();
CreateMap<FlatDto, OrderLines>()
.ForMember(des => des.Item, opt => opt.MapFrom(src => src.Item))
.ReverseMap();
}
}
The function performing the Transform:
public Order Transform (List<FlatDto> data)
{
var output = injectedFromCtorIMapper.Map<Order>(data);
//throws AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping
}
Example Source Data:
Customer1 1234 Item1 200
Customer1 1234 Item2 500
Customer1 1234 Item3 4000
Target should look like (not actually Json but looks better):
Order
{
AccountName = "Customer 1",
OrderNumber = "1234",
OrderLines [
OrderLine
{
Item: "Item1",
Amount: 200
},
OrderLine
{
Item: "Item2",
Amount: 500
}
OrderLine,
{
Item: "Item3",
Amount: 4000
}
]
}
A few Questions:
Is this possible with Automapper, and what should the mapping profile look like to support this scenario?
How would Automapper handle if row 2 had a value of 'Customer 2'? Overwrite the first value i.e. last one in wins?
To complete the edit of Stefan; here's an example of how you can do it if you need to have a multiple group by.
Understanding your expected result in your question, you want to group the master/detail data on AcccountName and OrderNumber.
This can be achieved by, indeed, mapping the IEnumerable and by using the Automapper's "ProjectTo" extension (from namespace AutoMapper.QueryableExtensions).
In my sample data I added one row with a different OrderNumber to illustrate this.
using AutoMapper;
using AutoMapper.QueryableExtensions;
using System.Collections.Generic;
using System.Linq;
namespace AutomapperUnflatten
{
public class FlatDTO
{
public string Supplier { get; set; }
public string OrderNumber { get; set; }
public string ItemNumber { get; set; }
public string Amount { get; set; }
}
public class Order
{
public string AccountName { get; set; }
public string OrderNumber { get; set; }
public List<OrderLines> OrderLines { get; set; }
}
public class OrderLines
{
public string Item { get; set; }
public string Amount { get; set; }
}
class Program
{
static void Main(string[] args)
{
var configuration = new MapperConfiguration(cfg => {
cfg.CreateMap<IEnumerable<FlatDTO>, Order>()
.ForMember(d => d.AccountName, opt => opt.MapFrom(src => src.FirstOrDefault().Supplier))
.ForMember(d => d.OrderNumber, opt => opt.MapFrom(src => src.FirstOrDefault().OrderNumber))
.ForMember(d => d.OrderLines, opt => opt.MapFrom(src =>
src.Select(s => new OrderLines() { Item = s.ItemNumber, Amount = s.Amount })));
});
var inputData = new List<FlatDTO>()
{
new FlatDTO(){ Supplier = "Customer1", OrderNumber = "1234", ItemNumber = "Item1", Amount = "200"},
new FlatDTO(){ Supplier = "Customer1", OrderNumber = "1234", ItemNumber = "Item1", Amount = "500"},
new FlatDTO(){ Supplier = "Customer1", OrderNumber = "1234", ItemNumber = "Item1", Amount = "4000"},
new FlatDTO(){ Supplier = "Customer1", OrderNumber = "9999", ItemNumber = "Item1", Amount = "4000"},
};
var result = inputData.GroupBy(c => (c.Supplier, c.OrderNumber)).Select(f => f).AsQueryable().ProjectTo<Order>(configuration);
}
}
}
Is this possible with Automapper, and what should the mapping profile look like to support this scenario?
Yes, this is possible - but do note: also AutoMapper has it's limitations. Not all transformations are suited to be handled with AutoMapper.
Do note: List<OrderLines> OrderLines { get; set; } must be public
I don't have all the details yes, but basically you can create a map from IEnumerable<FlatDTO> to Order. I will work out an example.
Example
CreateMap<IEnumerable<FlatDTO>, Order>()
.ForMember(d => d.AccountName, o => o.MapFrom(s=> s.Select(c => c.Supplier).FirstOrDefault()))
.ForMember(d => d.OrderLines, o => o.MapFrom(s=> s.Select(c => new OrderLines()
{
Amount = c.Amount,
Item = c.ItemNumber,
})));
How would Automapper handle if row 2 had a value of 'Customer 2'? Overwrite the first value i.e. last one in wins?
Depending on your mapping logic, it depends. You can even perform a check on these doubles by using the AfterMap or BeforeMap methods - doesn't make it faster though.
Ideally, you initial set can be mapped to a single order, thus, containing a single customer. - Otherwise, you'll need a mapping from IEnumerable<FlatDTO> to IEnumerable<Order> - which is really pushing the limits.
Tester
static void Main(string[] args)
{
var dtos = new[]
{
new FlatDTO() {Supplier = "you", Amount = "10", ItemNumber = "1", OrderNumber = "123"},
new FlatDTO() {Supplier = "you", Amount = "12", ItemNumber = "2", OrderNumber = "234"}
};
var config = new MapperConfiguration(cfg => cfg.AddProfile<MyAutomapperProfile>());
var mapper = config.CreateMapper();
var order = mapper.Map<IEnumerable<FlatDTO>, Order>(dtos);
}

AutoMapper Mapping a collection of strings to a property of a collection inside another collection

How to mapping IdContributors (collection of strings) to a collection (Contributors), inside a collection TAction, with a property (ContributorId) of string, using LINQ and AutoMapper ?
public ViewModelToDomainMappingProfile()
{
CreateMap<ActionViewModel, TAction>();
//.ForMember(d => d.Contributors, opt => opt.MapFrom(a => ids = a.IdContributors.Select(x => { })));
}
Models
Model TAction
public class TAction
{
public Guid Id {get; set; }
public string Title { get; set; }
public string Description { get; set; }
public ICollection<TActionContributor> Contributors { get; set; }
}
public class TActionContributor
{
public Guid TActionId { get; set; }
[ForeignKey("TActionId")]
public TAction Action { get; set; }
public string ContributorId { get; set; }
[ForeignKey("ContributorId")]
public ApplicationUser Contributor { get; set; }
}
ActionViewModel
public class ActionViewModel
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public ICollection<string> IdContributors { get; set; }
}
I can see two choices here, you'll choose the solution which better suits your needs:
1) Select() in from MapFrom()
CreateMap<ActionViewModel, TAction>()
.ForMember(
destination => destination.Contributors,
options => options.MapFrom((source, destination) =>
{
var contributors = source
.IdContributors
.Select(id => new TActionContributor
{
Action = destination,
Contributor = new ApplicationUser
{
Id = id,
},
ContributorId = id,
TActionId = source.Id,
})
.ToList();
contributors.ForEach(ac => ac.Contributor.Contributors = contributors);
return contributors;
}));
2) Custom ValueResolver
This is the same as the first solution, maybe more readable if you want to keep your mapping profiles clean and move the custom logic away when possible.
public class TActionContributorValueResolver : IValueResolver<ActionViewModel, TAction, ICollection<TActionContributor>>
{
public ICollection<TActionContributor> Resolve(
ActionViewModel source,
TAction destination,
ICollection<TActionContributor> destMember,
ResolutionContext context)
{
var contributors = source
.IdContributors
.Select(id => new TActionContributor
{
Action = destination,
Contributor = new ApplicationUser
{
Id = id,
},
ContributorId = id,
TActionId = source.Id,
})
.ToList();
contributors.ForEach(ac => ac.Contributor.Contributors = contributors);
return contributors;
}
}
Configuration:
CreateMap<ActionViewModel, TAction>()
.ForMember(
destination => destination.Contributors,
options => options.MapFrom<TActionContributorValueResolver>());
Final note:
contributors.ForEach(ac => ac.Contributor.Contributors = contributors);
Line above allows you to traverse endlessly from TAction through TActionContributor to ApplicationUser and the other way. If you don't need that feature, feel from to remove it and return the contributors list right away.

C# and MongoDriver - How to get values from foreign collection (aggregate + lookup)?

I've got a problem with getting values from foreign collection in C#.
In this case I can easily get values from list:
var gamesList = gamesCollection.Find(_ => true).ToList();
foreach (var item in gamesList)
{
Console.WriteLine($"{item.Title}");
}
But when I'm using aggregate with lookup function, I can not access to values from foreign collection.
Here are my two collections which I try to join:
public class GameModel
{
[BsonId]
public ObjectId Id { get; set; }
public string Title { get; set; }
public List<String> Type { get; set; }
public string GameMode { get; set; }
public List<String> Platform { get; set; }
public string Production { get; set; }
}
public class FavouriteGameModel
{
[BsonId]
public ObjectId Id { get; set; }
public ObjectId UserID { get; set; }
public ObjectId GameID { get; set; }
}
And here's the part of problematic code:
var joinedFavGamesList = favouriteGamesCollection.Aggregate().Match(x => x.UserID == loggedUser[0].Id).//ToList();
Lookup("Games", "GameID", "_id", #as: ("myAlias")).
Project(
new BsonDocument { { "_id", 0 }, { "myAlias.Title", 1 } }
).ToList();
Is there any way to invoke to myAlias.Title? I want only this value to display, but i get:
{ "myAlias" : [{ "Title" : "Some Game" }] }
I will be greatful if someone could look at this and tell me what I'm doing wrong. Thanks
my choice would be to join/lookup using the AsQueryable interface like so:
var favGames = favCollection.AsQueryable()
.Where(fg=> fg.UserID== "xxxxxxxxxxx")
.Join(gameCollection.AsQueryable(), //foreign collection
fg => fg.GameID, //local field
gm => gm.ID, //foreign field
(fg, gm) => new { gm.Title }) //projection
.ToList();
with aggregate interface:
public class JoinedGameModel
{
public GameModel[] Results { get; set; }
}
var favGames = favGameCollection.Aggregate()
.Match(fg => fg.UserID == "xxxxxxxxxxxx")
.Lookup<FavouriteGameModel, GameModel, JoinedGameModel>(
gameCollection,
fg => fg.GameID,
gm => gm.ID,
jgm => jgm.Results)
.ReplaceRoot(jgm => jgm.Results[0])
.Project(gm => new { gm.Title })
.ToList();

Categories