Dapper: иерархия сопоставления и одно другое свойство

Мне очень нравится простота и возможности Dapper. Я хотел бы использовать Dapper для решения общих задач, с которыми я сталкиваюсь на повседневной основе. Они описаны ниже.

Вот моя простая модель.

public class OrderItem {
    public long Id { get; set; }
    public Item Item { get; set; }
    public Vendor Vendor { get; set; }
    public Money PurchasePrice { get; set; }
    public Money SellingPrice { get; set; }
}

public class Item
{
    public long Id { get; set; }
    public string Title { get; set; }
    public Category Category { get; set; }
}

public class Category
{
    public long Id { get; set; }
    public string Title { get; set; }
    public long? CategoryId { get; set; }
}

public class Vendor
{
    public long Id { get; set; }
    public string Title { get; set; }
    public Money Balance { get; set; }
    public string SyncValue { get; set; }
}

public struct Money
{
    public string Currency { get; set; }
    public double Amount { get; set; }
}

Две проблемы превзошли меня.

Вопрос 1: Должен ли я всегда создавать DTO с логикой отображения между DTO-Entity в случаях, когда у меня есть одна разность свойств или простое перечисление enum/struct?

Например: существует объект Vendor, который имеет свойство Balance как struct (иначе это может быть Enum). Я не нашел ничего лучше, чем это решение:

public async Task<Vendor> Load(long id) {
    const string query = @"
        select * from [dbo].[Vendor] where [Id] = @id
    ";

    var row = (await this._db.QueryAsync<LoadVendorRow>(query, new {id})).FirstOrDefault();
    if (row == null) {
        return null;
    }

    return row.Map();
}

В этом методе у меня есть 2 служебных кода: 1. Я должен создать LoadVendorRow как объект DTO; 2. Мне нужно написать собственное сопоставление между LoadVendorRow и Vendor:

public static class VendorMapper {
    public static Vendor Map(this LoadVendorRow row) {
        return new Vendor {
            Id = row.Id,
            Title = row.Title,
            Balance = new Money() {Amount = row.Balance, Currency = "RUR"},
            SyncValue = row.SyncValue
        };
    }
}

Возможно, вы могли бы предположить, что мне нужно хранить сумму и валюту вместе и получать ее как _db.QueryAsync<Vendor, Money, Vendor>(...). Возможно, вы правы. В этом случае, что мне делать, если мне нужно сохранить/вернуть свойство Enum (свойство OrderStatus)?

var order = new Order
{
    Id = row.Id,
    ExternalOrderId = row.ExternalOrderId,
    CustomerFullName = row.CustomerFullName,
    CustomerAddress = row.CustomerAddress,
    CustomerPhone = row.CustomerPhone,
    Note = row.Note,
    CreatedAtUtc = row.CreatedAtUtc,
    DeliveryPrice = row.DeliveryPrice.ToMoney(),
    OrderStatus = EnumExtensions.ParseEnum<OrderStatus>(row.OrderStatus)
};

Могу ли я сделать эту работу без моих собственных реализаций и сэкономить время?

Вопрос 2: Что делать, если я хочу восстановить данные для объектов, которые немного сложнее, чем простой однопроцессорный DTO? OrderItem - прекрасный пример. Это метод, который я использую, чтобы восстановить его прямо сейчас:

public async Task<IList<OrderItem>> Load(long orderId) {
    const string query = @"
            select [oi].*,
                   [i].*,
                   [v].*,
                   [c].*
              from [dbo].[OrderItem] [oi]
              join [dbo].[Item] [i]
                on [oi].[ItemId] = [i].[Id]
              join [dbo].[Category] [c]
                on [i].[CategoryId] = [c].[Id]
              join [dbo].[Vendor] [v]
                on [oi].[VendorId] = [v].[Id]
             where [oi].[OrderId] = @orderId
    ";

    var rows = (await this._db.QueryAsync<LoadOrderItemRow, LoadItemRow, LoadVendorRow, LoadCategoryRow, OrderItem>(query, this.Map, new { orderId }));

    return rows.ToList();
}

Как вы можете видеть, моя проблема вопрос 1 заставляет меня писать настраиваемые mappers и DTO для каждого объекта в иерархии. Что мой картограф:

private OrderItem Map(LoadOrderItemRow row, LoadItemRow item, LoadVendorRow vendor, LoadCategoryRow category) {
    return new OrderItem {
        Id = row.Id,
        Item = item.Map(category),
        Vendor = vendor.Map(),
        PurchasePrice = row.PurchasePrice.ToMoney(),
        SellingPrice = row.SellingPrice.ToMoney()
    };
}

Есть много картографов, которые я бы хотел устранить, чтобы предотвратить ненужную работу.

Ответы

Ответ 1

Есть ли чистый способ получить и отобразить заказ объект с такими относительными свойствами, как Vendor, Item, Category и т.д.)

Вы не показываете свой объект Order, но я возьму ваш OrderItem в качестве примера и покажу вам, что вам не нужен инструмент сопоставления для конкретной проблемы (как указано). Вы можете получить OrderItem вместе с информацией Item и Vendor каждой из них, выполнив следующие действия:

var sql = @"
select oi.*, i.*, v.* 
from OrderItem 
    inner join Item i on i.Id = oi.ItemId
    left join Vendor v on v.Id = oi.VendorId
    left join Category c on c.Id = i.CategoryId";
var items = connection.Query<OrderItem, Item, Vendor, Category, OrderItem>(sql, 
    (oi,i,v,c)=>
    {
      oi.Item=i;oi.Item.Category=c;oi.Vendor=v;
      oi.Vendor.Balance = new Money { Amount = v.Amount, Currency = v.Currency};
      return oi; 
    });

ПРИМЕЧАНИЕ. Использование left join и его корректировка в зависимости от структуры вашей таблицы.

Ответ 2

Я не уверен, что я понимаю ваш вопрос на 100%. И тот факт, что никто еще не пытался ответить на него, заставляет меня поверить, что я не одинок, когда говорю, что это может быть немного запутанным.

Вы отмечаете, что вам нравится функциональность Dapper, но я не вижу, чтобы вы использовали ее в своих примерах. Разве вы хотите разработать альтернативу Dapper? Или вы не знаете, как использовать Dapper в вашем коде?

В любом случае, здесь ссылка на базу кода Dapper для вашего обзора: https://github.com/StackExchange/dapper-dot-net

Надеясь, что вы сможете уточнить свои вопросы, я с нетерпением жду вашего ответа.