How can I pass a LambdaExpression to an IncludeFilter? - entity-framework

I'm trying to pass a dynamically generated LambdaExpression to an IncludeFilter, as follows:
EDIT: I've changed my test code to the following, as (correctly) I wasn't implementing my "Where" statement. The correct where statement is being generated, but I can't pass the lambda statement into the IncludeFilter call:
DbSet<MyTestEntity> dbSet = db.Set<MyTestEntity>();
ParameterExpression parameter = Expression.Parameter(typeof(MyTestEntity), "t");
Expression idProperty = Expression.Property(parameter, "mytestentityid");
Expression delProperty = Expression.Property(parameter, "deleted");
Expression delTarget = Expression.Constant(false, typeof(bool));
Expression deletedMethod = Expression.Call(delProperty, "Equals", null, delTarget);
Expression<Func<MyTestEntity, bool>> lambda = Expression.Lambda<Func<MyTestEntity, bool>>(deletedMethod, parameter);
IQueryable<MyTestEntity> query = dbSet.Where(lambda);
Console.WriteLine("Current Query: {0}", query.ToString());
foreach (string include in includes)
{
Type subType = db.GetType().Assembly.GetTypes().SingleOrDefault(x => x.Name.EndsWith(include));
Assert.IsNotNull(subType);
ParameterExpression innerParam = Expression.Parameter(subType, subType.Name);
Assert.IsNotNull(innerParam);
MemberExpression inrDelProp = Expression.Property(innerParam, "deleted");
Assert.IsNotNull(inrDelProp);
ConstantExpression inrDelCstProp = Expression.Constant(false, typeof(bool));
Assert.IsNotNull(inrDelCstProp);
MethodCallExpression inrDelMthd = Expression.Call(inrDelProp, "Equals", null, inrDelCstProp);
Assert.IsNotNull(inrDelMthd);
var delegateType = typeof(Func<,>).MakeGenericType(subType, typeof(bool));
dynamic inrLmbdaExpr = Expression.Lambda(delegateType, inrDelMthd, innerParam);
Assert.IsNotNull(inrLmbdaExpr);
Console.WriteLine("inrLmbdaExpr: {0}", inrLmbdaExpr.ToString()); // Result: MyTestEntityChild => MyTestEntityChild.deleted.Equals(false)
query = query.IncludeFilter(inrLmbdaExpr); // ERROR HERE
Assert.IsNotNull(query);
Console.WriteLine("-------------------------------------------------");
Console.WriteLine("Current Query: {0}", query.ToString());
}
This is built into an abstract class allowing me to pass in an entity type, retrieve the records, and reuse the method irrespective of the entity type; however, I'm also trying to filter out child entities that are marked as deleted (thus the use of EF+).
How can I do this?
EDIT 2: So, I realized I also have Linq.Dynamic.Core (!) in my solution, so I already have access to parsing a LambdaExpression from string. However, the error I get says that IncludeFilter doesn't know which method it's trying to use. (I see in the Object Browser that one uses Expression> and one uses Expression>>. If I could just figure out how to get the IncludeFilter to recognize which method, I think I'd be done! Here's a sample of the code I've rewritten:
string myIncStr = String.Format("x => x.{0}.Where(s => s.deleted.Equals(false)).Where(x => x.MyEntityId.Equals(IncomingId)",includedEntityName);
IEnumerable<MyEntity> result = db.MyEntity.IncludeFilter(System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(typeof(MyChildEntity), myIncStr, null));
Is there a way to "force" (for lack of a better term) the IncludeFilter to use one method? Is it by passing a value instead of null in the Parser?
BTW, thanks for your help. Your EFP library is actually excellent.

Disclaimer: I'm the owner of the project Entity Framework Plus
Yes, it's possible but only if you can specify the generic argument type required by the method explicitly for the QueryFilter (As you mentioned in your comment).
Otherwise, you will need to also call the QueryFilter via the expression to make everything generic.
However, your current expression seems to have some error such as not calling the Where methods.
What you want to achieve is probably something similar to this:
query = query.IncludeFilter(x => x.Childs.Where(y => !y.Deleted));
Disclaimer: I'm the owner of the project Eval-Expression.NET
This library is not free but makes working with a dynamic expression easier and faster.
Once you get used, you can quickly create a dynamic expression in only a few lines as you normally write LINQ. Here is a code that could handle a similar scenario as your:
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Windows.Forms;
using Z.Expressions;
namespace Z.EntityFramework.Plus.Lab.EF6
{
public partial class Form_Request_IncludeFilter_Dynamic : Form
{
public Form_Request_IncludeFilter_Dynamic()
{
InitializeComponent();
// CLEAN
using (var context = new EntityContext())
{
context.MyEntityClasses.RemoveRange(context.MyEntityClasses);
context.MyEntityClassToFilters.RemoveRange(context.MyEntityClassToFilters);
context.SaveChanges();
}
// SEED
using (var context = new EntityContext())
{
var entity1 = context.MyEntityClasses.Add(new MyEntityClass {ColumnInt = 1, Childs = new List<MyEntityClassToFilter>()});
entity1.Childs.Add(new MyEntityClassToFilter {ColumnInt = 1, Deleted = true});
entity1.Childs.Add(new MyEntityClassToFilter {ColumnInt = 2, Deleted = false});
context.MyEntityClasses.Add(new MyEntityClass {ColumnInt = 2});
context.MyEntityClasses.Add(new MyEntityClass {ColumnInt = 3});
context.SaveChanges();
}
// TEST
using (var context = new EntityContext())
{
// You must register extension method only once
// That should not be done here, but for example purpose
EvalManager.DefaultContext.RegisterExtensionMethod(typeof(QueryIncludeFilterExtensions));
// That could be also dynamic. I believe you already handle this part
IQueryable<MyEntityClass> query = context.MyEntityClasses;
// The path to include
var include = "Childs";
// The dynamic expression to execute
var dynamicExpression = "IncludeFilter(x => x." + include + ".Where(y => !y.Deleted));";
query = query.Execute<IQueryable<MyEntityClass>>(dynamicExpression);
// The result
var list = query.ToList();
}
}
public class EntityContext : DbContext
{
public EntityContext() : base("CodeFirstEntities")
{
}
public DbSet<MyEntityClass> MyEntityClasses { get; set; }
public DbSet<MyEntityClassToFilter> MyEntityClassToFilters { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Types().Configure(x =>
x.ToTable(GetType().DeclaringType != null
? GetType().DeclaringType.FullName.Replace(".", "_") + "_" + x.ClrType.Name
: ""));
base.OnModelCreating(modelBuilder);
}
}
public class MyEntityClass
{
public int ID { get; set; }
public int ColumnInt { get; set; }
public List<MyEntityClassToFilter> Childs { get; set; }
}
public class MyEntityClassToFilter
{
public int ID { get; set; }
public int ColumnInt { get; set; }
public bool Deleted { get; set; }
}
}
}
EDIT: Answer question
Please review my changed code
You are still missing the where clause.
What you have is something similar to this as you commented
// Result: MyTestEntityChild => MyTestEntityChild.deleted.Equals(false)
What you want is something similar to this
// Result: MyTestEntityChild => MyTestEntityChild.Where(x => x.deleted.Equals(false))
EDIT: Answer question
Oh sorry, I now understand the problem with it.
If you don't know the type, you will need to call the IncludeFilter in an expression as well to make everything generic. It cannot be called explicitely like you are trying to do.

Related

Building GroupBy Expression Tree - IEnumerable parameter not defined error

I want to build an expression for IQueryable GroupBy. While at the moment I'm just simplifying the problem to try and get it working, the eventual final implementation will involve the creation of quite complex expression trees so I want to build a complete expression that can then be integrated into other expressions.
I specifically want to build an expression of this overload:
public static System.Linq.IQueryable<TResult> GroupBy<TSource,TKey,TResult> (
this System.Linq.IQueryable<TSource> source,
System.Linq.Expressions.Expression<Func<TSource,TKey>> keySelector,
System.Linq.Expressions.Expression<Func<TKey,System.Collections.Generic.IEnumerable<TSource>,TResult>> resultSelector);
... my problem is in the implementation of the resultSelector and and the IEnumerable<TSource>.
I have a table of Customers (just dummy data for the purposes of working out this problem). This is stored in an SQL DB and I specifically want to use IQueryable to access the data.
public class Customer
{
public int Id { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public int Age { get; set; }
}
I also have a GroupResult class used to hold the results of the GroupBy (I have different constructors which I've been using in my testing to work out where my problem is occurring)
internal class GroupResult
{
public string? Name { get; set; }
public int NumRecords { get; set; }
public decimal AverageAge { get; set; }
public int TotalAge { get; set; }
public GroupResult() { }
public GroupResult(string name)
{
Name = name;
}
public GroupResult(IEnumerable<Customer> customers)
{
Name = Guid.NewGuid().ToString();
NumRecords = customers.Count();
}
public GroupResult(string name, IEnumerable<Customer> customers)
{
Name = name;
NumRecords = customers.Count();
}
}
The main static class that displays prompts to select column to group on, creates the relevant expression tree and executes it
internal static class SimpleGroupByCustomer
{
internal static DataContext db;
internal static void Execute()
{
using (db = new DataContext())
{
//get input
Console.WriteLine();
Console.WriteLine("Simple Customer GroupBy");
Console.WriteLine("=======================");
Console.WriteLine("Simple GroupBy on the Customer Table");
Console.WriteLine();
Console.WriteLine("Select the property that you want to group by.");
Console.WriteLine();
var dbSet = db.Set<Customer>();
var query = dbSet.AsQueryable();
//for this example we're just prompting for a column in the customer table
//GetColumnName is a helper function that lists the available columns and allows
//one to be selected
string colName = Wrapper.GetColumnName("Customer");
MethodInfo? method = typeof(SimpleGroupByCustomer).GetMethod("GetGroupBy",
BindingFlags.Static | BindingFlags.NonPublic);
if (method != null)
{
method = method.MakeGenericMethod(new Type[] { typeof(String), query.ElementType });
method.Invoke(null, new object[] { query, colName });
}
}
}
internal static void GetGroupBy<T, TTable>(IQueryable query, string colName)
{
Type TTmp = typeof(TTable);
var param = Expression.Parameter(TTmp, "c");
var prop = Expression.PropertyOrField(param, colName);
LambdaExpression keySelector = Expression.Lambda<Func<TTable, T>>(prop, param);
var param1 = Expression.Parameter(typeof(T), "Key");
var param2 = Expression.Parameter(typeof(IEnumerable<TTable>), "Customers");
var ci = typeof(GroupResult).GetConstructor(new[] { typeof(T), typeof(IEnumerable<TTable>) });
//var ci = typeof(GroupResult).GetConstructor(new[] { typeof(T) });
//var ci = typeof(GroupResult).GetConstructor(new[] { typeof(IEnumerable<TTable>) });
if (ci == null)
return;
var pExp = new ParameterExpression[] { param1, param2 };
var methodExpression = Expression.Lambda<Func<T, IEnumerable<TTable>, GroupResult>>(
Expression.New(ci, new Expression[] { param1, param2 }), //<--- ERROR HERE
pExp
);
Type[] typeArgs = new Type[] { typeof(TTable), typeof(T), typeof(GroupResult) };
Expression[] methodParams = new Expression[] { query.Expression, keySelector, methodExpression };
var resultExpression = Expression.Call(typeof(Queryable), "GroupBy", typeArgs, methodParams);
IQueryable dbQuery = query.Provider.CreateQuery(resultExpression);
if (dbQuery is IQueryable<GroupResult> results)
{
foreach (var result in results)
{
Console.WriteLine("{0,-15}\t{1}", result.Name, result.NumRecords.ToString());
}
}
}
}
When I run this and try and iterate through the results I get the following exception:
System.InvalidOperationException: 'variable 'Customers' of type 'System.Collections.Generic.IEnumerable`1[ExpressionTrees3.Data.Customer]' referenced from scope '', but it is not defined'
which is being caused by the param2 ParameterExpression marked above.
If I use the GroupResult constructor that just takes the key value
var ci = typeof(GroupResult).GetConstructor(new[] { typeof(T) });
and omit the param2 from the Lambda body definition the code works as expected and I get a collection of GroupResult records containing the distinct key values in the Name field (but obviously no summary value).
I've tried everything I can think of and just can't get past this error - it's as though the GroupBy is not actually producing the IEnumerable grouping of Customers for each key.
I suspect I'm missing something really obvious here, but just can't see it. Any help would really very much appreciated.
Please note that I am after answers to this specific issue, I'm not looking for alternative ways of doing a GroupBy (unless there's a fundamental reason why this shouldn't work) - this will be rolled into a much larger solution for building queries and I want to use the same process throughout.
Thanks Svyatoslav - as I thought, it was me being especially dumb!
Your comments, as well as a discussion with a friend who has a lot SQL knowledge pointed me in the right direction.
I had been thinking that the GroupBy expression was going to return an Enumerable for each key value and was trying to pass that into a function ... it always felt wrong, but I just ignored that and kept going.
It's obvious now that I need to tell the GroupBy what to calculate and return (i.e. your comment about aggregation).
So for this easy example, the solution is very simple:
var pExp = new ParameterExpression[] { param1, param2 };
var countTypes = new Type[] { typeof(TTable) };
var countParams = new Expression[] { param2 };
var countExp = Expression.Call(typeof(Enumerable), "Count", countTypes, countParams);
var methodExpression = Expression.Lambda<Func<T, IEnumerable<TTable>, GroupResult>>(
Expression.New(ci, new Expression[] { param1, countExp }),
pExp
);
Just by adding the 'Count' expression into the GroupBy method call it works!
.. and adding a new ctor for GroupResult:
public GroupResult(string name, int count)
{
Name = name;
NumRecords = count;
}
(yep, I feel a bit stupid!)

Entity Framework Core: Dynamically build select list with navigational properties

The question is similar to this one, but answer does not provide 2 critical things to me:
I need the code to work over navigational properties
I am trying to build extension method
I want to write queries like this:
this.context.User
.Where(t => t.Id > 10)
.SelectCustom(t => t.Address.Country.Title)
.OrderBy(t => t.DisplayName)
.Skip(10).Take(5);
With answer in provided link I get this far:
public class SelectList<TSource>
{
private List<MemberInfo> members = new List<MemberInfo>();
public SelectList<TSource> Add<TValue>(Expression<Func<TSource, TValue>> selector)
{
var member = ((MemberExpression)selector.Body).Member;
members.Add(member);
return this;
}
public Expression<Func<TSource, TResult>> ToDynamicColumns()
{
return this.members.??????????;
}
}
public static IQueryable<T> SelectCustom<T>(this IQueryable<T> query, Expression<Func<TSource, TKey>> FirstAdditional = null)
{
var columns = new SelectList<T>();
columns.Add(t => t.Id);
columns.Add(t => t.DisplayName)
if (FirstAdditional != null)
columns.Add(FirstAdditional);
return query.Select(columns.ToDynamicColumns);
}
Can this be done with EF Core 2.0?
EF will look through lambda invoke operations as if the body of that expression was inlined. So I would recommend leaving the source expressions alone and just generate expressions to invoke them.
Also I would keep the result type simple, and just return each row as an array of object. This should result in less overhead than creating lots of dictionaries. If you do need to access fields by name, you should create a single dictionary to maintain the relationship between names and column numbers.
public class SelectList<TSource>
{
private List<LambdaExpression> members = new List<LambdaExpression>();
public SelectList<TSource> Add<TValue>(Expression<Func<TSource, TValue>> selector)
{
members.Add(selector);
return this;
}
public Expression<Func<TSource, TResult>> ToDynamicColumns()
{
var parameter = Expression.Parameter(typeof(TSource), "e");
return Expression.Lambda<Func<TSource, object[]>>(
Expression.NewArrayInit(
typeof(object),
members.Select(m =>
Expression.Convert(Expression.Invoke(m, parameter), typeof(object))
)
),
parameter);
}
}
Though in your case, since you are writing an extension method to only return the same key details and a single additional field, you could probably define a single generic type to hold the results, and avoid any mucking around with Linq expressions at all;
public class UserResult<V>{
public int Id { get; set; }
public string DisplayName { get; set; }
public V Value { get; set; }
}
public static IQueryable<UserResult<V>> SelectCustom<V>(this IQueryable<User> query, Expression<Func<User, V>> ValueGetter)
{
return query.Select(u => new UserResult<V>{
Id = u.Id,
DisplayName = u.DisplayName,
Value = ValueGetter(u)
});
}
Well almost, if c# would just allow you to compile a call of one Expression<Delegate> from within another. Instead we can implement an ExpressionVisitor to unwrap any call to Compile;
public class DontCompile : ExpressionVisitor
{
protected override Expression VisitMember(MemberExpression node)
{
// Inline any lambda arguments that are expressions
if (node.Expression is ConstantExpression lambdaArgs
&& node.Member is FieldInfo field
&& typeof(Expression).IsAssignableFrom(field.FieldType))
return (Expression)field.GetValue(lambdaArgs.Value);
return base.VisitMember(node);
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
// Don't compile lambda expressions
if (node.Method.Name == "Compile"
&& typeof(LambdaExpression).IsAssignableFrom(node.Object.Type))
return Visit(node.Object);
return base.VisitMethodCall(node);
}
public static Expression<T> Tidy<T>(Expression<T> func) => (Expression<T>)new DontCompile().Visit(func);
}
...
return query.Select(DontCompile.Tidy<...>(u => new UserResult<V>{
Id = u.Id,
DisplayName = u.DisplayName,
Value = ValueGetter.Compile()(u)
});
...
But that's starting to look a bit messy.
You could do this with Expression.ListInit, here the TResult must have an Add instance method e.g. Dictionary<string, object>. This could just work but I havent even compiled it. In any way this should give enough hint on how to build this the way you want.
public Expression<Func<TSource, Dictionary<string, object>>> ToDynamicColumns()
{
var addMethod = typeof(TResult).GetMethod("Add");
var paramX = Expression.Parameter(typeof(TSource), "e");
var bindings =
this.members.Select (
member =>
return Expression.ElementInit(
addMethod,
Expression.Constant(mem.Name),
Expression.Convert(Expression.Property(paramX, member), typeof(object))
);
)
var listInit = Expression.ListInit(
Expression.New(typeof(TResult)),
bindings
);
return Expression.Lambda<Func<TSource, Dictionary<string, object>>(
listInit,
paramX
);
}

How to use AutoMapper to map an Enum Id to a queryable projection based on Enum Values?

I am trying to map an Enum value (Int16) to an IQueryable projection so that the string representation of the Enum value is used rather than the integer value for the database sorting.
The approach I used was taken from here.
Enum as follows:
public enum SafetyCategoryEnum : Int16 { High, Medium, Low }
See my AutoMapper mapping below:
config.CreateMap<SupplierContractEntity, SupplierContractEntityWrapper>()
.ForMember(dest => dest.SafetyCategoryEnumId,
opt => { opt.MapFrom(EnumerableExpressionHelper.CreateEnumToStringExpression((SupplierContractEntity e) => e.SafetyCategoryEnumId)); })
SupplierContractEntity is the EntityFramework Entity.
public class SupplierContractEntity : Entity
{
//Other properties removed for brevity
public Int16 SafetyCategoryEnumId { get; set; }
}
SupplierContractEntityWrapper is a custom business object:
public class SupplierContractEntityWrapper : EntityWrapper
{
//Other properties removed for brevity
public SafetyCategoryEnum? SafetyCategoryEnumId { get; set; }
}
Expression mapping is reversed in AutoMapper which is why the Entity maps to the business object
Implementation of CreateEnumToStringExpression:
public static class EnumerableExpressionHelper
{
public static Expression<Func<TSource, string>> CreateEnumToStringExpression<TSource, TMember>(Expression<Func<TSource, TMember>> memberAccess, string defaultValue = "")
{
var type = typeof(SafetyCategoryEnum);
var enumNames = Enum.GetNames(type);
var enumValues = (Int16[])Enum.GetValues(type);
var inner = (Expression)Expression.Constant(defaultValue);
var parameter = memberAccess.Parameters[0];
for (int i = 0; i < enumValues.Length; i++)
{
inner = Expression.Condition(
Expression.Equal(memberAccess.Body, Expression.Constant(enumValues[i])),
Expression.Constant(enumNames[i]), inner);
}
MyExpressionVisitor myExpressionVisitor = new MyExpressionVisitor();
var expression = Expression.Lambda<Func<TSource, string>>(inner, parameter);
myExpressionVisitor.Visit(expression);
return expression;
}
}
When performing a sort AutoMapper throws the following exception:
InvalidOperationException: Rewriting child expression from type 'System.Nullable`1[SafetyCategoryEnum]' to type 'System.String' is not allowed, because it would change the meaning of the operation.
If this is intentional, override 'VisitUnary' and change it to allow this rewrite.
Is there any way around this type issue?
Any help would be greatly appreciated!
This works for me with the latest AM, but you need to make the destination a string. I think you're not on the latest version and you've hit an already fixed bug.

EF get dbset name in runtime from Type

Purpose:
I need to get the name of the dbset name of the entity
typeof(UserAccount) = "UserAccounts".
But in runtime I need a common type for the loop and therefor do not know example "UserAccount".
Only the "name" from typeof?
I have created an DbContext with some entities.
I have been googling for some time but it do not seem to be working for me because of the Type converting?
Please see my method GetDbSetName in the bottom of this description.
I am pretty new at this EF stuff - so please help med with my issue as described below ;-)
public class MyEntities : DbContext
{
public DbSet<UserAccount> UserAccounts { get; set;}
public DbSet<UserRole> UserRoles { get; set; }
public DbSet<UserAccountRole> UserAccountRoles { get; set; }
}
Defined a list of Type to control the output:
public static List<Type> ModelListSorted()
{
List<Type> modelListSorted = new List<Type>();
modelListSorted.Add(typeof(UserRole));
modelListSorted.Add(typeof(UserAccountRole));
modelListSorted.Add(typeof(UserAccount));
return modelListSorted;
}
The problem is below using Type - If I use "UserAccount" it Works and I get "UserAccounts".
But I do not have the "UserAccount" in runtime as I am in a loop with a serie of types.
I do only have the Type list giving the e
public static loopList()
{
List<Type> modelListSorted = ModelListSorted();
foreach (Type currentType in modelListSorted)
{
string s = DataHelper.GetDbSetName(currentType, db);
}
}
HERE IS THE METHOD GIVING ME THE CHALLANGES ;-)
Meaning not compiling.
saying I am missing a assembly?
I know it is pretty pseudo but can this be done smoothly?
public static string GetDbSetName(Type parmType, MyEntities db)
{
string dbsetname = (db as IObjectContextAdapter).ObjectContext.CreateObjectSet<parmType>().EntitySet.Name;
return dbsetname;
}
The challenge here is that two reflection steps are involved, one to invoke the generic CreateObjectSet method and one to get the EntitySet from the result. Here's a way to do this:
First, the method:
string GetObjectSetName(ObjectContext oc, MethodInfo createObjectSetMethodInfo,
Type objectSetType, Type entityType)
{
var objectSet = createObjectSetMethodInfo.MakeGenericMethod(entityType)
.Invoke(oc, null);
var pi = objectSetType.MakeGenericType(entityType).GetProperty("EntitySet");
var entitySet = pi.GetValue(objectSet) as EntitySet;
return entitySet.Name;
}
As you see, I first get the ObjectSet by invoking the MethodInfo representing the generic method CreateObjectSet<T>(). Then I find the PropertyInfo for the EntitySet property of the generic type ObectSet<T>. Finally, I get this property's value and the name of the obtained EntitySet.
To do this, I first get a MethodInfo for CreateObjectSet<>() (the one without parameters) and the ObjectSet<> type
var createObjectSetMethodInfo =
typeof(ObjectContext).GetMethods()
.Single(i => i.Name == "CreateObjectSet"
&& !i.GetParameters().Any());
var objectSetType = Assembly.GetAssembly(typeof(ObjectContext))
.GetTypes()
.Single(t => t.Name == "ObjectSet`1");
In GetObjectSetName their generic parameters are specified by a concrete entity type, which is done by these "MakeGeneric..." methods.
var oc = (dbContextInstance as IObjectContextAdapter).ObjectContext;
var entityType = typeof(UserRole);
var name = GetObjectSetName(oc, createObjectSetMethodInfo, objectSetType, entityType);
In EF 6 these should be the usings:
using System.Data.Entity.Core.Metadata.Edm
using System.Data.Entity.Core.Objects
using System.Data.Entity.Infrastructure
using System.Linq
using System.Reflection

How to use Entity Framework to map results of a stored procedure to entity with differently named parameters

I am trying to create a basic example using Entity Framework to do the mapping of the output of a SQL Server Stored procedure to an entity in C#, but the entity has differently (friendly) names parameters as opposed to the more cryptic names. I am also trying to do this with the Fluent (i.e. non edmx) syntax.
What works ....
The stored procedure returns values called: UT_ID, UT_LONG_NM, UT_STR_AD, UT_CITY_AD, UT_ST_AD, UT_ZIP_CD_AD, UT_CT
If I create an object like this ...
public class DBUnitEntity
{
public Int16 UT_ID { get; set; }
public string UT_LONG_NM { get; set; }
public string UT_STR_AD { get; set; }
public string UT_CITY_AD { get; set; }
public string UT_ST_AD { get; set; }
public Int32 UT_ZIP_CD_AD { get; set; }
public string UT_CT { get; set; }
}
and an EntityTypeConfiguration like this ...
public class DbUnitMapping: EntityTypeConfiguration<DBUnitEntity>
{
public DbUnitMapping()
{
HasKey(t => t.UT_ID);
}
}
... which I add in the OnModelCreating of the DbContext, then I can get the entities just fine out of the database, which is nice, using this ....
var allUnits = _context.Database.SqlQuery<DBUnitEntity>(StoredProcedureHelper.GetAllUnitsProc);
BUT, What Doesn't Work
If I want an entity like this, with friendlier names ....
public class UnitEntity : IUnit
{
public Int16 UnitId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public Int32 Zip { get; set; }
public string Category { get; set; }
}
and an EntityTypeConfiguration like this ...
public UnitMapping()
{
HasKey(t => t.UnitId);
Property(t => t.UnitId).HasColumnName("UT_ID");
Property(t => t.Name).HasColumnName("UT_LONG_NM");
Property(t => t.Address).HasColumnName("UT_STR_AD");
Property(t => t.City).HasColumnName("UT_CITY_AD");
Property(t => t.State).HasColumnName("UT_ST_AD");
Property(t => t.Zip).HasColumnName("UT_ZIP_CD_AD");
Property(t => t.Category).HasColumnName("UT_CT");
}
When I try to get the data I get a System.Data.EntityCommandExecutionException with the message ....
"The data reader is incompatible with the specified 'DataAccess.EFCodeFirstSample.UnitEntity'. A member of the type, 'UnitId', does not have a corresponding column in the data reader with the same name."
If I add the "stored procedure named" property to the entity, it goes and complains about the next "unknown" property.
Does "HasColumnName" not work as I expect/want it to in this code-first stored procedure fluent style of EF?
Update:
Tried using DataAnnotations (Key from ComponentModel, and Column from EntityFramework) ... ala
public class UnitEntity : IUnit
{
[Key]
[Column("UT_ID")]
public Int16 UnitId { get; set; }
public string Name { get; set; }
That did remove the need for any EntityTypeConfiguration at all for the DBUnitEntity with the database-identical naming (i.e. just adding the [Key] Attribute), but did nothing for the entity with the property names that don't match the database (same error as before).
I don't mind using the ComponentModel Annotations in the Model, but I really don't want to use the EntityFramework Annotations in the model if I can help it (don't want to tie the Model to any specific data access framework)
From Entity Framework Code First book (page 155):
The SQLQuery method always attempts the column-to-property matching based on property name...
None that the column-to-property name matching does not take any mapping into account. For example, if you had mapped the DestinationId property to a column called Id in the Destination table, the SqlQuery method would not use this mapping.
So you cannot use mappings when calling stored procedure. One workaround is to modify your stored procedure to return result with aliases for each column that will match your object properties' names.
Select UT_STR_AD as Address From SomeTable etc
This isn't using Entity Framework but it is stemming from dbcontext. I have spent hours upon hours scouring the internet and using dot peek all for nothing. I read some where that the ColumnAttribute is ignored for SqlQueryRaw. But I have crafted up something with reflection, generics, sql datareader, and Activator. I am going to be testing it on a few other procs. If there is any other error checking that should go in, comment.
public static List<T> SqlQuery<T>( DbContext db, string sql, params object[] parameters)
{
List<T> Rows = new List<T>();
using (SqlConnection con = new SqlConnection(db.Database.Connection.ConnectionString))
{
using (SqlCommand cmd = new SqlCommand(sql, con))
{
cmd.CommandType = CommandType.StoredProcedure;
foreach (var param in parameters)
cmd.Parameters.Add(param);
con.Open();
using (SqlDataReader dr = cmd.ExecuteReader())
{
if (dr.HasRows)
{
var dictionary = typeof(T).GetProperties().ToDictionary(
field => CamelCaseToUnderscore(field.Name), field => field.Name);
while (dr.Read())
{
T tempObj = (T)Activator.CreateInstance(typeof(T));
foreach (var key in dictionary.Keys)
{
PropertyInfo propertyInfo = tempObj.GetType().GetProperty(dictionary[key], BindingFlags.Public | BindingFlags.Instance);
if (null != propertyInfo && propertyInfo.CanWrite)
propertyInfo.SetValue(tempObj, Convert.ChangeType(dr[key], propertyInfo.PropertyType), null);
}
Rows.Add(tempObj);
}
}
dr.Close();
}
}
}
return Rows;
}
private static string CamelCaseToUnderscore(string str)
{
return Regex.Replace(str, #"(?<!_)([A-Z])", "_$1").TrimStart('_').ToLower();
}
Also something to know is that all of our stored procs return lowercase underscore delimited. The CamelCaseToUnderscore is built specifically for it.
Now BigDeal can map to big_deal
You should be able to call it like so
Namespace.SqlQuery<YourObj>(db, "name_of_stored_proc", new SqlParameter("#param",value),,,,,,,);
The example posted by "DeadlyChambers" is great but I would like to extend the example to include the ColumnAttribute that you can use with EF to add to a properties to map a SQL field to a Class property.
Ex.
[Column("sqlFieldName")]
public string AdjustedName { get; set; }
Here is the modified code.
This code also include a parameter to allow for custom mappings if needed by passing a dictionary.
You will need a Type Converter other than Convert.ChangeType for things like nullable types.
Ex. If you have a field that is bit in the database and nullable boolean in .NET you will get a type convert issue.
/// <summary>
/// WARNING: EF does not use the ColumnAttribute when mapping from SqlQuery. So this is a "fix" that uses "lots" of REFLECTION
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="database"></param>
/// <param name="sqlCommandString"></param>
/// <param name="modelPropertyName_sqlPropertyName">Model Property Name and SQL Property Name</param>
/// <param name="sqlParameters">SQL Parameters</param>
/// <returns></returns>
public static List<T> SqlQueryMapped<T>(this System.Data.Entity.Database database,
string sqlCommandString,
Dictionary<string,string> modelPropertyName_sqlPropertyName,
params System.Data.SqlClient.SqlParameter[] sqlParameters)
{
List<T> listOfT = new List<T>();
using (var cmd = database.Connection.CreateCommand())
{
cmd.CommandText = sqlCommandString;
if (cmd.Connection.State != System.Data.ConnectionState.Open)
{
cmd.Connection.Open();
}
cmd.Parameters.AddRange(sqlParameters);
using (var dataReader = cmd.ExecuteReader())
{
if (dataReader.HasRows)
{
// HACK: you can't use extension methods without a type at design time. So this is a way to call an extension method through reflection.
var convertTo = typeof(GenericExtensions).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(mi => mi.Name == "ConvertTo").Where(m => m.GetParameters().Count() == 1).FirstOrDefault();
// now build a new list of the SQL properties to map
// NOTE: this method is used because GetOrdinal can throw an exception if column is not found by name
Dictionary<string, int> sqlPropertiesAttributes = new Dictionary<string, int>();
for (int index = 0; index < dataReader.FieldCount; index++)
{
sqlPropertiesAttributes.Add(dataReader.GetName(index), index);
}
while (dataReader.Read())
{
// create a new instance of T
T newT = (T)Activator.CreateInstance(typeof(T));
// get a list of the model properties
var modelProperties = newT.GetType().GetProperties();
// now map the SQL property to the EF property
foreach (var propertyInfo in modelProperties)
{
if (propertyInfo != null && propertyInfo.CanWrite)
{
// determine if the given model property has a different map then the one based on the column attribute
string sqlPropertyToMap = (propertyInfo.GetCustomAttribute<ColumnAttribute>()?.Name ?? propertyInfo.Name);
string sqlPropertyName;
if (modelPropertyName_sqlPropertyName!= null && modelPropertyName_sqlPropertyName.TryGetValue(propertyInfo.Name, out sqlPropertyName))
{
sqlPropertyToMap = sqlPropertyName;
}
// find the SQL value based on the column name or the property name
int columnIndex;
if (sqlPropertiesAttributes.TryGetValue(sqlPropertyToMap, out columnIndex))
{
var sqlValue = dataReader.GetValue(columnIndex);
// ignore this property if it is DBNull
if (Convert.IsDBNull(sqlValue))
{
continue;
}
// HACK: you can't use extension methods without a type at design time. So this is a way to call an extension method through reflection.
var newValue = convertTo.MakeGenericMethod(propertyInfo.PropertyType).Invoke(null, new object[] { sqlValue });
propertyInfo.SetValue(newT, newValue);
}
}
}
listOfT.Add(newT);
}
}
}
}
return listOfT;
}