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