使用 Automapper 或 Dapper 在 JSON 中映射一个 属性?在哪一层?

Mapping one property in JSON using Automapper or Dapper? On which layer?

长话短说:
哪种反序列化方法 one JSON 属性 最纯粹的?


长话短说:
考虑数据库中的以下设计:

Id Name Type JSONAttributes
1 "Foo" "Red" {"Attr11":"Val11", "Attr12" : "Val12" }
2 "Bar" "Green" {"Attr21":"Val21", "Attr22" : "Val22" }
3 "Baz" "Blue" {"Attr31":"Val31", "Attr32" : "Val32" }

每种类型都有自己的属性,保存在 JSON 字符串的最后一列中。顺便说一句,程序不返回该类型。
我需要将此数据传输到控制器。我有洋葱(我相信)解决方案结构:
我的项目。网络(带控制器)
我的项目。核心(包含服务、DTO、实体、接口)
我的项目。数据(带存储库,使用 Dapper)
...和其他人,对本主题不重要。

我还创建了将这些行映射到的模型。一个抽象的,通用的,几个派生的:

namespace MyProject.Core.DTO.Details  
{
    public abstract class DtoItem  
    {
        public int Id { get; set; }  
        public string Name { get; set; }  
    }
}
namespace MyProject.Core.DTO.Details
{
    public class DtoTypeRed : DtoItem
    {
        public DtoTypeRedAttributes AttributesJson { get; set; }
    }
}
namespace MyProject.Core.DTO.Details
{
    public class DtoTypeGreen : DtoItem
    {
        public DtoTypeGreenAttributes AttributesJson { get; set; }
    }
}
namespace MyProject.Core.DTO.Details
{
    public class DtoTypeRedAttributes 
    {
        [JsonPropertyName("Attr11")]
        public string AttributeOneOne{ get; set; }
        [JsonPropertyName("Attr12")]
        public string AttributeOneTwo{ get; set; }
    }
}

也创建了实体,但仅在选项 2 中使用(稍后描述):

namespace MyProject.Core.Entities
{
    public class Item
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string AttributesJson { get; set; }
    }
}

我的问题是,什么是更好的方法:
选项 1 - 直接映射到 DTO,在 ItemRepository 中,在 MyProject.Data 中,当使用 Dapper multimap 从 DB 中获取数据时,如:

namespace MyProject.Data
{
    public class DetailsRepository : IDetailsRepository
    {
        public async Task<DtoItem> GetDetails(int itemId, int itemTypeId)
        {
            using (var connection = _dataAccess.GetConnection())
            {
                switch (itemTypeId)
                {
                    case 1:
                        return (await connection.QueryAsync<DtoTypeRed,string,DtoTypeRed>("[spGetDetails]", (redDetails, redDetailsAttributesJson) =>
                            {
                                redDetails.AttributesJson = JsonSerializer.Deserialize<List<DtoTypeRed>>(redDetailsAttributesJson).FirstOrDefault();
                                return redDetails;
                            },
                            splitOn: "AttributesJson",
                            param: new { itemId ,itemTypeId},
                            commandType: CommandType.StoredProcedure)).FirstOrDefault();
                    case 2:
                        return (await connection.QueryAsync<DtoTypeGreen,string,DtoTypeGreen>("[spGetDetails]", (greenDetails, greenDetailsAttributesJson) =>
                            {
                                greenDetails.AttributesJson = JsonSerializer.Deserialize<List<DtoTypeGreen>>(greenDetailsAttributesJson).FirstOrDefault();
                                return greenDetails;
                            },
                            splitOn: "AttributesJson",
                            param: new { itemId ,itemTypeId},
                            commandType: CommandType.StoredProcedure)).FirstOrDefault();
            
                    case ...
                    default: return null;
                }
            }
        }
    }
}

我的同事建议在存储库中执行这种业务逻辑不是一个好方法。 因此,有一种替代方法(至少我知道一种)——将数据提取到项目实体中(将 JSON 保留为扁平字符串),并将其映射到服务层上的 DTO(MyProject.Core) 要么使用简单的分配(选项 2.1),我发现这不是很优雅的方式,要么使用 Automapper(选项 2.2)

namespace MyProject.Data
{
    public class DetailsRepository : IDetailsRepository
    {
        public async Task<Item> GetDetails(int itemId, int itemTypeId)
        {
            using (var connection = _dataAccess.GetConnection())
            {
                return await connection.QuerySingleAsync<Item>("[spGetDetails]", param: new { itemId ,itemTypeId}, commandType: CommandType.StoredProcedure);
            }
        }
    }
}

选项 2.1:

namespace MyProject.Core.Services
{
    public class DetailsService : IDetailsService
    {
        public async Task<DtoItem> GetDetails(int itemId, int itemTypeId)
        {
            var itemDetailsEntity = await _detailsRepo.GetDetails(int itemId, int itemTypeId);
            switch(itemTypeId){

                case 1:
                    var result = new DtoTypeRed();
                    result.Id= itemDetailsEntity.Id;
                    result.Name = itemDetailsEntity.Name;
                    result.AttributesJson = JsonSerializer.Deserialize<List<DtoTypeRedAttributes>>(itemDetailsEntity.AttributesJson).FirstOrDefault();
                case ...
            }
            return result;
        }
    }
}

如果是这样,也许有些工厂会更好? (我还没有)

带有 Automapper 的选项 2.2:
问题是,我在某处读到,Automapper 并不是真的要包含 JSON 反序列化逻辑 - 它应该更“自动”

namespace MyProject.Core.Mappings
{
    public class MapperProfiles : Profile
    {
        public MapperProfiles()
        {
            CreateMap<Entities.Item, DTO.Details.DtoTypeRed>()
                .ForMember(dest => dest.AttributesJson, opts => opts.MapFrom(src =>JsonSerializer.Deserialize<List<DtoTypeRedAttributes>>(src.AttributesJson, null).FirstOrDefault()));
            (...)
        }
    }
}
namespace MyProject.Core.Services
{
    public class DetailsService : IDetailsService
    {
        public async Task<DtoItem> GetDetails(int itemId, int itemTypeId)
        {
            var itemDetailsEntity = await _detailsRepo.GetDetails(int itemId, int itemTypeId);
            switch(itemTypeId){
                case 1:
                    return _mapper.Map<DtoTypeRed>(itemDetailsEntity);
                case 2: 
                    return _mapper.Map<DtoTypeGreen>(itemDetailsEntity);
                case ...
            }
        }
    }
}

我真的缺乏这种“架构”知识和经验,所以任何建议都会非常感激。也许我在这里看不到其他方式?

考虑到您的软件设计很不寻常,我认为您首先应该问自己两个问题:

  • 您正在使用关系数据库,但也在使用它来存储 JSON(如在 NoSQL 数据库中)。这是一个预先存在的架构,您对更改它没有任何影响吗?如果不是,为什么要使用这种设计而不是像通常在关系模式中那样为不同的数据结构分离表?这也将为您提供关系数据库的优势,例如查询、外键、索引。
  • 如果你有一个控制器,无论如何你都可以分发你的对象,你可能会这样做 JSON,对吧?那么反序列化它有什么意义呢?不能让 JSON 保持原样吗?

除此之外,如果您想坚持您的选择,我会选择类似于您的选项 1 的解决方案。您的同事是对的,您不想在存储库中拥有业务逻辑,因为存储库的职责只是存储和查询数据。洋葱架构中的业务逻辑属于您的服务层。但是,如果您只是将数据库中的数据反序列化为程序中可用的结构,那么此逻辑应该在存储库中。在这种情况下,您所做的就是获取数据并将其转换为对象,这与 ORM 工具所做的相同。

不过,我会在选项 1 中更改的一件事是将 switch-case 移动到您的服务层,因为这是业务逻辑。所以:

public class DetailsService : IDetailService {
    public async Task<Item> GetDetails(int itemId, int itemTypeId) {
       Item myItem;
       switch(itemTypeId){
          case 1:
             myItem = _repo.getRedItem(itemId);
          // and so on
       }
    }
}

然后您的存储库将具有用于各个项目类型的方法。同样,它所做的只是查询和反序列化数据。

关于你的其他两个选项:DTO 的要点通常是有单独的对象,你可以与其他进程共享这些对象(比如在控制器中发送它们时)。这样,DTO 中的更改就不会影响实体中的更改,反之亦然,而且您可以选择要包含在 DTO 或实体中的属性,以及不包含的属性。在你的情况下,你似乎只是在你的程序中真正使用了 DTO,而实体只是作为一个中间步骤存在,这使得拥有任何实体都毫无意义。

此外,在您的场景中使用 AutoMapper 只会使代码更加复杂,您可以在 MapperProfiles 中的代码中看到这一点,您可能同意我的看法,即它不容易理解和维护。

Jakob 的回答很好,我很同意他说的很多。再加上...

您面临的挑战是您希望尽可能纯粹地使用清洁架构,但您受到存在两种相互冲突的架构模式的系统的限制:

  • 数据结构明确的“传统”方法。
  • 混合数据结构的替代方法:一些数据结构由数据库定义(table 设计),而其他数据结构在代码中构建(JSON)——其中数据库具有没有可见性(对数据库来说它只是一个字符串)。

更复杂的是在代码级别,您有一个带有明确定义的对象和层的经典分层架构,然后您有这个突变 JSON 东西。

根据到目前为止的信息,我想知道您是否可以为 JSON 属性使用通用列表 - 因为您的代码采用 JSON 并将其转换为本质上的键值对 -至少你的代码是这样建议的:

MyProject.Core.DTO.Details.DtoTypeRedAttributes.AttributeOneOne{ get; set; }

这告诉我控制器正在获取 AttributeOneOne,其中将包含 name/key 和值。无论消耗什么,它都会关心 name/key,但对于您的其余代码,您只有通用属性。

我也是数据库中type字段的suspicious/curious,你说的是'never returned by the procedure',但是你有特定类型的代码?例如DtoTypeRedAttributes.

基于所有这些,我会简化事情(这仍然让你保持清洁):

将 JSON 反序列化为通用 key/value 对:

  • 这可能发生在数据访问层或 DTO 本身。
  • 直接从 JSON 到 Key/Value 对转换纯粹是技术性的,不会影响任何业务规则,因此没有业务 rule/logic 驱动因素影响可能发生的地方。

问:反序列化的代价是多少?那些属性 总是 被访问了吗?如果它是费用,并且并不总是使用属性,那么您可以使用延迟加载仅在需要时触发反序列化 - 这建议在 DTO 中进行,因为如果您在数据访问层中进行操作,您将无论如何,每次都会受到反序列化惩罚。

如果您关心的话,一个潜在的缺点是您将拥有一个带有 private string RawJSONAttributes 属性 和 public List<string,string> JSONAttributes key/value 对的 DTO 属性。 IE。更大的内存占用,你必须考虑一致性(如果有人更改 public 值等,你是否关心私有字符串)。

DTO 整合

如果 DTO 仅提供 key/value 对的通用列表就可以逃脱惩罚,那么根据您目前所说的其他内容,我不明白您为什么有单独的红色、绿色、蓝色代码。一个 DTO 就可以做到。

如果您确实需要单独的类型,那么是的,某种类型的工厂可能是一个不错的方法。

  • 数据访问代码可以return“通用”DTO(如我的图表中的 WidgetInfo)直至消费逻辑。
  • 该逻辑(进行 DTO 转换)可能会屏蔽上面的消费者/控制器,以便他们只看到 BlueWidget、RedWidget(等)DTO 而不是通用的。

总结

该图显示了我通常如何执行此操作(使用未显示的控制器),假设通用 DTO 没问题。让我知道是否需要绘制出另一种方法(将通用 WidgetInfo 转换为 BlueWidget/RedWidget DTO)。

请注意,DTO 具有 public 属性 属性(key/value 对列表);如果您想在 DTO 中采用延迟加载方法,它还会有一个用于 RawJSONAttributes 的私有字符串。