I am trying to get MemberInfo for a child property from a MemberExpression. I have found ways to get the full name of the nested type, but not a way to get the whole MemberInfo of the nested type. Here is a quick example of the scenario I am talking about:
Some simple models (the goal is to eventually get the MemberInfo of the Data property of the Child class)
public class Parent
{
public int ParentProperty { get; set; }
public Child Child { get; set; }
}
public class Child
{
public string Data { get; set; }
}
The lambda expression
Expression<Func<Parent,string>> func = new Func<Parent, string>(p =>
{
return p.Child.Data;
});
Code used to get the MemberInfo from the lambda expression.
internal static MemberInfo FindMemberInfoFromLambda(LambdaExpression lambda)
{
var expression = (Expression) lambda;
var flag = false;
while (!flag)
{
switch (expression.NodeType)
{
case ExpressionType.Convert:
expression = ((UnaryExpression) expression).Operand;
continue;
case ExpressionType.Lambda:
expression = ((LambdaExpression) expression).Body;
continue;
case ExpressionType.MemberAccess:
var memberExpression = (MemberExpression) expression;
if (memberExpression.Expression.NodeType == ExpressionType.Parameter ||
memberExpression.Expression.NodeType == ExpressionType.Convert)
return memberExpression.Member;
throw new Exception();
default:
flag = true;
continue;
}
}
throw new Exception();
}
This code works great if I were trying to get the ParentProperty of the Parent class, but when I try to get the MemberInfo of the Data property of the Child class, it does not work. I have seen a few StackOverflow questions posted on getting the full name of the child property, but nothing on getting the whole MemberInfo of it. Has anyone done this before or can help point me in the right direction?
The expression you get is MemberExpression, you can grab its Member property directly:
class Program
{
class Parent
{
public int A { get; set; }
public Child Child { get; set; }
}
class Child
{
public string Data { get; set; }
}
public static MemberInfo GetMemberInfo(LambdaExpression exp)
{
var body = exp.Body as MemberExpression;
return body.Member;
}
static void Main(string[] args)
{
Expression<Func<Parent, string>> func1 = p => p.Child.Data;
Console.WriteLine(GetMemberInfo(func1));
Expression<Func<Parent, int>> func2 = p => p.A;
Console.WriteLine(GetMemberInfo(func2));
}
}
Output:
System.String Data
Int32 A
You must be using Expression instead of just Func
In your code in the MemberAccess section you are checking if the member is from the parameter, in this case Parent. If you remove that check then you will get the member for Data
Change this section
case ExpressionType.MemberAccess:
var memberExpression = (MemberExpression) expression;
if (memberExpression.Expression.NodeType == ExpressionType.Parameter ||
memberExpression.Expression.NodeType == ExpressionType.Convert)
return memberExpression.Member;
throw new Exception();
To
case ExpressionType.MemberAccess:
var memberExpression = (MemberExpression) expression;
return memberExpression.Member;
I don't know why you had the guard for the parameter, if you need it in certain cases then you can create a different method or pass in a parameter.
Related
Is it possible to dynamically build an IQueryable/Linq Expression with filtering criteria based on a NESTED/Child List Object's Property.
I have not included all code here - particularly the code around Pagination but I hope there is sufficient detail. Things to note is my use of EFCore5 and Automapper ProjectTo extension method.
For Example:
public class PersonModel
{
public int Id { get; set; }
public PersonName Name { get; set; }
public List<Pet> Pets { get; set; }
}
[Owned]
public class PersonName
{
public string Surname { get; set; }
public string GivenNames { get; set; }
}
public class Pet
{
public string Name { get; set; }
public string TypeOfAnimal { get; set; }
}
Here is my WebApi Controller.
[HttpGet(Name = nameof(GetAllPersons))]
public async Task<ActionResult<IEnumerable<PersonDTO>>> GetAllPersons(
[FromQuery] QueryStringParameters parameters)
{
IQueryable<Person> persons = _context.Persons;
parameters.FilterClauses.ForEach(filter =>
persons = persons.Where(filter.name, filter.op, filter.val));
// Note the use of 'Where' Extension Method.
var dTOs = persons
.ProjectTo<PersonDTO>(_mapper.ConfigurationProvider);;
var pagedPersons = PaginatedList<PersonDTO>
.CreateAsync(dTOs, parameters);
return Ok(await pagedPersons);
}
To query for all people with a Name.GivenNames property equal to "John" I would issue a GET call such as;
http://127.0.0.1/api/v1.0/?Filter=Name.GivenNames,==,John
This works perfectly fine.
However I would like to query for all people with a Pet with a Name property equal to "Scruffy" I would issue a GET call such as;
http://127.0.0.1/api/v1.0/?Filter=Pets.Name,==,Scruffy
Somewhat expectedly it throws the following exception on the line of code in BuildPredicate Function. This is because "Pets" is Type is a "List"... not a "Pet"
var left = propertyName.Split...
Instance property 'Pet:Name' is not defined for type
System.Collections.Generic.List`1[Person]' (Parameter 'propertyName')
Here are the Extension Methods.
public static class ExpressionExtensions
{
public static IQueryable<T> Where<T>(this IQueryable<T> source, string propertyName, string comparison, string value)
{
return source.Where(BuildPredicate<T>(propertyName, comparison, value));
}
}
public static Expression<Func<T, bool>> BuildPredicate<T>(string propertyName, string comparison, string value)
{
var parameter = Expression.Parameter(typeof(T), "x");
var left = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property);
var body = MakeComparison(left, comparison, value);
return Expression.Lambda<Func<T, bool>>(body, parameter);
}
private static Expression MakeComparison(Expression left, string comparison, string value)
{
switch (comparison)
{
case "==":
return MakeBinary(ExpressionType.Equal, left, value);
case "!=":
return MakeBinary(ExpressionType.NotEqual, left, value);
case ">":
return MakeBinary(ExpressionType.GreaterThan, left, value);
case ">=":
return MakeBinary(ExpressionType.GreaterThanOrEqual, left, value);
case "<":
return MakeBinary(ExpressionType.LessThan, left, value);
case "<=":
return MakeBinary(ExpressionType.LessThanOrEqual, left, value);
case "Contains":
case "StartsWith":
case "EndsWith":
return Expression.Call(MakeString(left), comparison, Type.EmptyTypes, Expression.Constant(value, typeof(string)));
default:
throw new NotSupportedException($"Invalid comparison operator '{comparison}'.");
}
}
private static Expression MakeString(Expression source)
{
return source.Type == typeof(string) ? source : Expression.Call(source, "ToString", Type.EmptyTypes);
}
private static Expression MakeBinary(ExpressionType type, Expression left, string value)
{
object typedValue = value;
if (left.Type != typeof(string))
{
if (string.IsNullOrEmpty(value))
{
typedValue = null;
if (Nullable.GetUnderlyingType(left.Type) == null)
left = Expression.Convert(left, typeof(Nullable<>).MakeGenericType(left.Type));
}
else
{
var valueType = Nullable.GetUnderlyingType(left.Type) ?? left.Type;
typedValue = valueType.IsEnum ? Enum.Parse(valueType, value) :
valueType == typeof(Guid) ? Guid.Parse(value) :
valueType == typeof(DateTimeOffset) ? DateTimeOffset.ParseExact(value, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal) :
Convert.ChangeType(value, valueType);
}
}
var right = Expression.Constant(typedValue, left.Type);
return Expression.MakeBinary(type, left, right);
}
Is there anyway to adapt this code to detect that if one of the nested properties is a LIST that it builds an 'Inner Predicate' to do a query on the child collection? ie: Enumerable.Any() ?
Working with raw expression tree's, it sometimes helps to start with an example, let the C# compiler have a go at it, and work backwards. eg;
Expression<Func<Person,bool>> expr = p => p.Pets.Any(t => t.Foo == "blah");
Though the compiler does take a shortcut in IL to specify type members that can't be decompiled.
The trick here is to make your method recursive. Instead of assuming that you can get each property;
var left = propertyName.Split('.').Aggregate((Expression)parameter, Expression.Property);
If you find a collection property in the list, you need to call BuildPredicate<Pet> with the remaining property string. Then use the return value as the argument to call .Pets.Any(...).
Perhaps something like;
public static Expression<Func<T, bool>> BuildPredicate<T>(string propertyName, string comparison, string value)
=> (Expression<Func<T, bool>>)BuildPredicate(typeof(T), propertyName.Split('.'), comparison, value);
public static LambdaExpression BuildPredicate(Type t, Span<string> propertyNames, string comparison, string value)
{
var parameter= Expression.Parameter(t, "x");
var p = (Expression)parameter;
for(var i=0; i<propertyNames.Length; i++)
{
var method = p.Type.GetMethods().FirstOrDefault(m => m.Name == "GetEnumerator" && m.ReturnType.IsGenericType);
if (method != null)
{
BuildPredicate(method.ReturnType.GetGenericArguments()[0], propertyNames.Slice(i), comparison, value);
// TODO ...
}
else
p = Expression.Property(p, propertyNames[i]);
}
// TODO ...
return Expression.Lambda(body, parameter);
}
Background
My client would like to have a method of sending over an array of field (string), value (string), and comparison (enum) values in order to retrieve their data.
public class QueryableFilter {
public string Name { get; set; }
public string Value { get; set; }
public QueryableFilterCompareEnum? Compare { get; set; }
}
My company and I have never attempted to do anything like this before, so it is up to my team to come up with a viable solution. This is the result of working on a solution with a week or so of research.
What Works: Part 1
I have created a service that is able to retrieve the data from our table Classroom. Retrieval of the data is done in Entity Framework Core by way of LINQ-to-SQL. The way I have written below works if one of the fields that are supplied in the filter doesn't exist for Classroom but does exist for its related Organization (the client wanted to be able to search among organization addresses as well) and has a navigatable property.
public async Task<IEnumerable<IExportClassroom>> GetClassroomsAsync(
IEnumerable<QueryableFilter> queryableFilters = null) {
var filters = queryableFilters?.ToList();
IQueryable<ClassroomEntity> classroomQuery = ClassroomEntity.All().AsNoTracking();
// The organization table may have filters searched against it
// If any are, the organization table should be inner joined to all filters are used
IQueryable<OrganizationEntity> organizationQuery = OrganizationEntity.All().AsNoTracking();
var joinOrganizationQuery = false;
// Loop through the supplied queryable filters (if any) to construct a dynamic LINQ-to-SQL queryable
if (filters?.Count > 0) {
foreach (var filter in filters) {
try {
classroomQuery = classroomQuery.BuildExpression(filter.Name, filter.Value, filter.Compare);
} catch (ArgumentException ex) {
if (ex.ParamName == "propertyName") {
organizationQuery = organizationQuery.BuildExpression(filter.Name, filter.Value, filter.Compare);
joinOrganizationQuery = true;
} else {
throw new ArgumentException(ex.Message);
}
}
}
}
// Inner join the classroom and organization queriables (if necessary)
var query = joinOrganizationQuery
? classroomQuery.Join(organizationQuery, classroom => classroom.OrgId, org => org.OrgId, (classroom, org) => classroom)
: classroomQuery;
query = query.OrderBy(x => x.ClassroomId);
IEnumerable<IExportClassroom> results = await query.Select(ClassroomMapper).ToListAsync();
return results;
}
What Works: Part 2
The BuildExpression that exists in code is something that I created as such (with room for expansion).
public static IQueryable<T> BuildExpression<T>(this IQueryable<T> source, string columnName, string value, QueryableFilterCompareEnum? compare = QueryableFilterCompareEnum.Equal) {
var param = Expression.Parameter(typeof(T));
// Get the field/column from the Entity that matches the supplied columnName value
// If the field/column does not exists on the Entity, throw an exception; There is nothing more that can be done
MemberExpression dataField;
try {
dataField = Expression.Property(param, propertyName);
} catch (ArgumentException ex) {
if (ex.ParamName == "propertyName") {
throw new ArgumentException($"Queryable selection does not have a \"{propertyName}\" field.", ex.ParamName);
} else {
throw new ArgumentException(ex.Message);
}
}
ConstantExpression constant = !string.IsNullOrWhiteSpace(value)
? Expression.Constant(value.Trim(), typeof(string))
: Expression.Constant(value, typeof(string));
BinaryExpression binary = GetBinaryExpression(dataField, constant, compare);
Expression<Func<T, bool>> lambda = (Expression<Func<T, bool>>)Expression.Lambda(binary, param)
return source.Where(lambda);
}
private static Expression GetBinaryExpression(MemberExpression member, ConstantExpression constant, QueryableFilterCompareEnum? comparisonOperation) {
switch (comparisonOperation) {
case QueryableFilterCompareEnum.NotEqual:
return Expression.Equal(member, constant);
case QueryableFilterCompareEnum.GreaterThan:
return Expression.GreaterThan(member, constant);
case QueryableFilterCompareEnum.GreaterThanOrEqual:
return Expression.GreaterThanOrEqual(member, constant);
case QueryableFilterCompareEnum.LessThan:
return Expression.LessThan(member, constant);
case QueryableFilterCompareEnum.LessThanOrEqual:
return Expression.LessThanOrEqual(member, constant);
case QueryableFilterCompareEnum.Equal:
default:
return Expression.Equal(member, constant);
}
}
}
The Problem / Getting Around to My Question
While the inner join on the Classroom and Organization works, I'd rather not have to pull in a second entity set for checking values that are navigatable. If I typed in a City as my filter name, normally I would do this:
classroomQuery = classroomQuery.Where(x => x.Organization.City == "Atlanta");
That doesn't really work here.
I have tried a couple of different methods in order to get me what I'm looking for:
A compiled function that would return Func<T, bool>, but when put through LINQ-to-SQL, the query did not include it.
I changed it to an Expression<Func<T, bool>>, but my return didn't return a bool in the way I attempted to implement it, so that didn't work.
I switched the way that I was implementing the navigation property, but none of my functions would read the value properly.
Basically, is there some way that I can implement the following in a way that LINQ-to-SQL from Entity Framework Core will work? Other options are welcome as well.
classroomQuery = classroomQuery.Where(x => x.Organization.BuildExpression(filter.Name, filter.Value, filter.Compare));
Edit 01:
When using the expression without the dynamic builder like so:
IQueryable<ClassroomEntity>classroomQuery = ClassroomEntity.Where(x => x.ClassroomId.HasValue).Where(x => x.Organization.City == "Atlanta").AsNoTracking();
The debug reads:
.Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.AsNoTracking(.Call System.Linq.Queryable.Where(
.Call System.Linq.Queryable.Where(
.Constant<Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[ClassroomEntity]>(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[ClassroomEntity]),
'(.Lambda #Lambda1<System.Func`2[ClassroomEntity,System.Boolean]>)),
'(.Lambda #Lambda2<System.Func`2[ClassroomEntity,System.Boolean]>)))
.Lambda #Lambda1<System.Func`2[ClassroomEntity,System.Boolean]>(ClassroomEntity $x)
{
($x.ClassroomId).HasValue
}
.Lambda #Lambda2<System.Func`2[ClassroomEntity,System.Boolean]>(ClassroomEntity $x)
{
($x.Organization).City == "Bronx"
}
I tried with the dynamic builder to get the Classroom teacher, which gave me a debug of:
.Lambda #Lambda3<System.Func`2[ClassroomEntity,System.Boolean]>(ClassroomEntity $var1)
{
$var1.LeadTeacherName == "Sharon Candelariatest"
}
Still cannot figure out how to get ($var1.Organization) as the entity I'm reading from.
If you can ask the client to supply the full dot notation expression for the property. eg "Organization.City";
dataField = (MemberExpression)propertyName.split(".")
.Aggregate(
(Expression)param,
(result,name) => Expression.Property(result, name));
If I am getting your problem statement, you want to be able to travel up the navigation property chain.
If that is indeed the case the real challenge is getting the navigation relationships from EF. And this is where EntityTypeExtensions comes in handy. GetNavigations() in particular.
You could recursively travel up your navigation properties and build property accessor expressions as you go:
private static IEnumerable<Tuple<IProperty, Expression>> GetPropertyAccessors(this IEntityType model, Expression param)
{
var result = new List<Tuple<IProperty, Expression>>();
result.AddRange(model.GetProperties()
.Where(p => !p.IsShadowProperty()) // this is your chance to ensure property is actually declared on the type before you attempt building Expression
.Select(p => new Tuple<IProperty, Expression>(p, Expression.Property(param, p.Name)))); // Tuple is a bit clunky but hopefully conveys the idea
foreach (var nav in model.GetNavigations().Where(p => p is Navigation))
{
var parentAccessor = Expression.Property(param, nav.Name); // define a starting point so following properties would hang off there
result.AddRange(GetPropertyAccessors(nav.ForeignKey.PrincipalEntityType, parentAccessor)); //recursively call ourselves to travel up the navigation hierarchy
}
return result;
}
then your BuildExpression method can probably be a bit simplified. Notice, I added DbContext as parameter:
public static IQueryable<T> BuildExpression<T>(this IQueryable<T> source, DbContext context, string columnName, string value, QueryableFilterCompareEnum? compare = QueryableFilterCompareEnum.Equal)
{
var param = Expression.Parameter(typeof(T));
// Get the field/column from the Entity that matches the supplied columnName value
// If the field/column does not exists on the Entity, throw an exception; There is nothing more that can be done
MemberExpression dataField;
try
{
var model = context.Model.FindEntityType(typeof(T)); // start with our own entity
var props = model.GetPropertyAccessors(param); // get all available field names including navigations
var reference = props.FirstOrDefault(p => RelationalPropertyExtensions.GetColumnName(p.Item1) == columnName); // find the filtered column - you might need to handle cases where column does not exist
dataField = reference.Item2 as MemberExpression; // we happen to already have correct property accessors in our Tuples
}
catch (ArgumentException)
{
throw new NotImplementedException("I think you shouldn't be getting these anymore");
}
ConstantExpression constant = !string.IsNullOrWhiteSpace(value)
? Expression.Constant(value.Trim(), typeof(string))
: Expression.Constant(value, typeof(string));
BinaryExpression binary = GetBinaryExpression(dataField, constant, compare);
Expression<Func<T, bool>> lambda = (Expression<Func<T, bool>>)Expression.Lambda(binary, param);
return source.Where(lambda);
}
and GetClassroomsAsync would look something like this:
public async Task<IEnumerable<IExportClassroom>> GetClassroomsAsync(IEnumerable<QueryableFilter> queryableFilters = null)
{
IQueryable<ClassroomEntity> classroomQuery = ClassroomEntity.All().AsNoTracking();
// Loop through the supplied queryable filters (if any) to construct a dynamic LINQ-to-SQL queryable
foreach (var filter in queryableFilters ?? new List<QueryableFilter>())
{
try
{
classroomQuery = classroomQuery.BuildExpression(_context, filter.Name, filter.Value, filter.Compare);
}
catch (ArgumentException ex)
{
// you probably should look at catching different exceptions now as joining is not required
}
}
query = classroomQuery.OrderBy(x => x.ClassroomId);
IEnumerable<IExportClassroom> results = await query.Select(ClassroomMapper).ToListAsync();
return results;
}
Testing it out
Since you didn't supply entity hierarchy, I experimented on one of my own:
public class Entity
{
public int Id { get; set; }
}
class Company: Entity
{
public string CompanyName { get; set; }
}
class Team: Entity
{
public string TeamName { get; set; }
public Company Company { get; set; }
}
class Employee: Entity
{
public string EmployeeName { get; set; }
public Team Team { get; set; }
}
// then i've got a test harness method as GetClassroomsAsync won't compile wothout your entities
class DynamicFilters<T> where T : Entity
{
private readonly DbContext _context;
public DynamicFilters(DbContext context)
{
_context = context;
}
public IEnumerable<T> Filter(IEnumerable<QueryableFilter> queryableFilters = null)
{
IQueryable<T> mainQuery = _context.Set<T>().AsQueryable().AsNoTracking();
// Loop through the supplied queryable filters (if any) to construct a dynamic LINQ-to-SQL queryable
foreach (var filter in queryableFilters ?? new List<QueryableFilter>())
{
mainQuery = mainQuery.BuildExpression(_context, filter.Name, filter.Value, filter.Compare);
}
mainQuery = mainQuery.OrderBy(x => x.Id);
return mainQuery.ToList();
}
}
// --- DbContext
class MyDbContext : DbContext
{
public DbSet<Company> Companies{ get; set; }
public DbSet<Team> Teams { get; set; }
public DbSet<Employee> Employees { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=.\\SQLEXPRESS;Database=test;Trusted_Connection=true");
base.OnConfiguring(optionsBuilder);
}
}
// ---
static void Main(string[] args)
{
var context = new MyDbContext();
var someTableData = new DynamicFilters<Employee>(context).Filter(new
List<QueryableFilter> {new QueryableFilter {Name = "CompanyName", Value = "Microsoft" }});
}
With the above, and a filter CompanyName = "Microsoft" EF Core 3.1 generated me the following SQL:
SELECT [e].[Id], [e].[EmployeeName], [e].[TeamId]
FROM [Employees] AS [e]
LEFT JOIN [Teams] AS [t] ON [e].[TeamId] = [t].[Id]
LEFT JOIN [Companies] AS [c] ON [t].[CompanyId] = [c].[Id]
WHERE [c].[CompanyName] = N'Microsoft'
ORDER BY [e].[Id]
This approach seems to produce desired result but has one issue: column names must be unique across all your entities. This likely can be dealt with but since I don't know much specifics of your data model I'd defer it to you.
(Disclaimer: I've written code similar to this, but I haven't actually tested the code in this answer.)
Your BuildExpression takes one query (in the form of an IQueryable<T>) and returns another query. This constrains all your filters to be applied to the property of the parameter -- x.ClassroomId -- when you actually want to apply some of them to a property of a property of the parameter -- x.Organization.City.
I would suggest a GetFilterExpression method, which produces the filter expression off of some arbitrary base expression:
private static Expression GetFilterExpression(Expression baseExpr, string columnName, string value, QueryableFilterCompareEnum? compare = QueryableFilterCompareEnum.Equal) {
MemberExpression dataField;
try {
dataField = Expression.Property(baseExpr, columnName);
} catch (ArgumentException ex) {
if (ex.ParamName == "propertyName") {
throw new ArgumentException($"Base expression type does not have a \"{propertyName}\" field.", ex.ParamName);
} else {
throw new ArgumentException(ex.Message);
}
}
if (!string.IsNullOrWhiteSpace(value)) {
value = value.Trim();
}
ConstantExpression constant = Expression.Constant(value, typeof(string));
BinaryExpression binary = GetBinaryExpression(dataField, constant, compare);
return binary;
}
Within GetClassroomsAsync, you can either build the filter expression against the original ClassroomEntity parameter, or against the returned value of the Organization property on the parameter, by passing in a different expression:
public async Task<IEnumerable<IExportClassroom>> GetClassroomsAsync(IEnumerable<QueryableFilter> queryableFilters = null) {
var filters = queryableFilters?.ToList();
var param = Expression.Parameter(typeof(ClassroomEntity));
var orgExpr = Expression.Property(param, "Organization"); // equivalent of x.Organization
IQueryable<ClassroomEntity> query = ClassroomEntity.All().AsNoTracking();
if (filters is {}) {
// Map the filters to expressions, applied to the `x` or to the `x.Organization` as appropriate
var filterExpressions = filters.Select(filter => {
try {
return GetFilterExpression(param, filter.Name, filter.Value, filter.Compare);
} catch (ArgumentException ex) {
if (ex.ParamName == "propertyName") {
return GetFilterExpression(orgExpr, filter.Name, filter.Value, filter.Compare);
} else {
throw new ArgumentException(ex.Message);
}
}
});
// LogicalCombined is shown later in the answer
query = query.Where(
Expression.Lambda<Func<ClassroomEntity, bool>>(LogicalCombined(filters))
);
}
query = query.OrderBy(x => x.ClassroomId);
IEnumerable<IExportClassroom> results = await query.Select(ClassroomMapper).ToListAsync();
return results;
}
LogicalCombined takes multiple bool-returning expressions and combines them into a single expression:
private static Expression LogicalCombined(IEnumerable<Expression> exprs, ExpressionType expressionType = ExpressionType.AndAlso) {
// ensure the expression type is a boolean operator
switch (expressionType) {
case ExpressionType.And:
case ExpressionType.AndAlso:
case ExpressionType.Or:
case ExpressionType.OrElse:
case ExpressionType.ExclusiveOr:
break;
default:
throw new ArgumentException("Invalid expression type for logically combining expressions.");
}
Expression? final = null;
foreach (var expr in exprs) {
if (final is null) {
final = expr;
continue;
}
final = Expression.MakeBinary(expressionType, final, expr);
}
return final;
}
Some suggestions:
As I've written it, GetFilterExpression is a static method. Since all the arguments (except the base expression) come from QueryableFilter, you might consider making it an instance method off of QueryableFilter.
I would also suggest changing GetBinaryExpression to use a dictionary to map from QueryableFilterCompareEnum to the built-in ExpressionType. Then, the implementation of GetBinaryExpression is just a wrapper for the built-in Expression.MakeBinary method:
private static Dictionary<QueryableFilterCompareEnum, ExpressionType> comparisonMapping = new Dictionary<QueryableFilterCompareEnum, ExpressionType> {
[QueryableFilterCompareEnum.NotEqual] = ExpressionType.NotEqual,
[QueryableFilterCompareEnum.GreaterThan] = ExpressionType.GreaterThan,
[QueryableFilterCompareEnum.GreaterThanOrEqual] = ExpressionType.GreaterThanOrEqual,
[QueryableFilterCompareEnum.LessThan] = ExpressionType.LessThan,
[QueryableFilterCompareEnum.LessThanOrEqual] = ExpressionType.LessThanOrEqual,
[QueryableFilterCompareEnum.Equal] = ExpressionType.Equal
}
private static Expression GetBinaryExpression(MemberExpression member, ConstantExpression constant, QueryableFilterCompareEnum? comparisonOperation) {
comparisonOperation = comparisonOperation ?? QueryableFilterCompareEnum.Equal;
var expressionType = comparisonMapping[comparisonOperation];
return Expression.MakeBinary(
expressionType,
member,
constant
);
}
Both GetFilterExpression and GetClassroomsAsync handle the possibility that the specified property doesn't exist on either ClassroomEntity or OrganizationEntity, by trying to construct the member-access expression and handling the thrown exception.
It might be clearer to use reflection to test if the property exists on either type or not.
More, you might consider storing a static HashSet<string> with all the valid fieldnames, and check against that.
I'd like to simplify some code, if possible.
Current Constructor (T is within the scope, defined in the outer type)
public Column(string propertyName)
{
PropertyInfo propertyInfo = typeof(T).GetProperty(propertyName);
_ = propertyInfo ?? throw new ArgumentException(message: $"Property {propertyName} does not exist on {typeof(T).Name}");
...
}
I want to know if it is possible to make property a Lambda expression or something to select the property of Generic Type T.
This of course is to make our development easier with fewer mistakes.
Current use
new DataTable<someClass>.Column(nameof(someClass.someProperty))
I would like to do something like:
new DataTable<someClass>.Column(someClass.someProperty) (without declaring a new someClass)
OR
new DataTable<someClass>.Column(t = > t.someProperty)
You can use the below method to extract property name from Expression
public static PropertyInfo GetAccessedMemberInfo<T>(this Expression<T> expression)
{
MemberExpression? memberExpression = null;
if (expression.Body.NodeType == ExpressionType.Convert)
{
memberExpression = ((UnaryExpression)expression.Body).Operand as MemberExpression;
}
else if (expression.Body.NodeType == ExpressionType.MemberAccess)
{
memberExpression = expression.Body as MemberExpression;
}
if (memberExpression == null)
{
throw new ArgumentException("Not a member access", "expression");
}
return memberExpression.Member as PropertyInfo ?? throw new Exception();
}
Then use it like this
public Column(Expression<Func<T, object>> prop)
{
PropertyInfo propertyInfo = prop.GetAccessedMemberInfo();
}
new DataTable<someClass>.Column(t = > t.someProperty)
Previous answer is more complete and support more scenarios, but also more complex.
If you don't need such flexibility, this enforces you to strongly type and also makes sure that the constructor never throws
namespace ConsoleApp1
{
public class Column<T, TProperty>
{
Func<T, TProperty> functionToBeApplied;
// Pass a function, it can never throw
public Column(Func<T, TProperty> functionToBeApplied)
{
this.functionToBeApplied = functionToBeApplied;
}
// Apply the function to the object
public string GetPropertyAsString(T obj)
{
TProperty property = functionToBeApplied(obj);
return property.ToString();
}
}
class Program
{
static void Main(string[] args)
{
var column = new Column<string, int>(x => x.Length);
Console.WriteLine($"Size of string is {column.GetPropertyAsString("this is my object")}");
}
}
}
I am trying to create a function that takes in a object property and returns back object property.
How to get the property values into this specific Function, so that this function only takes takes in the object's specific property and not the entire object?
class Program
{
public T MapFrom<T,V>(T SourceObject.property, V DestinationObject.Property)
//Not able to achieve this//
{
// Code to Map Property
}
// Here I want to specifically pass only one property of Object , not the entire one
ProgramClassObject.MapFrom<Details,Person>(Details.FirstName,Person.FName)
}
}
// Class Containing Property
class Details
{
public string FirstName { get; set;}
}
// Class Containing Property
class Person
{
public string FName { get; set;}
}
You can do it manually, or use some library (see comments, someone mentetioned about it).
If still want to implement yourself:
Prepare some useful Expression extensions:
public static B GetProperty<T, B>(this Expression<Func<T, B>> propertySelector, T target) where T : class
{
if (target == null)
{
throw new ArgumentNullException("target");
}
if (propertySelector == null)
{
throw new ArgumentNullException("propertySelector");
}
var memberExpression = propertySelector.Body as MemberExpression;
if (memberExpression == null)
{
throw new NotSupportedException("Only member expression is supported.");
}
var propertyInfo = memberExpression.Member as PropertyInfo;
if (propertyInfo == null)
{
throw new NotSupportedException("You can select property only. Currently, selected member is: " +
memberExpression.Member);
}
return (B)propertyInfo.GetValue(target);
}
public static void SetProperty<T, B>(this Expression<Func<T, B>> propertySelector, T target, B value)
{
SetObjectProperty(target, propertySelector, value);
}
public static void SetObjectProperty<T, B>(T target, Expression<Func<T, B>> propertySelector, object value)
{
if (target == null)
{
throw new ArgumentNullException("target");
}
if (propertySelector == null)
{
throw new ArgumentNullException("propertySelector");
}
var memberExpression = propertySelector.Body as MemberExpression;
if (memberExpression == null)
{
throw new NotSupportedException("Cannot recognize property.");
}
var propertyInfo = memberExpression.Member as PropertyInfo;
if (propertyInfo == null)
{
throw new NotSupportedException("You can select property only. Currently, selected member is: " + memberExpression.Member);
}
propertyInfo.SetValue(target, value);
}
MapFrom implementation:
public static void MapFrom<TObject, TTarget, TProp>(TObject source, TTarget dest,
Expression<Func<TObject, TProp>> sourceSelector, Expression<Func<TTarget, TProp>> targetSelector)
where TObject : class where TTarget : class
{
var sourceValue = sourceSelector.GetProperty(source);
targetSelector.SetProperty(dest, sourceValue);
}
Usage:
programClassObject.MapFrom(details, person, det => det.FirstName, per => per.FName);
It sounds like what you're looking for is an expression. That's how some libraries like Entity Framework effectively "parse" the code that they're passed.
First, you can get the PropertyInfo from an expression through a method such as this. I'm going to explain how to use this below, so bear with me.
public static PropertyInfo GetPropertyInfo<TIn, TOut>(Expression<Func<TIn, TOut>> PropertyExpression)
{
MemberExpression memberExpr;
switch (PropertyExpression.Body.NodeType)
{
case ExpressionType.MemberAccess:
memberExpr = (MemberExpression)PropertyExpression.Body;
break;
case ExpressionType.Convert:
memberExpr = (MemberExpression)((UnaryExpression)PropertyExpression.Body).Operand;
break;
default:
throw new NotSupportedException();
}
var property = (PropertyInfo)memberExpr.Member;
return property;
}
Then, the method will become something like this. I've taken the liberty of ensuring the datatypes to be the same here, although you could change TOut to object if you'd prefer. I did this based on your name of MapFrom, which leads me to believe the properties are meant to "communicate" directly.
public T MapFrom<T, V, TOut>(Expression<Func<T, TOut>> Source, Expression<Func<V, TOut>> Destination)
{
var sourceProperty = GetPropertyInfo<T, TOut>(Source);
var destinationProperty = GetPropertyInfo<V, TOut>(Destination);
// Use those
// They're PropertyInfo instances, so it should be pretty easy to handle them however you would have expected to.
}
Once you've got all that,
var ret = MapFrom<Person, Details, string>(c => c.FName, c => c.FirstName);
The signature there could be cleaned up through the use of a generically typed master class, since you wouldn't have to specify any type arguments, and the string would be inferred. In a real-world situation, that's likely what you'd want to do, particularly since you appear to be, again, mapping values.
Is there any way to ensure that I can only pass in an expression that points to property on the a class?
public class Foo
{
public string Bar { get; set; }
public void DoSomething()
{
HelperClass.HelperFunc(() => Bar);
}
}
public static class HelperClass
{
public static void HelperFunc(Expression<Func<string>> expression)
{
// Ensure that the expression points to a property
// that is a member of the class Foo (or throw exception)
}
}
Also, if need be, I can change the signature to pass the actual class in as well...
Here is extension method, which converts lambda to property. If lambda does not points to property, exception is thrown
public static PropertyInfo ToPropertyInfo(this LambdaExpression expression)
{
MemberExpression body = expression.Body as MemberExpression;
if (body != null)
{
PropertyInfo member = body.Member as PropertyInfo;
if (member != null)
{
return member;
}
}
throw new ArgumentException("Property not found");
}