简体   繁体   中英

How to call generic method

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM