henkmollema / Dommel

CRUD operations with Dapper made simple.
MIT License
611 stars 99 forks source link

Issues resolving table names on derived types #284

Closed bezinit closed 1 year ago

bezinit commented 2 years ago

There is an issue when using Expressions on base properties in derived types. For example:

public abstract class BaseClass
{       [Key]
        public int Id { get; set; }
}

[Table("People.Person"]
public class Person : BaseClass
{  public` string Name { get;set;} 
}

var result = await connection.SelectAsync<Person>(p => p.Name == model.Name && c.Id != model.Id);

Resolves to:

SELECT * FROM People.Person WHERE People.Person.Name = 'Bob' AND BaseClass.Id <> 1)

The column resolved in Resolvers is using PropertyInfo to derive the table name - this uses the ReflectedType which is the base type not the derived one.

My quick and dirty solution was to add the following to Resolvers.cs:

    /// <summary>
    /// Gets the name of the column in the database for the specified type,
    /// using the configured <see cref="IColumnNameResolver"/>.
    /// </summary>
    /// <param name="expression">The <see cref="MemberExpression"/> to get the column name for.</param>
    /// <param name="sqlBuilder">The SQL builder instance.</param>
    /// <returns>The column name in the database for <paramref name="expression"/>.</returns>
    public static string Column(MemberExpression expression, ISqlBuilder sqlBuilder)
        => Column(expression, sqlBuilder, DommelMapper.IncludeTableNameInColumnName);

    /// <summary>
    /// Gets the name of the column in the database for the specified type,
    /// using the configured <see cref="IColumnNameResolver"/>.
    /// </summary>
    /// <param name="expression">The <see cref="MemberExpression"/> to get the column name for.</param>
    /// <param name="sqlBuilder">The SQL builder instance.</param>
    /// <param name="includeTableName">Whether to include table name with the column name for unambiguity. E.g. <c>[Products].[Name]</c>.</param>
    /// <returns>The column name in the database for <paramref name="expression"/>.</returns>
    public static string Column(MemberExpression expression, ISqlBuilder sqlBuilder, bool includeTableName = true)
    {
        var propertyInfo = (PropertyInfo) expression.Member;

        var key = $"{sqlBuilder.GetType()}.{expression.Expression.Type}.{propertyInfo.Name}.{includeTableName}";
        if (!ColumnNameCache.TryGetValue(key, out var columnName))
        {
            columnName = sqlBuilder.QuoteIdentifier(DommelMapper.ColumnNameResolver.ResolveColumnName(propertyInfo));
            if (includeTableName && propertyInfo.ReflectedType?.IsDefined(typeof(CompilerGeneratedAttribute)) == false)
            {
                // Include the table name for unambiguity, except for anonymyes types e.g. x => new { x.Id, x.Name }
                var tableName = Table(expression.Expression.Type, sqlBuilder);
                columnName = $"{tableName}.{columnName}";
            }
            ColumnNameCache.TryAdd(key, columnName);
        }

        DommelMapper.LogReceived?.Invoke($"Resolved column name '{columnName}' for '{propertyInfo}'");
        return columnName;
    }

And call this from SqlExpression.cs:


    /// <summary>
    /// Proccesses a member expression.
    /// </summary>
    /// <param name="expression">The member expression.</param>
    /// <returns>The result of the processing.</returns>
    protected virtual string MemberToColumn(MemberExpression expression) =>
        Resolvers.Column(expression, SqlBuilder);

Not sure if this is the best solution!

henkmollema commented 1 year ago

Does setting DommelMapper.IncludeTableNameInColumnName = false at application startup help?