Left outer join with Linq Expressions - c#

I'm trying to build left outer join queries with Linq Expressions but now I really hit the wall.
What I want to accomplish is the following query:
var q =
from i in ProcessInstances
join dof1 in Queries.DataObjectFieldsQuery(this) on new { instanceId = i.ObjectID, dataId = fields[0,0], fieldId = fields[0,1] } equals new { instanceId = dof1.ProcessInstanceObjectID, dataId = dof1.DataID, fieldId = dof1.FieldID } into dofs1
from dof1 in dofs1.DefaultIfEmpty()
join dof2 in Queries.DataObjectFieldsQuery(this) on new { instanceId = i.ObjectID, dataId = fields[1,0], fieldId = fields[1,1] } equals new { instanceId = dof2.ProcessInstanceObjectID, dataId = dof2.DataID, fieldId = dof2.FieldID } into dofs2
from dof2 in dofs2.DefaultIfEmpty()
join dof3 in Queries.DataObjectFieldsQuery(this) on new { instanceId = i.ObjectID, dataId = fields[2,0], fieldId = fields[2,1] } equals new { instanceId = dof3.ProcessInstanceObjectID, dataId = dof3.DataID, fieldId = dof3.FieldID } into dofs3
from dof3 in dofs3.DefaultIfEmpty()
select new WorkitemListModel
{
InstanceId = i.ObjectID,
FormFieldValue1 = dof1.FieldValue,
FormFieldValue2 = dof2.FieldValue,
FormFieldValue3 = dof3.FieldValue,
};
I have defined the following classes:
public class WorkitemListModel
{
public string InstanceId { get; set; }
public string FormFieldValue1 { get; set; }
public string FormFieldValue2 { get; set; }
public string FormFieldValue3 { get; set; }
}
public class DataObjectField
{
public string ProcessInstanceObjectID { get; set; }
public string DataID { get; set; }
public string FieldID { get; set; }
public string FieldValue { get; set; }
}
public class ModelWithFields<TModel>
{
public IEnumerable<DataObjectField> DataObjectFields { get; set; }
public TModel Model { get; set; }
}
public class OuterKeySelector
{
public string instanceId { get; set; }
public string dataId { get; set; }
public string fieldId { get; set; }
}
I created the GroupJoin expression wich gives no compile errors. (I left out som code here):
var q = dc.Instances;
System.Type modelType = typeof(ModelWithFields<>);
System.Type outerKeyType = typeof(WorkitemListModel);
System.Type resultType = modelType.MakeGenericType(outerKeyType);
//... MemberInitExpression and Expression.Bind
q = q.Provider.CreateQuery(
Expression.Call(
typeof(Queryable),
"GroupJoin",
new[]
{
typeof(WorkitemListModel),
typeof(DataObjectField),
typeof(OuterKeySelector),
resultType,
},
query.Expression,
Expression.Constant(Queries.DataObjectFieldsQuery(dc)),
Expression.Quote(outerLambda),
Expression.Quote((Expression<Func<DataObjectField,OuterKeySelector>>)(
(DataObjectField dof) =>
new OuterKeySelector
{
instanceId = dof.ProcessInstanceObjectID,
dataId = dof.DataID,
fieldId = dof.FieldID
})),
Expression.Quote(resultLambda)));
// selectmany expression
// collectionSelector lambda -- temp.DataObjectFields.DefaultIfEmpty()
ParameterExpression collectionParameter = Expression.Parameter(resultType, "temp");
// This throw an exception
MethodCallExpression collectionCallExpression =
Expression.Call(
typeof(Queryable),
"DefaultIfEmpty",
new System.Type[]
{
typeof(IQueryable<>).MakeGenericType(typeof(DataObjectField))
},
Expression.Property(collectionParameter, resultType.GetProperty("DataObjectFields")));
But in the SelectMany method I'm trying to add DefaultIfEmpty but I get an exception saying:
No generic method 'DefaultIfEmpty' on
type 'System.Linq.Queryable' is
compatible with the supplied type
arguments and arguments. No type
arguments should be provided if the
method is non-generic.
I've tried the switched the typeparams from IQueryable to IEnumerable and event tried to call Enumerable.DefaultIfEmpty with no luck. Perhaps it's something wrong with the PropertyExpression?

I prefer a different overload for Expression.Call using MethodInfo, here is a simple example I have working.
Expression constant = Expression.Constant(new string[] { "a", "b" });
MethodInfo methodInfo = typeof(Enumerable).GetMethods().FirstOrDefault(c => (c as MethodInfo).Name == "DefaultIfEmpty");
methodInfo = methodInfo.MakeGenericMethod(typeof(string));
MethodCallExpression methodExpression = Expression.Call(methodInfo, constant);

I had defined incorrect type parameter of the Expression.Call method. It wasn't IQueryable<DataObjectField> but only DataObjectField. This is what fixed it.
MethodCallExpression collectionCallExpression =
Expression.Call(
typeof(Enumerable),
"DefaultIfEmpty",
new System.Type[]
{
typeof(DataObjectField)
},
Expression.Property(collectionParameter, newResultType.GetProperty("DataObjectFields"))

Related

Linq expression filter in IQueryable

I want to create generic expression filtering in IQueryable
public class Vehicle
{
public int Id { get; set; }
public string VehicleNO { get; set; }
public int DriverId { get; set; }
public Driver Driver {get;set;}
}
public class Driver
{
public int Id { get; set; }
public string Name { get; set; }
}
operator = "Contain", field name = "Driver.Name", Value filter =
"Micheal"
I don't know how to filter driver name.
Here is my full code
IQueryable<SysClientSiteUser> query = entity.SysClientSiteUsers.Include(i => i.SysClientSiteRole);
Dictionary<string, string> dtFilter = new Dictionary<string, string>();
dtFilter.Add("VehicleNo", "A123");
Dictionary<Type, Func<string, object>> lookup = new Dictionary<Type, Func<string, object>>();
lookup.Add(typeof(string), x => { return x; });
lookup.Add(typeof(long), x => { return long.Parse(x); });
lookup.Add(typeof(int), x => { return int.Parse(x); });
lookup.Add(typeof(double), x => { return double.Parse(x); });
var paramExpr = Expression.Parameter(typeof(Vehicle), "VehicleNo");
var keyPropExpr = Expression.Property(paramExpr, "VehicleNo");
if (!lookup.ContainsKey(keyPropExpr.Type))
throw new Exception("Unknown type : " + keyPropExpr.Type.ToString());
var typeDelegate = lookup[keyPropExpr.Type];
var constantExp = typeDelegate("A123");
var eqExpr = Expression.Equal(keyPropExpr, Expression.Constant(constantExp));
var condExpr = Expression.Lambda<Func<SysClientSiteUser, bool>>(eqExpr, paramExpr);
query = query.Where(condExpr);
for normal field, it's working. But if I want to call Driver name. it's not work. How to call "Driver.Name"?
You can use a helper function to convert a nested property name string to an Expression that accesses that property for a given ParameterExpression and type:
private static Expression MakePropertyExpression<T>(string propertyName, Expression baseExpr) =>
propertyName.Split('.').Aggregate(baseExpr, (b, pname) => Expression.Property(b, pname));

C# Create Lambda Expression Dynamically

I work with Dapper and I try to create auto-mapped method for inner join.
This is the example of the models:
public class User
{
public long IdUser { get; set; }
public string Email { get; set; }
}
public class Page
{
public long Id { get; set; }
public string Name { get; set; }
public long IdUserCreatedPage { get; set; }
public User UserCreatedPage { get; set; }
}
This is the query:
SELECT * FROM "PAGE" INNER JOIN "USER" ON "PAGE"."IdUserCreatedPage" = "USER"."IdUser"
if I write code mannualy I will write this:
public List<Page> GetPage(IDbConnection dbConnection, string sql)
{
return (List<Page>)dbConnection.Query<Page, User, Page>(sql,
(Page p, User u) =>
{
p.UserCreatedPage = u;
return p;
},
splitOn: "IdUser").ToList();
}
Now, what I want is create dynamically the Func<TFirst, TSecond, TOut> that I need for mapping the object.
Can someone help me? Thank you very much.
P.S. I know that in this case it does not make sense create it dynamically, but this it's only a simply version of all the project of auto-mapping.
SOLUTION
Finally I find the way to do what I want.
This is the code of the function that Generate Func<TFirst, TSecond, TOut>:
public static Func<TFirst, TSecond, TFirst> MappingDynamicFunc<TFirst, TSecond>()
{
ParameterExpression paramFirst = Expression.Parameter(typeof(TFirst), "paramFirst");
ParameterExpression paramSecond = Expression.Parameter(typeof(TSecond), "paramSecond");
MemberExpression memberExpression = Expression.PropertyOrField(paramFirst, "UserCreatedPage");
BinaryExpression assign = Expression.Assign(memberExpression, paramSecond);
LabelTarget labelTarget = Expression.Label(typeof(TFirst));
GotoExpression returnExpression = Expression.Return(labelTarget, paramFirst, typeof(TFirst));
LabelExpression labelExpression = Expression.Label(labelTarget, Expression.Default(typeof(TFirst)));
BlockExpression block = Expression.Block(
assign,
returnExpression,
labelExpression
);
return Expression.Lambda<Func<TFirst, TSecond, TFirst>>(block, new ParameterExpression[] { paramFirst, paramSecond }).Compile();
}
And this is the "GetPage" method:
public List<Page> GetPage(IDbConnection dbConnection, string sql)
{
return (List<Page>)dbConnection.Query<Page, User, Page>(sql,
MappingDynamicFunc<Page, User>(),
splitOn: "IdUser").ToList();
}
Take a look at PredicateBuilder. http://www.albahari.com/nutshell/predicatebuilder.aspx
Here is some pseudo code
var predicate = PredicateBuilder.True<SomeClass>();
if (SomeCondition)
{
var inner = PredicateBuilder.False<SomeClass>();
inner = inner.Or(p => p.Category == "WhatEver");
inner = inner.Or(p => p.Category == "");
predicate = predicate.And(inner);
}
...
var result = MyIEnumerable<SomeClass>.AsQueryable()
.Where(predicate)
.FirstOrDefault();

LINQ to SQL select property name by string on projection

How can I achieve the projection on the last select? I need the property defined by the string prop.Name to be selected into the SeriesProjection object.
public override IQueryable<SeriesProjection> FilterOn(string column)
{
//Get metadata class property by defined Attributes and parameter column
var prop = typeof(CommunicationMetaData)
.GetProperties()
.Single(p => p.GetCustomAttribute<FilterableAttribute>().ReferenceProperty == column);
var attr = ((FilterableAttribute)prop.GetCustomAttribute(typeof(FilterableAttribute)));
var param = Expression.Parameter(typeof(Communication));
Expression conversion = Expression.Convert(Expression.Property(param, attr.ReferenceProperty), typeof(int));
var condition = Expression.Lambda<Func<Communication, int>>(conversion, param); // for LINQ to SQl/Entities skip Compile() call
var result = DbQuery.Include(prop.Name)
//.GroupBy(c => c.GetType().GetProperty(attr.ReferenceProperty))
.GroupBy(condition)
.OrderByDescending(g => g.Count())
.Select(group => new SeriesProjection()
{
Count = group.Count(),
Id = group.Key,
//set this navigation property dynamically
Name = group.FirstOrDefault().GetType().GetProperty(prop.Name)
});
return result;
}
For the GroupBy I used the fk property name that's always an int on the Communication entity, but for the select I can't figure out the expression.
[EDIT]
System.Data.Entity.Infrastructure.DbQuery<Communication> DbQuery;
---
[MetadataType(typeof(CommunicationMetaData))]
public partial class Communication
{
public int CommunicationId { get; set; }
public Nullable<int> TopicId { get; set; }
public int CreateById { get; set; }
public virtual Employee CreateByEmployee { get; set; }
public virtual Topic Topic { get; set; }
}
---
public class CommunicationMetaData
{
[Filterable("By Employee", nameof(Communication.CreateById))]
public Employee CreateByEmployee { get; set; }
[Filterable("By Topic", nameof(Communication.TopicId))]
public Topic Topic { get; set; }
}
---
[AttributeUsage(AttributeTargets.Property)]
public class FilterableAttribute : System.Attribute
{
public FilterableAttribute(string friendlyName, string referenceProperty)
{
FriendlyName = friendlyName;
ReferenceProperty = referenceProperty;
}
public string FriendlyName { get; set; }
public string ReferenceProperty { get; set; }
}
---
public class SeriesProjection
{
public int Count { get; set; }
public int Id { get; set; }
public object Name { get; set; }
}
Without some expression helper library, you have to build the whole selector expression manually.
The input of the selector will be a parameter of type IGrouping<int, Communication>, the result type - SeriesProjection, and the body will be MemberInit expression:
var projectionParameter = Expression.Parameter(typeof(IGrouping<int, Communication>), "group");
var projectionType = typeof(SeriesProjection);
var projectionBody = Expression.MemberInit(
// new SeriesProjection
Expression.New(projectionType),
// {
// Count = group.Count(),
Expression.Bind(
projectionType.GetProperty(nameof(SeriesProjection.Count)),
Expression.Call(typeof(Enumerable), "Count", new[] { typeof(Communication) }, projectionParameter)),
// Id = group.Key
Expression.Bind(
projectionType.GetProperty(nameof(SeriesProjection.Id)),
Expression.Property(projectionParameter, "Key")),
// Name = group.FirstOrDefault().Property
Expression.Bind(
projectionType.GetProperty(nameof(SeriesProjection.Name)),
Expression.Property(
Expression.Call(typeof(Enumerable), "FirstOrDefault", new[] { typeof(Communication) }, projectionParameter),
prop.Name))
// }
);
var projectionSelector = Expression.Lambda<Func<IGrouping<int, Communication>, SeriesProjection>>(projectionBody, projectionParameter);
and then of course use simply:
var result = DbQuery.Include(prop.Name)
.GroupBy(condition)
.OrderByDescending(g => g.Count())
.Select(projectionSelector);

Dynamic Selector Linq To Entities

I have a dynamic selector expression that produces anonymous type. It's working fine in linq to objects, but in linq to entities, it throws:
Attempt 1
NotSupportedException
Only parameterless constructors and initializers are supported in LINQ to Entities.
Expression<Func<User, T>> DynamicSelect<T>(T obj, ParameterExpression userParam)
{
var newExpression = Expression.New(
typeof(T).GetConstructor(typeof(T).GenericTypeArguments),
userParam,
Expression.Constant("X"),
Expression.Constant("Y")
);
return Expression.Lambda<Func<User, T>>(newExpression, userParam);
}
var userParam = Expression.Parameter(typeof(User), "u");
var obj = new { User = new User(), Address = string.Empty, Fax = string.Empty };
var arr = context.Set<T>()
.Select(DynamicSelect(obj, userParam))
.ToArray();
Attempt 2, If I create a custom type, it's working, but I don't want to, because I want to reuse this helper method without creating additional custom type for each entity, I want to be able to pass the type based on consumer.
public class Container
{
public User User { get; set; }
public string Address { get; set; }
public string Fax { get; set; }
}
Expression<Func<User, T>> DynamicSelect<T>(T obj, ParameterExpression userParam)
{
var initExpression = Expression.MemberInit(
Expression.New(typeof(T)),
Expression.Bind(typeof(T).GetProperty("User"), userParam),
Expression.Bind(typeof(T).GetProperty("Address"), Expression.Constant("X")),
Expression.Bind(typeof(T).GetProperty("Fax"), Expression.Constant("Y"))
);
return Expression.Lambda<Func<User, T>>(initExpression, userParam);
}
var userParam = Expression.Parameter(typeof(User), "u");
var arr = context.Set<T>()
.Select(DynamicSelect<Container>(null, userParam))
.ToArray();
Attempt 3, I also tried using Tuple<User, string, string>, but it's not supported too.
NotSupportedException
LINQ to Entities does not recognize the method
'System.Tuple`3[User,System.String,System.String]
Create[User,String,String](User, System.String, System.String)'
method, and this method cannot be translated into a store expression.
Expression<Func<User, T>> DynamicSelect<T>(T obj, ParameterExpression userParam)
{
var createExpression = Expression.Call(
typeof(Tuple),
"Create",
typeof(T).GenericTypeArguments,
userParam,
Expression.Constant("X"),
Expression.Constant("Y"));
return Expression.Lambda<Func<User, T>>(createExpression, userParam);
}
var userParam = Expression.Parameter(typeof(User), "u");
var arr = context.Set<User>()
.Select(DynamicSelect<Tuple<User, string, string>>(null, userParam))
.ToArray();
Please help.
update
I was trying to reuse this helper method in any consumer (User, Customer, Associate, etc) without having specific implementation to each consumer.
The class structure look like.
public class User
{
public int Id { get; set; }
public string UserName { get; set; }
public virtual ICollection<Contact> Contacts { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string CompanyName { get; set; }
public virtual ICollection<Contact> Contacts { get; set; }
}
public class Contact
{
public int Id { get; set; }
public string Type { get; set; }
public string Content { get; set; }
}
public class UserDto
{
public int Id { get; set; }
public string UserName { get; set; }
public ContactDto Contact { get; set; }
}
public class CustomerDto
{
public int Id { get; set; }
public string CompanyName { get; set; }
public ContactDto Contact { get; set; }
}
public class ContactDto
{
public string Email { get; set; }
public string Address { get; set; }
public string Fax { get; set; }
// other contact informations
}
And I have many contacts that could be different for each consumer.
var users = context.Set<User>()
.Select(x => new UserDto
{
Id = x.Id,
UserName = x.UserName,
Contact = new ContactDto
{
Email = x.Contacts.Where(c => c.Type == "Email").Select(c => c.Value).FirstOrDefault()
}
})
.ToArray();
var customers = context.Set<Customer>()
.Select(x => new CustomerDto
{
Id = x.Id,
CompanyName = x.CompanyName,
Contact = new ContactDto
{
Address = x.Contacts.Where(c => c.Type == "Address").Select(c => c.Value).FirstOrDefault(),
Fax = x.Contacts.Where(c => c.Type == "Fax").Select(c => c.Value).FirstOrDefault(),
}
})
.ToArray();
And have refactored the x.Contacts.Where(c => c.Type == "Address").Select(c => c.Value).FirstOrDefault() into expression, but I can't use it directly inside the method like:
var users = context.Set<User>()
.Select(x => new UserDto
{
Id = x.Id,
UserName = x.UserName,
Contact = new ContactDto
{
Email = GetContactExpression("Email").Compile()(x)
}
})
.ToArray();
It will throw error because Invoke method is not supported in linq to expression, so that I need to refactored the whole Select expression, but I need to make it generic (User has UserName, but Customer has CompanyName, and any other information) and probably passing the contact type(s) too after this get solved. The expected result at the moment would be something lile:
var obj = new { User = new User(), Email = "" };
var users = context.Set<User>()
.Select(x => DynamicSelect(obj))
.Select(x => new UserDto
{
Id = x.User.Id,
UserName = x.User.UserName,
Contact = new ContactDto
{
Email = x.Email
}
})
.ToArray();
When I compare your expression with the one created by the compiler for something like u => new { User = u, Address = "X", Fax = "Y" }, the difference is that the latter has filled Members.
I don't quite understand what is the reason for Members to exist at all, but I would try to set it, my guess is that it will fix your problem. The code might look something like:
var constructor = typeof(T).GetConstructor(typeof(T).GenericTypeArguments);
var newExpression = Expression.New(
constructor,
new Expression[] { userParam, Expression.Constant("X"), Expression.Constant("Y") },
constructor.GetParameters().Select(p => typeof(T).GetProperty(p.Name)));

Better way to write this linq query Pt2

In response to this question:
Better way to write this linq query?
How would i build a dynamic query following the same pattern in that thread?
For example, the signature of the method changes to:
public List<PeopleSearchList> GetPeople(string filter, string searchType, string searchOption)
{
return a new List of type PeopleSearchList
}
So now i am not returning a single array of "Firstnames" etc i am returning a custom class.
The class would look like this:
public class PeopleSearchList
{
public String IdentityCode { get; set; }
public String Firstname { get; set; }
public String Surname { get; set; }
public Int32 LoanCount { get; set; }
public String Group { get; set; }
}
I worked it out.
Just thought i would post the solution for others to see.
public List<PeopleSearchList> GetPeople(string filter, string searchType, string searchOption)
{
IQueryable<Person> query = _context.People;
PropertyInfo property = typeof (Person).GetProperty(searchType);
MethodInfo method = typeof (string).GetMethod(searchOption, new[] {typeof (string)});
query = query.Where(WhereExpression(property, method, filter));
IQueryable<PeopleSearchList> resultQuery = query.Select(p => new PeopleSearchList
{
Firstname = p.Firstname,
Group = p.Level.Year,
IdentityCode = p.IdentityCode,
LoanCount = p.Loans.Count(),
Surname = p.Surname
}
).OrderBy(p => p.Surname);
return resultQuery.ToList();
}
Expression<Func<Person, bool>> WhereExpression(PropertyInfo property, MethodInfo method, string filter)
{
var param = Expression.Parameter(typeof(Person), "o");
var propExpr = Expression.Property(param, property);
var methodExpr = Expression.Call(propExpr, method, Expression.Constant(filter));
return Expression.Lambda<Func<Person, bool>>(methodExpr, param);
}

Categories

Resources