Dynamically selecting fields in a linq query - c#

I have a linq query where I’d like to dynamically select only the fields requested by my user.
Currently I’m mapping my Jobs to a data transformation object like this:
var jobs = (from p in jobsDB
select new JobReportDTO()
{
JobID = p.JobID,
EventType = p.EventType,
DateApproved = p.ApprovedDate,
DateEntered = p.EnteredDate,
DateClosed = p.ClosedDate,
StartDate = p.StartDate,
FinishDate = p.FinishDate,
InsuredName = p.InsuredName,
StreetAddress = p.StreetAddress,
Suburb = p.Suburb,
State = p.State,
Postcode = p.Postcode,
.... etc
Within this function I have a number of boolean variables that identify whether that field should be sent to the view, i.e.:
public bool ShowInsuredName { get; set; }
public bool ShowSuburb { get; set; }
public bool ShowICLA { get; set; }
public bool ShowClaimNumber { get; set; }
public bool ShowFileMananger { get; set; }
public bool ShowSupervisor { get; set; }
public bool ShowStatus { get; set; }
... etc
How can I modify my linq query to show selected fields only?
I’ve tried
var jobs = (from p in jobsDB
select new JobReportDTO()
{
JobID = p.JobID,
jobReport.ShowEventType == true ? EventType = p.EventType : "",
... etc
But am getting “invalid initialiser member declarator”

If you can afford LINQ method syntax and use strong naming convention for options like public bool Show{DTOPropertyName} { get; set; }, then you can make your life much easier with the help of the System.Linq.Expressions and the following little helper method
public static class MyExtensions
{
public static IQueryable<TResult> Select<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector, object options)
{
var memberInit = (MemberInitExpression)selector.Body;
var bindings = new List<MemberBinding>();
foreach (var binding in memberInit.Bindings)
{
var option = options.GetType().GetProperty("Show" + binding.Member.Name);
if (option == null || (bool)option.GetValue(options)) bindings.Add(binding);
}
var newSelector = Expression.Lambda<Func<TSource, TResult>>(
Expression.MemberInit(memberInit.NewExpression, bindings), selector.Parameters);
return source.Select(newSelector);
}
}
What it does is to remove the assignments which has associated ShowProperty with value set to false.
The usage is simple
var jobs = jobsDB.Select(p => new JobReportDTO
{
JobID = p.JobID,
EventType = p.EventType,
DateApproved = p.ApprovedDate,
DateEntered = p.EnteredDate,
DateClosed = p.ClosedDate,
StartDate = p.StartDate,
FinishDate = p.FinishDate,
InsuredName = p.InsuredName,
StreetAddress = p.StreetAddress,
Suburb = p.Suburb,
State = p.State,
Postcode = p.Postcode,
.... etc
}, jobReport);
If you set a breakpoint in the debugger and examine the newSelector variable, you'll see that only properties that do not have ShowProperty (like JobID) or have ShowProperty = true are included.

Try this way:
EventType = jobReport.ShowEventType == true ? p.EventType : string.Empty,

Related

Linq expression orderBy on child collections

I've got this class with a collection of revisions:
public class Client : BaseEntity
{
public virtual ICollection<Draw> Draws { get; set; }
public virtual ICollection<ClientRevision> ClientRevisions { get; set; }
}
public class ClientRevision : BaseEntity
{
public Guid ClientId { get; set; }
public Client Client { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public double? Increase { get; set; }
public string RevisionCode { get; set; }
public string RevisionNote { get; set; }
}
On controller, I'd like to retrieve the list of clients with the last revision available. I've got a base repository with the following get method:
public async Task<IEnumerable<T>> GetAsync(
Expression<Func<T, bool>> filter = null,
Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,
IEnumerable<Expression<Func<T, object>>> includeProperties = null,
int? pageIndex = null,
int? itemsPerPage = null,
bool ignoreQueryFilter = true,
bool ignoreTracking = true)
{
var query = DataContext.GetData<T>(ignoreTracking);
if (filter != null)
{
query = query.Where(filter);
}
if (orderBy != null)
{
query = orderBy(query);
}
if (ignoreQueryFilter)
{
query = query.IgnoreQueryFilters();
}
query = ApplyIncludePropertiesIfNeeded(includeProperties, query);
if (pageIndex.HasValue && itemsPerPage.HasValue)
{
query = query
Skip(pageIndex.Value * itemsPerPage.Value)
Take(itemsPerPage.Value + 1);
}
return await query.ToListAsync();
}
I'd like to order by the list by the Name of the client; the name is in the Client Revision. I try to implement the following filter and orderby, but I cannot create a correct linq expression for retrive the Name of the Client.
public async Task<ActionResult<ClientResponse>> GetClientListAsync()
{
var result = new List<ClientResponse>();
List<Expression<Func<Client, object>>> includes = new() { i => i.Draws };
var sortOn = "ClientRevisions.Name";
var param = Expression.Parameter(typeof(Client), "client");
var parts = sortOn.Split('.');
Expression parent = param;
foreach (var part in parts)
{
parent = Expression.Property(parent, part);
}
var sortExpression = Expression.Lambda<Func<Client, object>>(parent, param);
Func<IQueryable<Client>, IOrderedQueryable<Client>> order = o => o.OrderBy(sortExpression);
}
The error is the following:
Instance property 'Name' is not defined for type 'System.Collections.Generic.ICollection`1[ClientRevision]' (Parameter 'propertyName')
Because is a list. How to retrieve this data from a list? Thanks

Orderby on left join null value

Following situation:
Table of users
Table of addresses
The user has a single optional reference to the address table (=left join)
The query to fetch the data is:
IQueryable<User> query =
from u in _dbContext.Users
join a in _dbContext.Address on u.AddressId equals a.Id into address
from addresses in address.DefaultIfEmpty()
select new User(u, a);
Now I want to do a sorting on the query based on the municipality of the address
query = query.OrderBy(u => u.Address.Municipality);
The problem is that the Address can be a null value (as the address is optional) and for that reason it is throwing following exception.
NullReferenceException: Object reference not set to an instance of an object.
Is there a way to order on the municipality knowing that it can be null?
Already tried following things with same outcome:
query = query.OrderBy(u => u.Address.Municipality ?? "");
query = query.OrderBy(u => u.Address == null).ThenBy(u => u.Address.Municipality);
You can use
query = query.OrderBy(u => u.Address.Municipality.HasValue);
You can write your comparer like this:
public class One
{
public int A { get; set; }
}
public class Two
{
public string S { get; set; }
}
public class T
{
public One One { get; set; }
public Two Two { get; set; }
}
public class OrderComparer : IComparer<Two>
{
public int Compare(Two x, Two y)
{
if (((x == null) || (x.S == null)) && ((y == null) || (y.S == null)))
return 0;
if ((x == null) || (x.S == null))
return -1;
if ((y == null) || (y.S == null))
return 1;
return x.S.CompareTo(y.S);
}
}
static void Main(string[] args)
{
var collection = new List<T> {
new T{ One = new One{A=1}, Two = new Two{ S="dd" } },
new T{ One = new One{A=5}, Two = null },
new T{ One = new One{A=0}, Two = new Two{ S=null } },
new T{ One = new One{A=6}, Two = new Two{ S="aa" } },
};
var comparer = new OrderComparer();
collection = collection.OrderBy(e => e.Two, comparer).ToList();
}
But in your case you have to write var collection = query.AsEnumerable().OrderBy(x=>x.Address).
Also there is other method:
var result = query.Where(x=>x.Address==null || x.Address.Municipality==null)
.Union(query.Where(x.Address!=null && x.Address.Municipality!=null).OrderBy(x=>x.Address.Municipality));
I create a simple demo and it works well when I add nullable foreign key to the two tables.
Besides, I do not understabd what is select new User(u, a); in your code.
Below is my sample code:
Models:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
[ForeignKey("Address")]
public int? AddressId { get; set; }
public Address Address { get; set; }
}
public class Address
{
public int Id { get; set; }
public string AddressName { get; set; }
public string Municipality { get; set; }
}
Action:
IQueryable<User> query =
from u in _context.Users
join a in _context.Address on u.AddressId equals a.Id into address
from addresses in address.DefaultIfEmpty()
select new User
{
Id = u.Id,
Name = u.Name,
AddressId = u.AddressId,
Address = addresses
};
query = query.OrderBy(u => u.Address.Municipality);

Cannot implicitly convert type IQueryable to class

I am trying to get multiple results into a list to send back to JS to populate a grid. The first query (_mappedQuery) is getting data. I then want to end up putting the values into the _udfList object. I keep getting variances on the error 'cannot convert queryable to class'
I have tried setting as lists, creating query objects, single class objects. All no luck so far
MemberMNCFormsList _udfList = new MemberMNCFormsList();
foreach (var _row in _udfTables) {
System.Diagnostics.Debugger.Break();
System.Diagnostics.Debugger.Launch();
var _mappedQuery = (from res in Query<UdfColumnMapping>().AsNoTracking()
join udf in Query<UserDefinedForms>().AsNoTracking() on res.Func_Area equals udf.Func_Area
join ds in Query<Data_Set>().AsNoTracking() on res.Data_ID equals ds.DATA_ID
join df in Query<DEFITEM>().AsNoTracking() on ds.DEF_ID equals df.DEF_ID
where udf.UserDefinedForms_ID == _row.UserDefinedForms_ID &&
(res.FieldName.ToLower().StartsWith("reviewname") ||
res.FieldName.ToLower().StartsWith("disposition") ||
res.FieldName.ToLower().StartsWith("reviewdate"))
select (new MemberMNCForms {
UserDefinedFormData_ID = _row.UserDefinedFormData_ID,
FormId = udf.UserDefinedForms_ID,
MappedColumnName = res.MappedColumnName,
FieldName = res.FieldName,
MappedTableName = res.MappedTableName,
Reviewed_Name = _row.LAST_NAME.Trim() + ", " + _row.FIRST_NAME.Trim(),
Reviewed_Date = _row.CreateDate.GetShortDateorEmpty().ToString()
}));
var _formRow = _mappedQuery.Select(t => new MemberMNCForms {
UserDefinedFormData_ID = t.UserDefinedFormData_ID,
FormId = t.FormId,
MappedColumnName = t.MappedColumnName,
FieldName = t.FieldName,
MappedTableName = t.MappedTableName,
Reviewed_Name = t.Reviewed_Name,
Reviewed_Date = t.Reviewed_Date
})));
_udfList.list.Add(_formRow);
public sealed class MemberMNCForms {
public Guid? UserDefinedFormData_ID { get; set; }
public int FormId { get; set; }
public string Reviewed_Name { get; set; }
public string MappedColumnName { get; set; }
public string FieldName { get; set; }
public string MappedTableName { get; set; }
public int? MNCDetermination_ID { get; set; }
public string Reviewed_By { get; set; }
public string Reviewed_Date { get; set; }
}
public sealed class MemberMNCFormsList : ErrorInfo
{
public List<MemberMNCForms> list = new List<MemberMNCForms>();
public int Count { get; set; }
}
I am trying to get the _udfList object populated with the values coming from _mappedQuery. The only thing I thought would work was to create a MemberMNCForms object for each record in _mappedQuery to then add to _udfList.list
_formRow is an IEnumerable<MemberMNCForms>
var _formRow = _mappedQuery.Select(t => new MemberMNCForms {
UserDefinedFormData_ID = t.UserDefinedFormData_ID,
FormId = t.FormId,
MappedColumnName = t.MappedColumnName,
FieldName = t.FieldName,
MappedTableName = t.MappedTableName,
Reviewed_Name = t.Reviewed_Name,
Reviewed_Date = t.Reviewed_Date
})));
Here you are trying to add an IEnumerable<MemberMNCForms> to a List<MemberMNCForms>
_udfList.list.Add(_formRow);
You can't do this with .Add. You have to use .AddRange
Try this:
_udfList.list.AddRange(_formRow);
When you use linq like that, even if there is a single item in the list that you are Selecting on, it is just an expression tree until it is iterated on.
I assume that you are expecting a collection of MemberMNCForms back so you would need use AddRange instead of Add
_udfList.list.AddRange(_formRow);
To make sure that it has been executed, you can use ToList
_udfList.list.AddRange(_formRow.ToList());
If you are just expecting a single result, you can use SingleOrDefault.
var result = _formRow.SingleOrDefault();
if (result != null) {
_udfList.list.Add(result);
}

Create predicate with nested classes with Expression

I have this :
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
}
public class City
{
public int Id { get; set; }
public string Name { get; set; }
public int ZipCode { get; set; }
}
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int? Age { get; set; }
public City City { get; set; }
public Company Company { get; set; }
}
I'd like a some case generate the predicate like this :
var result = listPerson.Where(x => x.Age == 10).ToList<>();
Or this :
var result = listPerson.Where( x => x.Company.Name == 1234).ToList();
Or this :
var result = listPerson.Where( x => x.City.ZipCode == "MyZipCode").ToList();
Or this :
var result = listPerson.Where( x => x.Company.Name == "MyCompanyName").ToList();
Then I created a "PredicateBuilder", that's work (I get the type, if nullable or not and I build the predicate) when I do this :
BuildPredicate<Person>("Age", 10); I get this : x => x.Age == 10
But I don't how manage when there is an nested property like this :
BuildPredicate<Person>("City.ZipCode", "MyZipCode");
I'd like get this : x => x.City.ZipCode == "MyZipCode"
Or this :
BuildPredicate<Person>("City.Name", "MyName");
I'd like get this : x => x.City.Name == "MyName"
Or this :
BuildPredicate<Person>("Company.Name", "MyCompanyName");
I'd like get this : x => x.Company.Name == "MyCompanyName"
(not intending to duplicate Jon - OP contacted me to provide an answer)
The following seems to work fine:
static Expression<Func<T,bool>> BuildPredicate<T>(string member, object value) {
var p = Expression.Parameter(typeof(T));
Expression body = p;
foreach (var subMember in member.Split('.')) {
body = Expression.PropertyOrField(body, subMember);
}
return Expression.Lambda<Func<T, bool>>(Expression.Equal(
body, Expression.Constant(value, body.Type)), p);
}
The only functional difference between that and Jon's answer is that it handles null slightly better, by telling Expression.Constant what the expected type is. As a demonstration of usage:
static void Main() {
var pred = BuildPredicate<Person>("City.Name", "MyCity");
var people = new[] {
new Person { City = new City { Name = "Somewhere Else"} },
new Person { City = new City { Name = "MyCity"} },
};
var person = people.AsQueryable().Single(pred);
}
You just need to split your expression by dots, and then iterate over it, using Expression.Property multiple times. Something like this:
string[] properties = path.Split('.');
var parameter = Expression.Parameter(typeof(T), "x");
var lhs = parameter;
foreach (var property in properties)
{
lhs = Expression.Property(lhs, property);
}
// I've assumed that the target is a string, given the question. If that's
// not the case, look at Marc's answer.
var rhs = Expression.Constant(targetValue, typeof(string));
var predicate = Expression.Equals(lhs, rhs);
var lambda = Expression.Lambda<Func<T, bool>>(predicate, parameter);

Nested Generic Lambdas in LINQ

I'm driving myself crazy trying to understand Expressions in LINQ. Any help is much appreciated (even telling me that I'm totally off base here).
Let's say I have three classes
public class Person
{
public string Name { get; set;}
public IEnumerable<PersonLocation> Locations { get; set;}
public IEnumerable<PersonEducation> Educations { get; set:}
}
public class PersonLocation
{
public string Name { get; set;}
public string Floor { get; set;}
public string Extension { get; set;}
}
public class PersonEducation
{
public string SchoolName { get; set;}
public string GraduationYear { get; set;}
}
I'm trying to create a method that takes in a string, such as Locations.Name or Locations.Floor, or Educations.SchoolName which will then create a dynamic linq query
IEnumerable<Person> people = GetAllPeople();
GetFilteredResults(people, "Location.Name", "San Francisco");
GetFilteredResults(people, "Location.Floor", "17");
GetFilteredResults(people, "Educations.SchoolName", "Northwestern");
This GetFilteredResults(IEnumerable<Person> people, string ModelProperty, string Value) method should create an expression that is roughly equivalent to people.Where(p => p.Locations.Any(pl => pl.Name == Value);
I have this working if ModelProperty is a string, i.e. people.Where(p => p.Name == Value) looks like this:
string[] modelPropertyParts = ModelProperty.Split('.');
var prop = typeof(Person).GetProperty(modelPropertyParts[0]);
var sourceParam = Expression.Parameter(typeof(Person), "person");
var expression = Expression.Equal(Expression.PropertyOrField(sourceParam, modelPropertyParts[0]), Expression.Constant(option.Name));
var whereSelector = Expression.Lambda<Func<Person, bool>>(orExp, sourceParam);
return people.Where(whereSelector.Compile());
Here's what I have been playing around with for an IEnumerable type, but I just can't get the inner Any, which seems correct, hooked into the outer Where:
/*i.e. modelPropertyParts[0] = Locations & modelPropertyParts[1] = Name */
string[] modelPropertyParts = ModelProperty.Split('.');
var interiorProperty = prop.PropertyType.GetGenericArguments()[0];
var interiorParameter = Expression.Parameter(interiorProperty, "personlocation");
var interiorField = Expression.PropertyOrField(interiorParameter, modelPropertyParts[1]);
var interiorExpression = Expression.Equal(interiorField, Expression.Constant(Value));
var innerLambda = Expression.Lambda<Func<PersonLocation, bool>>(interiorExpression, interiorParameter);
var outerParameter = Expression.Parameter(typeof(Person), "person");
var outerField = Expression.PropertyOrField(outerParameter, modelPropertyParts[0]);
var outerExpression = ??
var outerLambda == ??
return people.Where(outerLambda.Compile());
The problem is that System.Linq.Enumerable.Any is a static extension method.
Your outerExpression must reference System.Linq.Enumerable.Any(IEnumerable<T>, Func<T, bool>):
var outerExpression = Expression.Call(
typeof(System.Linq.Enumerable),
"Any",
new Type[] { outerField.Type, innerLambda.Type },
outerField, innerLambda);
Take a look at these links for more information:
MSDN Expression.Call(Type, String, Type[], params Expression[])
Some helpful, similar examples.

Categories