6

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.

  1. Get all constructors for object by reflection
  2. If any parameters in constructor is a type, get its constructors
  3. 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

Nkosi
  • 235,767
  • 35
  • 427
  • 472
morleyc
  • 2,169
  • 10
  • 48
  • 108
  • `but i would rather (must) have this concern in the mapping code rather than in my domain model.` Can you talk us through why you **must** have it this way? – mjwills Aug 12 '17 at 23:20
  • I do not have any persistence concerns in my domain model for other repositories (MongoDB) where possible i would like to use SQL/Dapper as it keeps things simpler. Will update my post as to what would need to be done, could roll my own but don't want to reinvent the wheel nor could i write code as good as Dapper's – morleyc Aug 13 '17 at 07:59

2 Answers2

5

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
)

You would then construct the desired model using the list of dynamic objects.

So using the example from the original post, the parameterised query would be changed from...

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

To...

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

Which is the intended purpose of the framework. Just make sure that the properties used directly map to the fields returned by the query.

This may seem intensive but this is a viable solution given the complex nature of the target object's constructor.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
1

You can also consider an option of decoupling your "domain" models from the persistence and construct them else-where. For example:

  • Create a class per database record: ProductRecord
  • Create a factory: ProductFactory
  • Get the data: var productRecords = connection.Query<ProductRecord>("select * from products").AsList();
  • Build the product: factory.Build(productRecords)

Some pros: Separation of concern, flexibility, suits larger projects

Some cons: More code, an over-kill for small projects

Void Ray
  • 9,849
  • 4
  • 33
  • 53
  • Thanks, yes have done similar with a POCO record and using automapper, but can get painful with double classes. Certainly one valid option to point out. – morleyc Aug 15 '17 at 16:48