I have two related models
ModelBase.cs
public class ModelBase
{
public virtual ModelBase Map(DataRow dr)
{
return this;
}
}
User.cs which derives from above class. In the future I want to have more classes like user where I can map fields from DataRow to my properties
public class User : ModelBase
{
public string Id { get; set; }
public string Surname { get; set; }
public string Name { get; set; }
public User() { }
public override User Map(DataRow dr)
{
var config = new MapperConfiguration
(cfg => cfg.CreateMap<DataRow, User>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(row => row["x"]))
.ForMember(dest => dest.Name, opt => opt.MapFrom(row => row["xx"]))
.ForMember(dest => dest.Surname, opt => opt.MapFrom(row => row["xxx"]))
);
var mapper = config.CreateMapper();
return mapper.Map<User>(dr);
}
}
I am receiving Users as DataTable so I have created simple method to convert it to list, but if there will be more classes (all received as DataTables), I want my function to be flexible and valid with rest of my models. The question is how can I use this dynamically?
Utility.cs
public class DataTableConverter<T> where T : ModelBase
{
public static List<T> ConvertToList(DataTable dt, Type type)
{
var results = new List<T>(dt.Rows.Count);
foreach (DataRow row in dt.Rows)
{
// Here I want to call Map function from dynamic model and add it to results
}
return results;
}
}
You could do something like this:
Make ModelBase
abstract and generic and use the "Curiously Recurring Template Pattern" to enable you to define an abstract Map()
method that returns the derived model types:
public abstract class ModelBase<T> where T : ModelBase<T>
{
public abstract T Map(DataRow dr);
}
Then change User
so that it is instead a ModelBase<User>
(the Map()
method remains unchanged):
public class User : ModelBase<User>
{
...
}
Your DataTableConverter
needs to have a constraint that T
is a ModelBase<T>
and has a parameterless constructor ( new()
). The ConvertToList()
method does not require a Type
parameter to be specified - the type returned is determined by T
:
public class DataTableConverter<T> where T : ModelBase<T>, new()
{
public static List<T> ConvertToList(DataTable dt)
{
var results = new List<T>(dt.Rows.Count);
foreach (DataRow row in dt.Rows)
{
results.Add(new T().Map(row));
}
return results;
}
}
As noted in the comments, the side effect of the above is that each row results in two model objects being created (one when new T()
is called in ConvertToList()
, and then the Map()
method creates a new model object as a result of the mapping. It should be possible to avoid this duplication with a small change to the use of AutoMapper so that it populates the existing model object, rather than creating a new object:
public static User Map(DataRow dr)
{
...
// Populate `this` with the data row, rather than creating a new `User`
return mapper.Map(dr, this);
}
@Iridium's answer describes how to use the CRTP to solve your issue, but has the wart that since Map
is an instance method, you need to create a new, empty User
instance in order to call Map
on it, to get the User
instance you actually want.
If you're using C# 10 (and .NET 6), you can make use of static abstract interface methods . You will probably need <EnablePreviewFeatures>true</EnablePreviewFeatures>
in your csproj (and may also need <LangVersion>preview</LangVersion>
).
This lets you write:
public interface IModelMapper<T> where T : IModelMapper<T>
{
static abstract T Map(DataRow dr);
}
public class User : IModelMapper<User>
{
public string Id { get; set; }
public string Surname { get; set; }
public string Name { get; set; }
public User() { }
public static User Map(DataRow dr)
{
var config = new MapperConfiguration
(cfg => cfg.CreateMap<DataRow, User>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(row => row["x"]))
.ForMember(dest => dest.Name, opt => opt.MapFrom(row => row["xx"]))
.ForMember(dest => dest.Surname, opt => opt.MapFrom(row => row["xxx"]))
);
var mapper = config.CreateMapper();
return mapper.Map<User>(dr);
}
}
public class DataTableConverter<T> where T : IModelMapper<T>
{
public static List<T> ConvertToList(DataTable dt, Type type)
{
var results = new List<T>(dt.Rows.Count);
foreach (DataRow row in dt.Rows)
{
results.Add(T.Map(row));
}
return results;
}
}
See it on dotnetfiddle.net .
Now, the Map
method is static: you don't need to create a new User
instance just to call Map
on it.
If static abstract interface methods aren't available, you can get the same effect (but with a bit more boilerplate) by separating your concerns: split the User
class from the class which does the mapping.
public class User
{
public string Id { get; set; }
public string Surname { get; set; }
public string Name { get; set; }
public User() { }
}
public abstract class ModelMapper<T>
{
public abstract T Map(DataRow dr);
}
public class UserModelMapper : ModelMapper<User>
{
public static UserModelMapper Instance { get; } = new();
public override User Map(DataRow dr)
{
var config = new MapperConfiguration
(cfg => cfg.CreateMap<DataRow, User>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(row => row["x"]))
.ForMember(dest => dest.Name, opt => opt.MapFrom(row => row["xx"]))
.ForMember(dest => dest.Surname, opt => opt.MapFrom(row => row["xxx"]))
);
var mapper = config.CreateMapper();
return mapper.Map<User>(dr);
}
}
You then pass an instance of the right ModelMapper
into DataTableConverter
, and it's this mapper instance which does the conversion:
public static class DataTableConverter
{
public static List<T> ConvertToList<T>(ModelMapper<T> mapper, DataTable dt, Type type)
{
var results = new List<T>(dt.Rows.Count);
foreach (DataRow row in dt.Rows)
{
results.Add(mapper.Map(row));
}
return results;
}
}
See it on dotnetfiddle.net .
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.