Entity Framework Core: Dynamically build select list with navigational properties - entity-framework

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

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!)

How can I pass a LambdaExpression to an IncludeFilter?

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.

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.

Entity Framework Common Where

I have two entities with common properties. I need to apply some where in my query using the common properties. So I decide to do this:
public interface IContract
{
string Name{get;set;}
}
public class Entity1 : IContract
{
public string Name{get;set;}
}
public class Entity2 : IContract
{
public string Name{get;set;}
}
public class Repository
{
public IQueryable<T> Filter<T>(IQueryable<T> query, Request request) where T : IContract
{
return query.Where(x => x.Name== request.Name);
}
public IQueryable<Entity1> GetEntitity1()
{
return Filter(entities.Entity1, new Request { Name = "X" };
}
public IQueryable<Entity2> GetEntitity2()
{
return Filter(entities.Entity2, new Request { Name = "X" };
}
}
The problem is using this way and after apply .ToList() I receive:
NotSupportedException. LINQ to Entities only supports casting EDM primitive or enumeration types.
I there a way to fix it or I need to use the Where(string) of ObjectQuery?
Thanks
The problem is, that your type constraint for T is an interface.
The result of the filter method is basically a query like this.
entities.Entity1.Where(p => ((IContract)p).Name == request.Name);
This ((IContract)p) cast cannot be translated into a sql statement. As this cast is more or less useless in your case it can simply be removed from the query expression. The best way to do this is an ExpressionVisitor.
public IQueryable<T> Filter<T>(IQueryable<T> query, Request request) where T : IContract
{
var result = query.Where(x => x.Name == request.Name);
result = RemoveContract<T, IContract>(result);
return result;
}
public IQueryable<T> RemoveContract<T, TContract>(IQueryable<T> query) where T : TContract
{
var exp = query.Expression;
exp = new RemoveConvertExpressionVisitor<TContract>().Visit(exp);
return query.Provider.CreateQuery<T>(exp);
}
private class RemoveConvertExpressionVisitor<TContract> : ExpressionVisitor{
public override Expression Visit(Expression node)
{
var unary = node as UnaryExpression;
if (unary != null && unary.Type == typeof(TContract)) {
return unary.Operand;
}
return base.Visit(node);
}
}
After the RemoveContract method is called the query looks like this.
entities.Entity1.Where(p => p.Name == request.Name);

Dynamic way to Generate EntityTypeConfiguration : The type 'TResult' must be a non-nullable value type

I was thinking to generate EntityTypeConfiguration dynamically from run time and i don't want any EF dependency in Models[That is why i avoid Data Annotation].
So I declare a custom attribute(or can be any configuration file later on)
[AttributeUsage(AttributeTargets.Property, AllowMultiple=true )]
public class PersistableMemberAttribute : Attribute
{
public bool Iskey;
public bool IsRequired;
public bool IsIgnored;
public bool IsMany;
public string HasForeignKey;
public bool PropertyIsRequired;
public bool PropertyIsOptional;
}
And here is one of my Models is look like:
public class Blog
{
[PersistableMember(Iskey=true)]
public Guid BlogId { get; set; }
[PersistableMember(PropertyIsRequired = true)]
public string Name { get; set; }
public string Url { get; set; }
[PersistableMember(IsIgnored=true)]
public int Rating { get; set; }
[PersistableMember(IsMany =true)]
public ICollection<Post> Posts { get; set; }
}
Now I am going to write a generic EntityTypeConfiguration , which will create the configuration dynamically on run time based on the attribute values :
public class GenericEntityConfiguration<T> : EntityTypeConfiguration<T> where T : class
{
public GenericEntityConfiguration()
{
var members = typeof(T).GetProperties();
if (null != members)
{
foreach (var property in members)
{
var attrb= property.GetCustomAttributes(typeof( PersistableMemberAttribute ),false).OfType<PersistableMemberAttribute>();
if (attrb != null && attrb.Count() > 0)
{
foreach (var memberAttributute in attrb)
{
if (memberAttributute.Iskey || memberAttributute.IsIgnored)
{
var entityMethod = this.GetType().GetMethod("Setkey");
entityMethod.MakeGenericMethod(property.PropertyType)
.Invoke(this, new object[] { property, memberAttributute });
}
if (memberAttributute.IsRequired)
{
var entityMethod = this.GetType().GetMethod("SetRequired");
entityMethod.MakeGenericMethod(property.PropertyType)
.Invoke(this, new object[] { property, memberAttributute });
}
if (memberAttributute.PropertyIsRequired || memberAttributute.PropertyIsOptional)
{
var entityMethod = this.GetType().GetMethod("SetPropertyConfiguration");
entityMethod.MakeGenericMethod(property.PropertyType)
.Invoke(this, new object[] { property, memberAttributute });
}
}
}
}
}
}
public void SetPropertyConfiguration<TResult>(PropertyInfo propertyInfo, PersistableMemberAttribute attribute)
{
var functorParam = Expression.Parameter(typeof(T));
var lambda = Expression.Lambda(
Expression.Property(functorParam, propertyInfo)
, functorParam);
if (attribute.PropertyIsRequired)
{
this.Property<TResult>((Expression<Func<T, TResult>>)lambda).IsRequired();
}
if (attribute.PropertyIsOptional)
{
this.Property<TResult>((Expression<Func<T, TResult>>)lambda).IsOptional();
}
}
public void Setkey<TResult>(PropertyInfo propertyInfo, PersistableMemberAttribute attribute)
{
var functorParam = Expression.Parameter(typeof(T));
var lambda = Expression.Lambda(
Expression.Property(functorParam, propertyInfo)
, functorParam);
if (attribute.Iskey)
{
this.HasKey<TResult>((Expression<Func<T,TResult>>)lambda);
}
if (attribute.IsIgnored)
{
this.Ignore<TResult>((Expression<Func<T, TResult>>)lambda);
}
}
public void SetRequired<TResult>(PropertyInfo propertyInfo, PersistableMemberAttribute attribute) where TResult : class
{
var functorParam = Expression.Parameter(typeof(T));
var lambda = Expression.Lambda(
Expression.Property(functorParam, propertyInfo)
, functorParam);
if (attribute.IsRequired)
{
this.HasRequired<TResult>((Expression<Func<T, TResult>>)lambda);
}
}
}
But i got the compilation error of
Error 1 The type 'TResult' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'System.Data.Entity.ModelConfiguration.Configuration.StructuralTypeConfiguration.Property(System.Linq.Expressions.Expression>)' D:\R&D\UpdateStorePOC\UpdateStorePOC\Data\GenericEntityConfiguration.cs 63 17 UpdateStorePOC
which for these two statements:
this.Property<TResult>((Expression<Func<T, TResult>>)lambda).IsRequired();
this.Property<TResult>((Expression<Func<T, TResult>>)lambda).IsOptional();
that means that I need to put a constraint on my method to restrict it to a value type. In C#, this is done with the ‘struct’ keyword.
public void SetPropertyConfiguration<TResult>(PropertyInfo propertyInfo, PersistableMemberAttribute attribute) Where TResult : struct
But Its not the solution since my property type can be a class e.g string or int, bool double, etc . So it is not at all clear that I can send them into this method. Please help me to solve this issue whether there is any other way to do it.
I don't want any EF dependency in models.
With fluent mapping you're almost there and you won't come any closer. Your attributes, even though intended to be moved to a configuration file, don't make your model any more free of any EF footprint.1 Worse, they only add a second mapping layer (if you like) between your model and EF's mapping. I only see drawbacks:
You still have to maintain meta data for your model, probably not any less than regular fluent mapping and (probably) in awkward manually edited XML without compile-time checking.
You will keep expanding your code to cover cases that EF's mapping covers but yours doesn't yet.2 So it's a waste of energy: in the end you'll basically have rewritten EF's mapping methods.
You'll have to keep your fingers crossed when you want to upgrade EF.
With bugs/problems you're on your own: hard to get support from the community.
So my answer to your question help me to solve this issue would be: use fluent mapping out of the box. Keep it simple.
1 For example, you would still have to use the virtual modifier to enable proxies for lazy loading.
2 Like support for inheritance, unmapped foreign keys, max length, db data type, ... this could go on for a while.