问题
I'm currently trying to implement a generic method to adapt a DTO that comes from an external service into a model of my servisse and I ran into an issue. First let me just contextualize the problem. Let's say that an external service returns the following DTO to my application.
public class ExampleDTO
{
public int Field1 { get; set; }
public int Field2 { get; set; }
}
And this is my model.
public class ExampleModel
{
public int Field1 { get; set; }
public int Field2 { get; set; }
}
If I wanted to adapt the first class into my model I could just simply write the following method:
public ExampleModel Adapt(ExampleDTO source)
{
if (source == null) return null;
var target = new ExampleModel()
{
Field1 = source.Field1,
Field2 = source.Field2
};
return target;
}
Now let's say that when we get a collection of ExampleDTOs, the service instead of just returning a collection of type ICollection, returns the following class:
public class PagedCollectionResultDTO<T>
{
public List<T> Data { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int Total { get; set; }
}
Where the list of ExampleDTOs comes on the Data field and the page number, page size and total number of records comes on the rest of the fields.
I'm trying to implement a generic method to adapt this class to my own model, which has the same structure. I want to do this independently of the type T of the Data field.
public class PagedCollectionResult<T>
{
public List<T> Data { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int Total { get; set; }
public PagedCollectionResult() => (Data, Page, PageSize, Total) = (new List<T>(), 0, 0, 0);
}
I've tried the following method, where I try to adapt the DTO paged result (S) into the model paged result (T) :
public PagedCollectionResult<T> Adapt<S,T>(PagedCollectionResultDTO<S> source)
{
if (source == null) return null;
var target = new PagedCollectionResult<T>();
foreach (var item in source.Data)
target.Data.Add(this.Adapt(item));
target.Page = source.Page;
target.PageSize = source.PageSize;
target.Total = source.Total;
return target;
}
The thing is that I'm getting an error in the line:
target.Data.Add(this.Adapt(item));
It says that it can't convert S into ExampleDTO. If I put a restriction for ExampleDTO/ExampleModel on Adapt this isn't generic anymore. Is there a way to call the Adapt(item) method for the specific type?
This is my complete TypeAdapter:
public class TypeAdapter
{
public PagedCollectionResult<T> Adapt<S,T>(PagedCollectionResultDTO<S> source)
{
if (source == null) return null;
var target = new PagedCollectionResult<T>();
foreach (var item in source.Data)
target.Data.Add(this.Adapt(item));
target.Page = source.Page;
target.PageSize = source.PageSize;
target.Total = source.Total;
return target;
}
public ExampleModel Adapt(ExampleDTO source)
{
if (source == null) return null;
var target = new ExampleModel()
{
Field1 = source.Field1,
Field2 = source.Field2
};
return target;
}
}
回答1:
As I understand it, the solution is broken into two parts, that should work independently from one another.
Part A: generic pattern for object transformation
Create an interface to be implemented by all your models that need to be adapted from an external DTO.
public interface IExampleModel<S>
{
void Adapt(S source);
}
This interface requires only an Adapt
method and the generic type S
describes the type to adapt from.
Now build a class for each model that you want to adapt from another type.
public class ExampleModel : IExampleModel<ExampleDTO>
{
public int Field1 { get; set; }
public int Field2 { get; set; }
public void Adapt(ExampleDTO source)
{
Field1 = source.Field1;
Field2 = source.Field2;
}
}
Your TypeAdapter class:
public class TypeAdapter
{
public PagedCollectionResult<T> Adapt<S,T>(PagedCollectionResultDTO<S> source)
where T: IExampleModel<S>, new()
{
var target = new PagedCollectionResult<T>();
target.Page = source.Page;
target.Page = source.PageSize;
target.Total = source.Total;
target.Data = AdaptList<S,T>(source.Data).ToList();
return target;
}
protected IEnumerable<T> AdaptList<S,T>(IEnumerable<S> sourceList)
where T : IExampleModel<S>, new()
{
foreach (S sourceItem in sourceList)
{
T targetItem = new T();
targetItem.Adapt(sourceItem);
yield return targetItem;
}
}
}
NOTE 1: the PagedCollectionResult
constructor is not required in this approach.
NOTE 2: I chose to put the Adapt
method in each model class rather than in the TypeAdapter to satisfy the open-closed principle. This way when you want to extend the solution in order to accept a second type transformation, you don't modify any existing classes (i.e. the TypeModifier
), but add instead a new class.
public class ExampleModel2 : IExampleModel<ExampleDTO2>
{
public int Field1 { get; set; }
public int Field2 { get; set; }
public void Adapt(ExampleDTO2 source)
{
Field1 = source.Field1;
Field2 = source.Field2;
}
}
Adding this class will extend the solution to include this transformation as well.
Ofcourse you can choose your own way if it suits your application better.
Part B: the service call
There isn't a way, that I know of, for the application to extract the target type, given only the source type. You must provide it in the service call. You have no description on what your service call looks like, so I will just give you some hints.
If your service call is something like this it would work:
public void ServiceCall<S,T>(PagedCollectionResultDTO<S> sourceCollection)
where T : IExampleModel<S>, new()
{
var typeAdapter = new TypeAdapter();
var targetCollection = typeAdapter.Adapt<S,T>(sourceCollection);
}
If passing the T type in the service call is not possible you might use an if-clause to properly define the target type:
public void ServiceCall<S>(PagedCollectionResultDTO<S> sourceCollection)
{
var typeAdapter = new TypeAdapter();
if (typeof(S) == typeof(ExampleDTO))
{
var targetCollection = typeAdapter.Adapt<ExampleDTO, ExampleModel>(sourceCollection as PagedCollectionResultDTO<ExampleDTO>);
}
else if(typeof(S) == typeof(ExampleDTO2))
{
var targetCollection = typeAdapter.Adapt<ExampleDTO2, ExampleModel2>(sourceCollection as PagedCollectionResultDTO<ExampleDTO2>);
}
}
回答2:
You have two choices here, 1. Generate list of properties of Model and Dto via reflection. and then match their types.
class AdapterHelper<T1, T2>
{
public T1 Adapt(T2 source)
{
T1 targetItem = Activator.CreateInstance<T1>();
var props = typeof(T1).GetProperties();
var targetProps = typeof(T2).GetProperties();
foreach (var prop in props)
{
foreach (var targetProp in targetProps)
{
if (prop.Name == targetProp.Name)
{
targetProp.SetValue(targetItem, prop.GetValue(source));
//assign
}
}
}
return targetItem;
}
}
2.Use Automapper
回答3:
Because you are implementing a generic method, you need to either implement a generic method for transforming S to T (see other answers), or you need to pass in the transformation function.
public PagedCollectionResult<T> Adapt<S, T>(PagedCollectionResultDTO<S> source, Func<S, T> adapt)
{
if (source == null) return null;
var target = new PagedCollectionResult<T>();
foreach (var item in source.Data)
target.Data.Add(adapt(item));
target.Page = source.Page;
target.PageSize = source.PageSize;
target.Total = source.Total;
return target;
}
Here is example code to call the above method.
static void Main(string[] args)
{
var src = new PagedCollectionResultDTO<ExampleDTO>();
src.Data = new List<ExampleDTO>{
new ExampleDTO{ Field1 = 1, Field2 = 2 }
};
var adapter = new TypeAdapter();
var result = adapter.Adapt(src, AdaptExampleDTO);
}
public static ExampleModel AdaptExampleDTO(ExampleDTO source)
{
if (source == null) return null;
var target = new ExampleModel()
{
Field1 = source.Field1,
Field2 = source.Field2
};
return target;
}
回答4:
Thank you all for your responses, they helped me getting in the right direction. I used reflection at runtime to resolve the right adapt method. I learned a few things about reflection thanks to you. I'm sharing the solution in hope I can also give something back. This is what I ended up with.
public PagedCollectionResult<T> Adapt<S, T>(PagedCollectionResultDTO<S> source)
{
if (source == null)
{
return null;
}
var target = new PagedCollectionResult<T>();
// Get the type of S at runtime
Type[] types = { typeof(S) };
// Find the Adapt method on the TypeAdapter class that accepts an object of type S
var adaptMethod = typeof(TypeAdapter).GetMethod("Adapt", types);
foreach (var item in source.Data)
{
// for each item call the adapt method previously resolved and pass the item as parameter
var parameters = new object[] { item };
target.Data.Add((T)adaptMethod.Invoke(this, parameters));
}
target.Page = source.Page;
target.PageSize = source.PageSize;
target.Total = source.Total;
return target;
}
来源:https://stackoverflow.com/questions/51733971/calling-a-particular-implementation-inside-a-generic-method