I'm working with a large enterprise SQL Server database with dozens of tables that are used mainly as lookups to populate dropdown lists, etc. These tables all follow the convention of having (table)Name as the string value of the item. For example the State
table has a StateName
column with values like California, Florida, etc.
There is an old method used to load data for a dropdown that uses raw SQL, it looks like this:
public async Task<IEnumerable<string>> GetLookupOptions(string table)
{
List<string> values = new List<string>();
using (var command = _context.Database.GetDbConnection().CreateCommand())
{
command.CommandText = $"SELECT {table}Name FROM {table} WHERE IsApproved = 1";
_context.Database.OpenConnection();
using (var result = await command.ExecuteReaderAsync())
{
do
{
while (result.Read())
{
values.Add(result.GetString(0));
}
} while (result.NextResult());
}
}
return values;
}
This isn't testable using InMemoryDatabase
and only works against an actual database connection. I have rewritten it using Reflection and Expressions to query the DbContext.DbSet<T>
based on the table name provided, like this:
public IEnumerable<string> GetLookupOptions(string table)
{
// Get the Type for State and then replace its name with whatever entity this is querying.
// This is hacky but it's used so the AssemblyQualifiedName will always have correct
// version info.
Type t = typeof(State);
t = Type.GetType(t.AssemblyQualifiedName.Replace("State", table));
// Get lambda used to filter <table> where IsApproved is true.
object whereLamda = this.GetType()
.GetMethod(nameof(CreateWhereExpression), BindingFlags.NonPublic | BindingFlags.Instance)
.MakeGenericMethod(t)
.Invoke(this, new object[0]);
// Get lambda used to select <table>Name from <table>.
object selectLamda = this.GetType()
.GetMethod(nameof(CreateSelectExpression), BindingFlags.NonPublic | BindingFlags.Instance)
.MakeGenericMethod(t)
.Invoke(this, new object[] { table });
// Get the DbSet<T> for the <table>.
object set = _context.GetType()
.GetMethod("Set")
.MakeGenericMethod(t)
.Invoke(_context, new object [0]);
IEnumerable<MethodInfo> whereMethods = typeof(Enumerable)
.GetMethods().Where(m => m.Name == nameof(Enumerable.Where));
// Apply Where() method to DbSet.
object filteredApproved = whereMethods
.ElementAt(0)
.MakeGenericMethod(t)
.Invoke(set, new object[] { set, whereLamda });
IEnumerable<MethodInfo> selectMethods = typeof(Enumerable)
.GetMethods().Where(m => m.Name == nameof(Enumerable.Select));
// Apply Select() method to filtered query.
object r = selectMethods
.ElementAt(0)
.MakeGenericMethod(t, typeof(string))
.Invoke(filteredApproved, new object[] { filteredApproved, selectLamda });
return r as IEnumerable<string>;
}
private Func<T, string> CreateSelectExpression<T>(string tableName)
{
var param = Expression.Parameter(typeof(T));
var expr = Expression.Property(param, $"{tableName}Name");
return Expression.Lambda<Func<T, string>>(expr, param).Compile();
}
private Func<T, bool> CreateWhereExpression<T>()
{
var param = Expression.Parameter(typeof(T));
var expr = Expression.Equal(
Expression.Property(param, "IsApproved"),
Expression.Constant(true));
return Expression.Lambda<Func<T, bool>>(expr, param).Compile();
}
My concerns are:
- Getting the correct
Where
andSelect
methods from Reflection, I don't like relying onElementAt()
. Both methods have overloads with 2 parameters. - Any and all other problems. Have I created a monster?