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;
}
}
CodePudding user response:
@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.
CodePudding user response:
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);
}
