I have the following class which i am trying to hydrate to:
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;
}
}
Which uses the below classes that implement concepts from my DDD entity domain model (non-relevant code removed to keep code short, set as readonly to make immutable once constructed):
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;
}
}
I try and run the parameterised query which will hydrate in to a Product object:
using (connection)
{
connection.Open();
return connection.QueryFirst<Product>(ReadQuery, new { VendorId = sku.VendorId.VendorShortname, SkuValue = sku.SkuValue });
}
It throws the following exception as it doesn't know how to deal with the Sku type in the constructor:
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'
I looked into using a custom SqlMapper.TypeHandler<Product> but the Parse(object value) only ever passes in a single parsed value from the VendorId database column (if it passed in an array of values here I could do the mapping myself).
Is there a way to customize the handling of the object so that I can pass all the parameters in to the constructor like the below:
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"));
}
I guess I could create a specific constructor with Product(string VendorShortname, string SkuValue, string Name, string Description, UInt64 IsArchived) but I would rather (must) have this concern in the mapping code rather than in my domain model.
Going over some pseudocode, what I could do is roll my own ORM, but would want to instead do similar via Dapper.
- Get all constructors for object by reflection
- If any parameters in constructor is a type, get its constructors
- For each constructor (including parameters), map the constructor name to the SQL reader column (and type)
This would equate to VendorShortname used for VendorId(string vendorShortname), and Name, Description, isArchived used for public Product(Sku sku, string name, string description, bool isArchived)... something is similarly done by MongoDB as per my answer posted at the following link, a Dapper manual mapping equivalent would be awesome MongoDB Composite Key: InvalidOperationException: {document}.Identity is not supported