首页 > 解决方案 > 使用 Automapper 或 Dapper 在 JSON 中映射一个属性?在哪一层?

问题描述

TL;DR:
哪种反序列化JSON属性的方法是最纯粹的


长话短说:
考虑在 DB 中进行以下设计:

ID 姓名 类型 JSON属性
1 “福” “红色的” {"Attr11":"Val11", "Attr12" : "Val12" }
2 “酒吧” “绿色的” {"Attr21":"Val21", "Attr22" : "Val22" }
3 “巴兹” “蓝色的” {"Attr31":"Val31", "Attr32" : "Val32" }

每种类型都有自己的属性,保存在 JSON 字符串的最后一列中。顺便说一句,过程不返回该类型。
我需要将此数据传输到控制器。我有洋葱(我相信)解决方案结构:
MyProject。Web(带控制器)
MyProject。核心(带有服务、DTO、实体、接口)
MyProject。数据(带有存储库,使用 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 从数据库中获取数据时,例如:

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 保留在平面字符串中),并将其映射到服务层 (MyProject.Core) 上的 DTO,或者通过简单的分配(选项 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 ...
            }
        }
    }
}

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

标签: c#jsonasp.net-corearchitectureclean-architecture

解决方案


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

  • 您正在使用关系数据库,但也使用它来存储 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一点,您可能同意我的观点,即它不容易理解和维护。


推荐阅读