Search code examples
c#dappersqlconnectiondapper-extensions

Exception with message 'No columns were mapped' using Dapper Extensions


I have a class which implements some interface:

public interface IDb {}
public class DbModel : IDb {}

After this I use Dapper Extensions to insert the object into the DB. This code works well:

var obj = new DbModel();
sqlConnection.Insert(obj); 

But when I try insert an instance of this class, casting on the corresponding interface, it gives an Exception:

IDb obj = new DbModel();
sqlConnection.Insert(obj); // exception here

System.ArgumentException: 'No columns were mapped.'


Solution

  • Reason why it not work:

    Proceeding from the fact that, DapperExtensions.Insert method is generic, DapperExstensions can't find corresponding map for IDb

    Solution:

    Create mapper class for IDb and register DbModel properties

    public sealed class IDbMapper : ClassMapper<IDb>
    {
        public IDbMapper()
        {
            base.Table("TableName");
            Map(m => new DbModel().Title);
            // and such mapping for other properties
        }
    }
    

    After this it will be work:

    IDb obj = new DbModel();
    sqlConnection.Insert(obj);
    

    But here is a problem, when we have many implementations of IDb interface, in IDbMapper config we can't map columns to corresponding tables here:

    base.Table("TableName");
    Map(m => new DbModel().Title);
    

    Because we don't know type of object instance when we do mapping.

    Edit:

    After some debuging I notice that, in every Insert method call, dapper do mapping, and construct corresponding ClassMapper<> class. We can use this weirdness.

    For this we should create SharedState and store in it instance type before calling Insert method.

    public static class DapperExstensionsExstensions
    {
        public static Type SharedState_ModelInstanceType { get; set; }
        ...
    }
    
    IDb obj = new DbModel();
    DapperExstensionsExstensions.SharedState_ModelInstanceType = obj.GetType();
    sqlConnection.Insert(obj);
    

    After this we can access this property when Dapper will do mapping

    public sealed class IDbMapper : ClassMapper<IDb>
    {
        public IDbMapper()
        {
             // here we can retrieve Type of model instance and do mapping using reflection
             DapperExstensionsExstensions.SharedState_ModelInstanceType
        }
    }
    

    Whole code snippet:

    public interface IDb { }
    
    [MapConfig("TableName", "Schema")]
    public class DbTemp : IDb
    {
        public string Title { get; set; }
    }
    
    public class MapConfigAttribute : Attribute
    {
        public MapConfigAttribute(string name, string schema)
        {
            Name = name;
            Schema = schema;
        }
        public string Name { get; }
        public string Schema { get; }
    }
    
    public sealed class DbMapper : ClassMapper<IDb>
    {
        public DbMapper()
        {
            DapperExstensionsExstensions.CorrespondingTypeMapper<IDb>((tableName, sechemaName, exprs) =>
            {
                Table(tableName);
                Schema(SchemaName);
                return exprs.Select(Map);
            });
        }
    }
    
    public static class DapperExstensionsExstensions
    {
        private static readonly object _LOCK = new object();
        public static Type SharedState_ModelInstanceType { get; set; }
        public static List<PropertyMap> CorrespondingTypeMapper<T>(Func<string, string, IEnumerable<Expression<Func<T, object>>>, IEnumerable<PropertyMap>> callback)
        {
            var tableNameAttribute = (MapConfigAttribute)SharedState_ModelInstanceType.GetCustomAttribute(typeof(MapConfigAttribute));
            var tableName = tableNameAttribute.Name;
            var schemaName = tableNameAttribute.Schema;
            var result = callback(tableName, schemaName, new GetPropertyExpressions<T>(SharedState_ModelInstanceType));
            Monitor.Exit(_LOCK);
            return result.ToList();
        }
        public static object Insert<TInput>(this IDbConnection connection, TInput entity) where TInput : class
        {
            Monitor.Enter(_LOCK);
            SharedState_ModelInstanceType = entity.GetType();
            return DapperExtensions.DapperExtensions.Insert(connection, entity);
        }
    }
    
    public class GetPropertyExpressions<TInput> : IEnumerable<Expression<Func<TInput, object>>>
    {
        private readonly Type _instanceType;
    
        public GetPropertyExpressions(Type instanceType)
        {
            _instanceType = instanceType;
        }
        public IEnumerator<Expression<Func<TInput, object>>> GetEnumerator()
        {
            return _instanceType
                .GetProperties()
                .Select(p => new GetPropertyExpression<TInput>(_instanceType, p.Name).Content()).GetEnumerator();
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
    
    public class GetPropertyExpression<TInput> : IContent<Expression<Func<TInput, object>>>
    {
        private readonly Type _instanceType;
        private readonly string _propertyName;
        public GetPropertyExpression(Type instanceType, string propertyName)
        {
            _instanceType = instanceType;
            _propertyName = propertyName;
        }
        public Expression<Func<TInput, object>> Content()
        {
            // Expression<Func<IDb, object>> :: model => (object)(new DbModel().Title)
    
            var newInstance = Expression.New(_instanceType);
            var parameter = Expression.Parameter(typeof(TInput), "model");
    
            var getPropertyExpression = Expression.Property(newInstance, _propertyName);
            var convertedProperty = Expression.Convert(getPropertyExpression, typeof(object));
            var lambdaExpression = Expression.Lambda(convertedProperty, parameter);
    
            return (Expression<Func<TInput, object>>)lambdaExpression;
        }
    }
    

    It works for me

    IDb obj = new DbModel();
    sqlConnection.Insert(obj);