C
C#•2y ago
malkav

Generic Dto Mapper

possibly oddball question, but I'm trying to make Dto mappers, and I came across List<T> and Array<T> items in my Dto's that I needed to map by using a foreach loop for each of these items. But I was wondering if there is a way to create a generic mapper of sorts for these things? I mean I tried starting, but I get stuck.. Currently each of these loops does the same, but with different properties in a different model class. Here's an example of the foreach loop they do:
foreach (TargetDto item in dto.ListProperty)
{
sourceDto.ListProperty.Add(
new()
{
// each property is set here like so:
PropertyName = item.PropertyName
});
}
foreach (TargetDto item in dto.ListProperty)
{
sourceDto.ListProperty.Add(
new()
{
// each property is set here like so:
PropertyName = item.PropertyName
});
}
however when I try to do this in a static helper class, I get stuck on the following:
public static ICollection<T> ComplexMapper<TS, T>(this ICollection<TS> source, ICollection<T> target)
{
foreach (var item in target)
{
source.Add(
// And now I'm stuck because I can't create 'new()' of T because it does not have a new() keyword
);
}
}
public static ICollection<T> ComplexMapper<TS, T>(this ICollection<TS> source, ICollection<T> target)
{
foreach (var item in target)
{
source.Add(
// And now I'm stuck because I can't create 'new()' of T because it does not have a new() keyword
);
}
}
49 Replies
atakancracker
atakancracker•2y ago
Is there specific reason to not use reliable libraries for mapping such as AutoMapper ?
malkav
malkav•2y ago
because it's also educational for me 😅
Timtier
Timtier•2y ago
Couldn't you add a condition for this mapper? Something along the lines of where T : new() behind the method definition?
malkav
malkav•2y ago
I've been using the project I'm working on to write a lot of these static generic helper methods to learn about them
atakancracker
atakancracker•2y ago
Its hard to manage nested collections
malkav
malkav•2y ago
Apparently not, because T or TS does not have the new() constraint I know, which makes it a perfect educational topic 🤣
Timtier
Timtier•2y ago
But you can enforce it using the snippet I described, right 🤔
malkav
malkav•2y ago
I tried that.. here's what I have:
public static ICollection<T> ComplexObjectMapper<TS, T>(this ICollection<TS> source, ICollection<T> target) where T : new()
{
foreach (T t in target)
{
source.Add(
new() // Here I get 'cannot create instance of type parameter 'T' because it does not have the new() constraint
{

});
}
}
public static ICollection<T> ComplexObjectMapper<TS, T>(this ICollection<TS> source, ICollection<T> target) where T : new()
{
foreach (T t in target)
{
source.Add(
new() // Here I get 'cannot create instance of type parameter 'T' because it does not have the new() constraint
{

});
}
}
Timtier
Timtier•2y ago
And why specifically are you trying to add an instance of type T while the source is using type TS?
malkav
malkav•2y ago
Oh, my bad, it says TS.
malkav
malkav•2y ago
Timtier
Timtier•2y ago
In that case the same where TS : new() would apply, wouldn't it? Pretty sure you can also do it for both
malkav
malkav•2y ago
okay, so how do I get all the keys now..
Timtier
Timtier•2y ago
What's 'keys' in this context? Each property?
malkav
malkav•2y ago
yea each of the TS properties, as well as the T properties. So that I can do source.Add( new() { TS.PropertyOne = T.PropertyOne } ); sort of..
Timtier
Timtier•2y ago
Something along the lines of, keep in mind I haven't validated it;
public static ICollection<T> ComplexObjectMapper<TS, T>(this ICollection<TS> source, ICollection<T> target)
where T : new()
where TS : new()
{
// Get a collection of properties your target type has.
var targetProps = typeof(T).GetProperties();

foreach(var item in source)
{
// Construct a new object to fill.
T obj = new T();

// Loop through your properties, if it can be written to we try to get the value from TS and set it.
foreach(var property in targetProps)
{
if (property is not null && property.CanWrite)
{
var sourceValue = typeof(T).GetProperty(property.Name).GetValue(item);
property.SetValue(obj, sourceValue);
}
}

target.Add(obj);
}

return target;
}
public static ICollection<T> ComplexObjectMapper<TS, T>(this ICollection<TS> source, ICollection<T> target)
where T : new()
where TS : new()
{
// Get a collection of properties your target type has.
var targetProps = typeof(T).GetProperties();

foreach(var item in source)
{
// Construct a new object to fill.
T obj = new T();

// Loop through your properties, if it can be written to we try to get the value from TS and set it.
foreach(var property in targetProps)
{
if (property is not null && property.CanWrite)
{
var sourceValue = typeof(T).GetProperty(property.Name).GetValue(item);
property.SetValue(obj, sourceValue);
}
}

target.Add(obj);
}

return target;
}
This is ofc very naive and does not consider if it does not exist on the source type etc etc.
cap5lut
cap5lut•2y ago
u could pass a delegate for the actual mapping, but then u more or less have the same thing as linq:
public static ICollection<TTo> ComplexObjectMapper<TFrom, TTo>(this IEnumerable<TFrom> source, ICollection<TTo> target, Func<TFrom, TTo> mapper)
{
foreach (var element: source)
{
target.Add(mapper(element));
}
}
public static ICollection<TTo> ComplexObjectMapper<TFrom, TTo>(this IEnumerable<TFrom> source, ICollection<TTo> target, Func<TFrom, TTo> mapper)
{
foreach (var element: source)
{
target.Add(mapper(element));
}
}
which u could use like:
var source = new int[] { 1, 2, 3 };
var target = new List<string>();
source.ComplexObjectMapper(target, element => element.ToString());
var source = new int[] { 1, 2, 3 };
var target = new List<string>();
source.ComplexObjectMapper(target, element => element.ToString());
(if u want filtering u could add Func<TFrom, bool> parameter.) but u could also just do something like var target = source.Where(element = element >= 2).Select(element => element.ToString()); (this is linq btw) by using a delegate as parameter u can avoid writing dozens of similar methods
malkav
malkav•2y ago
okay, so I used this, but then I must be doing something wrong 😅 help me understand this You're mapping from source into target, by getting all the properties from source, and telling the target object to use those property names and give the source value of that propertyName to the target propertyName. but now, when I want to do SomeListOfComplexItems.ComplexObjectMapper<ComplexItem, ComplexDto>(SomeListOfComplexDtoItems); I get the following error: Cannot convert from System.Collections.Generic.List<ComplexModel> to System (and that's where the error message cuts off, but I recon it says System.Collections.Generic.List<ComplexDto>
cap5lut
cap5lut•2y ago
hard to tell without knowing the real code
malkav
malkav•2y ago
Wow... now my mind is melting 🤣 My objects are not as simple as ints and strings though.. I basically have:
public class SomeModel
{
public string SomeProperty {get;set;}
public int SomeIdentifier {get;set;}
}

public class SomeDto
{
public string SomeProperty {get;set;}
public int SomeIdentifier {get;set;}
}
public class SomeModel
{
public string SomeProperty {get;set;}
public int SomeIdentifier {get;set;}
}

public class SomeDto
{
public string SomeProperty {get;set;}
public int SomeIdentifier {get;set;}
}
so I would then use Linq to do what now?
var source = new List<SomeModel>();
var target = new List<SomeDto>();
source.Where(element => element..... what now???
var source = new List<SomeModel>();
var target = new List<SomeDto>();
source.Where(element => element..... what now???
okay, lemme give you what I have now..
public static model MapHoursToModel(dto dto)
{
model result = new()
{
ModelId = dto.ModelId,
CreatedAt = dto.CreatedAt,
UpdatedAt = dto.UpdatedAt,
Month = dto.Month,
Year = dto.Year,
UserMonth = dto.UserMonth,
WorkedDays = new()
};
result.WorkedDays = dto.WorkedDays.ComplexObjectMapper<HostDaysWorked, DomainDaysWorked>(result.WorkedDays);
// foreach (DomainDaysWorked item in dto.WorkedDays)
// {
// result.WorkedDays.Add(
// new()
// {
// DayWorked = item.DayWorked,
// Hours = item.Hours,
// Assignment = item.Assignment,
// Remarks = item.Remarks,
// EntryState = item.EntryState,
// TypeOfHours = item.TypeOfHours
// });
// }
return result;
}
public static model MapHoursToModel(dto dto)
{
model result = new()
{
ModelId = dto.ModelId,
CreatedAt = dto.CreatedAt,
UpdatedAt = dto.UpdatedAt,
Month = dto.Month,
Year = dto.Year,
UserMonth = dto.UserMonth,
WorkedDays = new()
};
result.WorkedDays = dto.WorkedDays.ComplexObjectMapper<HostDaysWorked, DomainDaysWorked>(result.WorkedDays);
// foreach (DomainDaysWorked item in dto.WorkedDays)
// {
// result.WorkedDays.Add(
// new()
// {
// DayWorked = item.DayWorked,
// Hours = item.Hours,
// Assignment = item.Assignment,
// Remarks = item.Remarks,
// EntryState = item.EntryState,
// TypeOfHours = item.TypeOfHours
// });
// }
return result;
}
I commented out the foreach, because that's how I did it when I went literal. I'm doing the same for a much more complex object with four to six of those foreach loops that do basically the same, but with different properties the ComplexObjectMapper() is what was sent earlier:
public static ICollection<T> ComplexObjectMapper<TS, T>(this ICollection<TS> source, ICollection<T> target)
where TS : new()
where T : new()
{
PropertyInfo[] targetProperties = typeof(T).GetProperties();

foreach (TS item in source)
{
T obj = new();

foreach (PropertyInfo? prop in targetProperties)
{
if (prop is null) continue;
object? sourceValue = typeof(T).GetProperty(prop.Name).GetValue(item);
prop.SetValue(obj, sourceValue);
}
target.Add(obj);
}

return target;
}
public static ICollection<T> ComplexObjectMapper<TS, T>(this ICollection<TS> source, ICollection<T> target)
where TS : new()
where T : new()
{
PropertyInfo[] targetProperties = typeof(T).GetProperties();

foreach (TS item in source)
{
T obj = new();

foreach (PropertyInfo? prop in targetProperties)
{
if (prop is null) continue;
object? sourceValue = typeof(T).GetProperty(prop.Name).GetValue(item);
prop.SetValue(obj, sourceValue);
}
target.Add(obj);
}

return target;
}
cap5lut
cap5lut•2y ago
u would use a Select() call there, which is used to transform/map elements. Where() is to filter elements
var target = source.Select(element => new SomeDto {
SomeProperty: element.SomeProperty,
SomeIdentifier: element.SomeIdentifier
});
var target = source.Select(element => new SomeDto {
SomeProperty: element.SomeProperty,
SomeIdentifier: element.SomeIdentifier
});
malkav
malkav•2y ago
Ah, so a Linq way of doing my commented out foreach loops 🤣 that's already much closer to what I wanted
cap5lut
cap5lut•2y ago
note that target is now only a IEnumerable<SomeDto>, if u would for example would want a List<SomeDto>, u would do an ToList() call at the end
malkav
malkav•2y ago
fair enough. I'm just trying to figure it out though.. eh... So List<SomeDto> target = source.Select(element => new SomeDto { SomeProperty: element.SomeProperty } ).ToList() right?
cap5lut
cap5lut•2y ago
yep
malkav
malkav•2y ago
Dude...
malkav
malkav•2y ago
you're a hero and a legend 🤣
cap5lut
cap5lut•2y ago
actually the linq authors r ;p
malkav
malkav•2y ago
though still I'd love to learn how to generic that though... but I'll figure that out eventually yea they are too.. Linq has saved my ass so many times now
cap5lut
cap5lut•2y ago
how to generic that though
basically without passing a delegate?
mrphil2105
mrphil2105•2y ago
Uhhhh reflection is quite slow I'd suggest using Mapster or AutoMapper
cap5lut
cap5lut•2y ago
yeah, im not sure what they r meaning, so thats why i was asking linq isnt that fast either ;p
mrphil2105
mrphil2105•2y ago
Faster than reflection though AFAIK
malkav
malkav•2y ago
o.O
cap5lut
cap5lut•2y ago
ofc, reflection is really slow and complex, an alternative would be source generators but thats even more complex. imo either way it would be adding too much magic to it which can be quite error prone and thus not worth it
malkav
malkav•2y ago
Fair enough..
mrphil2105
mrphil2105•2y ago
Some LINQ functions are getting a speed boost though in .NET 7 Min and Max for example are getting vectorization finally
cap5lut
cap5lut•2y ago
well, talking about optimization at this stage isnt really helpful, this might not even be the bottleneck of the program. write it working -> refactor it clean -> check bottlenecks -> optimize them
mrphil2105
mrphil2105•2y ago
Still, using reflection for mapping is not a good idea like this Just use Mapster :)
malkav
malkav•2y ago
The thing is that the senior at my work gave me this "skeleton" format I should follow, and he's making me write my own mappers 😅
mrphil2105
mrphil2105•2y ago
Mapster has really good performance And is easy to use
malkav
malkav•2y ago
fair enough
mrphil2105
mrphil2105•2y ago
It can also generate mapping code
cap5lut
cap5lut•2y ago
oh mapster uses code generation
mrphil2105
mrphil2105•2y ago
You can do that too You can also just use the global mapper Or use the interface it provides
malkav
malkav•2y ago
The skeleton I'm using is basically: Host.MyApp Host.UnitTests Domain.PortsAndAdapterInterfaces Adapter.Persistence and a Mapper between each of those boundaries
mrphil2105
mrphil2105•2y ago
So it has a few options
malkav
malkav•2y ago
But you guys have been legends, I'll close the post now as I'm helped! thanks!!!