Dapper: manejo del mapeo personalizado para la entidad ddd con campos de solo lectura a través del constructor

c# dapper orm

Pregunta

Tengo la siguiente clase a la que estoy tratando de hidratar:

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;
    }
}

Que utiliza las siguientes clases que implementan conceptos de mi modelo de dominio de entidad DDD (código no relevante eliminado para mantener corto el código, configurado como readonly para hacer inmutables una vez construido):

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;
    }
}

Intento ejecutar la consulta parametrizada que se hidratará en un objeto Producto:

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

Lanza la siguiente excepción ya que no sabe cómo tratar con el tipo de Sku en el constructor:

System.InvalidOperationException: 'Se requiere un constructor predeterminado sin parámetros o una firma coincidente (System.String VendorId, System.String SkuValue, System.String Name, System.String Description, System.UInt64 IsArchived) para Domain.Model.Products.Product materialization '

SqlMapper.TypeHandler<Product> usar un SqlMapper.TypeHandler<Product> personalizado SqlMapper.TypeHandler<Product> pero el Parse(object value) solo pasa en un solo valor analizado desde la columna de la base de datos VendorId (si pasaba en una matriz de valores, podría hacer el mapeo yo mismo).

¿Hay alguna manera de personalizar el manejo del objeto para que pueda pasar todos los parámetros al constructor como el siguiente:

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"));
}

Creo que podría crear un constructor específico con Product(string VendorShortname, string SkuValue, string Name, string Description, UInt64 IsArchived) pero preferiría (debo) tener esta preocupación en el código de mapeo en lugar de en mi modelo de dominio.

Repasando algún pseudocódigo, lo que podría hacer es rodar mi propio ORM, pero me gustaría hacer algo similar a través de Dapper.

  1. Obtener todos los constructores para el objeto por reflexión
  2. Si algún parámetro en el constructor es un tipo, obtenga sus constructores
  3. Para cada constructor (incluidos los parámetros), asigne el nombre del constructor a la columna del lector de SQL (y escriba)

Esto equivaldría a VendorShortname usado para VendorId(string vendorShortname) , y Name , Description , isArchived utilizado para isArchived Product(Sku sku, string name, string description, bool isArchived) ... MongoDB hace algo similar según mi respuesta publicada en el siguiente enlace, un equivalente de asignación manual de Dapper sería increíble. Clave compuesta de MongoDB: InvalidOperationException: {document} .Identity no es compatible.

Respuesta aceptada

Ejecute una consulta y asóciela a una lista de objetos dinámicos

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

A continuación, construiría el modelo deseado utilizando la lista de objetos dinámicos.

Entonces, usando el ejemplo de la publicación original, la consulta parametrizada se cambiaría de ...

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"));
}

A...

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)
            );
}

¿Cuál es el propósito del marco? Solo asegúrese de que las propiedades utilizadas se correlacionen directamente con los campos devueltos por la consulta.

Esto puede parecer intensivo, pero esta es una solución viable dada la naturaleza compleja del constructor del objeto de destino.


Respuesta popular

También puede considerar una opción para desacoplar los modelos de "dominio" de la persistencia y construirlos en otro lugar. Por ejemplo:

  • Crear una clase por registro de base de datos: ProductRecord
  • Crear una fábrica: ProductFactory
  • Obtenga los datos: var productRecords = connection.Query<ProductRecord>("select * from products").AsList();
  • Construya el producto: factory.Build(productRecords)

Algunos pros: la separación de la preocupación, la flexibilidad, se adapta a proyectos más grandes

Algunas desventajas: Más código, una sobre matar para pequeños proyectos



Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué