简体   繁体   中英

Mapping API result to class, should I use reflection and is this a case of DataAdapter design pattern?

It is my first time using an API and I am not entirely sure about the good practices regarding it. I am using a third party API that returns me a class 'A' in C#, that is not quite compatible with my DTO class 'MyA'. I obviously have to map it to my entity, but I don't know what the best way to do it is. I have read of the Data Adapter pattern and from what I understand, it has the purpose to act as a bridge between two incompatible interfaces. Those classes share some properties, but differ in more important ones, so I wonder if this pattern is what I am looking for.

I believe I could create a base class that uses reflection to map the matching properties for the generic case, and build on top of that for the different properties. I have however heard that reflections should be avoided, so another approach would be to manually map even the shared properties. This would probably be easier to do now, but it is not maintainable at all, involves some avoidable logic repetition, and overall contradicts good design practices.

Would my situation be considered a case usage of DataAdapter? From the definitions I read, I think yes, however the posts I found were predominantly about using different data sources (XML,JSON,CSV), not just classes in the same language with some properties that differ?

The question is very hard to read but I suspect it asks how to map between two similar DTO types. There are libraries that do this, for example AutoMapper .

AutoMapper can map between types based on property names and some conventions that allow it to handle eg flattening, or mapping method results to properties, eg GetTotal() to Total {get;set;} . It's possible to configure specific mappings for properties with different names, but if the types are too different, it may not be worth it.

AutoMapper uses Reflection to find which properties to map but caches that information instead of recalculating it every time.

For example, given a User and UserDTO:

public class User
{
    public string UserName {get;set;}
    public string Email {get;set;}
    public string Address {get;set;}
}

public class UserDTO
{
    public string UserName {get;set;}
    public string Email {get;set;}
}

AutoMapper can map between them directly:

var config = new MapperConfiguration(cfg => {
    cfg.CreateMap<User, UserDTO>();
})
;
var mapper = config.CreateMapper();
...
UserDto dto = mapper.Map<UserDto>(user);

The Flattening example shows how properties of a complex model can be mapped to a simpler DTO based on naming conventions, eg Customer.Name gets mapped to CustomerName . It also shows how Order.GetTotal() gets mapped to

This complex source model:

public class Order
{
    private readonly IList<OrderLineItem> _orderLineItems = new List<OrderLineItem>();

    public Customer Customer { get; set; }

    public OrderLineItem[] GetOrderLineItems()
    {
        return _orderLineItems.ToArray();
    }

    public void AddOrderLineItem(Product product, int quantity)
    {
        _orderLineItems.Add(new OrderLineItem(product, quantity));
    }

    public decimal GetTotal()
    {
        return _orderLineItems.Sum(li => li.GetTotal());
    }
}

public class Product
{
    public decimal Price { get; set; }
    public string Name { get; set; }
}

public class OrderLineItem
{
    public OrderLineItem(Product product, int quantity)
    {
        Product = product;
        Quantity = quantity;
    }

    public Product Product { get; private set; }
    public int Quantity { get; private set;}

    public decimal GetTotal()
    {
        return Quantity*Product.Price;
    }
}

public class Customer
{
    public string Name { get; set; }
}

can be mapped to this simple DTO simply using conventions:

public class OrderDto
{
    public string CustomerName { get; set; }
    public decimal Total { get; set; }
}

without special configuration:

// Configure AutoMapper

var configuration = new MapperConfiguration(cfg =>{ 
    cfg.CreateMap<Order, OrderDto>();
});
// Complex model

var customer = new Customer
    {
        Name = "George Costanza"
    };
var order = new Order
    {
        Customer = customer
    };
var bosco = new Product
    {
        Name = "Bosco",
        Price = 4.99m
    };
order.AddOrderLineItem(bosco, 15);

// Perform mapping

OrderDto dto = mapper.Map<Order, OrderDto>(order);

It's possible to specify complex mappings when it's not possible to map by convention:

public class CalendarEvent
{
    public DateTime Date { get; set; }
    public string Title { get; set; }
}

public class CalendarEventForm
{
    public DateTime EventDate { get; set; }
    public int EventHour { get; set; }
    public int EventMinute { get; set; }
    public string Title { get; set; }
}

In this case each Event property can be mapped individually, with a full mapping expression:

var configuration = new MapperConfiguration(cfg =>
  cfg.CreateMap<CalendarEvent, CalendarEventForm>()
    .ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.Date.Date))
    .ForMember(dest => dest.EventHour, opt => opt.MapFrom(src => src.Date.Hour))
    .ForMember(dest => dest.EventMinute, opt => opt.MapFrom(src => src.Date.Minute)));

This makes sense when only a few properties need custom mapping, eg when only 2 out of 10 properties need mapping. It also helps when mappings can occur in many places.

For this example though, 3 out of 4 properties need mapping and the "manual" code is actually simpler than the configuration itself:

var dest=new CalendarEventForm {
    Title     =scr.Title,
    EventDate =src.Date.Date,
    EventHour =src.Date.Hour,
    EventMinute =src.Date.Minute,
};

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