Dapper - 通过构造函数处理具有只读字段的 ddd 实体的自定义映射

Dapper - Handling custom mapping for ddd entity with read-only fields via constructor

我有以下 class 我正在尝试补充水分:

public class Product
{
    public readonly Sku Sku;
    public string Name { get; private set; }
    public string Description { get; private set; }
    public bool IsArchived { get; private set; }

    public Product(Sku sku, string name, string description, bool isArchived)
    {
        Sku = sku;
        Name = name;
        Description = description;
        IsArchived = isArchived;
    }
}

它使用以下 classes 来实现我的 DDD 实体域模型中的概念(删除了不相关的代码以保持代码简短,设置为只读以使构造后不可变):

public class Sku
{
    public readonly VendorId VendorId;
    public readonly string SkuValue;

    public Sku(VendorId vendorId, string skuValue)
    {
        VendorId = vendorId;
        SkuValue = skuValue;
    }
}

public class VendorId
{
    public readonly string VendorShortname;

    public VendorId(string vendorShortname)
    {
        VendorShortname = vendorShortname;
    }
}

我尝试 运行 参数化查询,它将合并到产品对象中:

using (connection)
{
    connection.Open();
    return connection.QueryFirst<Product>(ReadQuery, new { VendorId = sku.VendorId.VendorShortname, SkuValue = sku.SkuValue });
}

它抛出以下异常,因为它不知道如何处理构造函数中的 Sku 类型:

System.InvalidOperationException: 'A parameterless default constructor or one matching signature (System.String VendorId, System.String SkuValue, System.String Name, System.String Description, System.UInt64 IsArchived) is required for Domain.Model.Products.Product materialization'

我研究过使用自定义 SqlMapper.TypeHandler<Product>,但 Parse(object value) 只从 VendorId 数据库列中传入单个解析值(如果它在此处传入值数组我可以自己做映射)。

有没有办法自定义对象的处理,以便我可以将所有参数传递给构造函数,如下所示:

using (connection)
{
    var command = connection.CreateCommand();
    command.CommandText = "SELECT VendorShortname, SkuValue, Name, Description, IsArchived FROM Products WHERE VendorShortname=@VendorShortname AND SkuValue=@SkuValue";
    command.Parameters.AddWithValue("@VendorShortname", sku.VendorId.VendorShortname);
    command.Parameters.AddWithValue("@SkuValue", sku.SkuValue);
    connection.Open();

    var reader = command.ExecuteReader();
    if (reader.HasRows==false)
        return null;

    reader.Read();

    return new Product(
        new Sku(new VendorId(reader.GetString("VendorId")),reader.GetString("SkuValue")),
        reader.GetString("Name"),
        reader.GetString("Description"),
        reader.GetBoolean("IsArchived"));
}

我想我可以用 Product(string VendorShortname, string SkuValue, string Name, string Description, UInt64 IsArchived) 创建一个特定的构造函数,但我宁愿(必须)在映射代码中而不是在我的域模型中考虑这个问题。

通过一些伪代码,我可以做的是推出我自己的 ORM,但我想通过 Dapper 做类似的事情。

  1. 通过反射获取对象的所有构造函数
  2. 如果构造函数中的任何参数是类型,则获取其构造函数
  3. 对于每个构造函数(包括参数),将构造函数名称映射到 SQL reader 列(和类型)

这相当于 VendorShortname 用于 VendorId(string vendorShortname)NameDescriptionisArchived 用于 public Product(Sku sku, string name, string description, bool isArchived)...根据我在下面 link 上发布的回答,MongoDB 也做了类似的事情,一个 Dapper 手动映射等效项会很棒

Execute a query and map it to a list of dynamic objects

public static IEnumerable<dynamic> Query (
    this IDbConnection cnn, 
    string sql, 
    object param = null, 
    SqlTransaction transaction = null, 
    bool buffered = true
)

然后您将使用动态对象列表构建所需的模型。

因此使用原始 post 中的示例,参数化查询将从...

using (connection)
{
    var command = connection.CreateCommand();
    command.CommandText = "SELECT VendorShortname, SkuValue, Name, Description, IsArchived FROM Products WHERE VendorShortname=@VendorShortname AND SkuValue=@SkuValue";
    command.Parameters.AddWithValue("@VendorShortname", sku.VendorId.VendorShortname);
    command.Parameters.AddWithValue("@SkuValue", sku.SkuValue);
    connection.Open();

    var reader = command.ExecuteReader();
    if (reader.HasRows==false)
        return null;

    reader.Read();

    return new Product(
        new Sku(new VendorId(reader.GetString("VendorId")),reader.GetString("SkuValue")),
        reader.GetString("Name"),
        reader.GetString("Description"),
        reader.GetBoolean("IsArchived"));
}

到...

var ReadQuery = "SELECT VendorShortname, SkuValue, Name, Description, IsArchived FROM Products WHERE VendorShortname=@VendorShortname AND SkuValue=@SkuValue";
using (connection) {
    connection.Open();
    return connection.Query(ReadQuery, new { VendorShortname = sku.VendorId.VendorShortname, SkuValue = sku.SkuValue })
            .Select(row => new Product(
                new Sku(new VendorId(row.VendorShortname), row.SkuValue),
                row.Name,
                row.Description,
                row.IsArchived)
            );
}

这是框架的预期目的。只需确保使用的属性直接映射到查询返回的字段即可。

这可能看起来很复杂,但考虑到目标对象的构造函数的复杂性,这是一个可行的解决方案。

您还可以考虑将 "domain" 模型与持久性分离并在其他地方构建它们的选项。例如:

  • 为每个数据库记录创建 class:ProductRecord
  • 创建工厂:ProductFactory
  • 获取数据:var productRecords = connection.Query<ProductRecord>("select * from products").AsList();
  • 构建产品:factory.Build(productRecords)

一些优点:关注点分离、灵活性、适合大型项目

一些缺点:更多的代码,对小项目来说有点过头了