How to define dynamic custom Field in ElasticSearch Nest? - c#

With the following data structure.
public class Contact
{
public string Firstname { get; set; }
public string Lastname { get; set; }
public List<Phone> Phones { get; set; }
}
public class Phone
{
public string AreaCode { get; set; }
public string PhoneNumber { get; set; }
public bool IsMobile { get; set; }
}
I've created a simple utility function to build a custom Term query. I'm trying to define the fieldExpression using a string. I would like to subtitude "p.Phone.First().PhoneNumber" with a stringExpression parameter instead. Is that possible?
private TermQuery BuildTermQuery(string stringExpression, string value)
{
// Expression<Func<Contact, string>> fieldExpression = p => p.Phone.First().PhoneNumber;
Expression<Func<Contact, string>> fieldExpression = p => $"{stringExpression}";
var query = new TermQuery
{
Field = new Field(fieldExpression),
Value = value
};
return query;
}
If there is other way to accomplish this please let me know. Thanks for your help in advance🙂

Using an Expression<Func<T, TProp>> lambda expression to access a member of T is intended to help with typed access to properties, so passing an expression string kinda defeats the purpose :)
It's possible to just pass a string to Field constructor, or use the implicit conversion from string to Field
Field f = "my_field";
Field f2 = new Field("my_field_2");
If you know what casing convention you're using, you could use nameof() in conjunction with a property name, then case accordingly. For example, for camel case
Field f3 = nameof(ElasticContact.Property).ToCamelCase();
public class ElasticContact
{
public string Property {get;set;}
}
public static class Extensions
{
public static string ToCamelCase(this string s)
{
if (string.IsNullOrEmpty(s) || char.IsLower(s, 0))
return s;
var array = s.ToCharArray();
array[0] = char.ToLowerInvariant(array[0]);
return new string(array);
}
}
EDIT
For a nested object field, a string can still be passed
Field f = "phone.phoneNumber";
In your example, you could just pass the expression into the method, and avoid strings
private static void Main()
{
var query = BuildTermQuery<Contact, string>(c => c.Phones.First().PhoneNumber, "123456");
}
private static TermQuery BuildTermQuery<T, TProp>(Expression<Func<T, TProp>> fieldExpression, string value)
{
var query = new TermQuery
{
Field = new Field(fieldExpression),
Value = value
};
return query;
}
If you really want to build an member expression from a string expression, you could reference Microsoft.CodeAnalysis.CSharp.Scripting and evaluate the expression string into an expression delegate.

Related

Dynamically sort collection navigation property using linq

I am attempting to sort a collection based on a property.
So I do not know the property until runtime that I want to sort on.
The following works on the primary object but not any child objects
var prop = TypeDescriptor.GetProperties(typeof(TvRequests)).Find(sortProperty, tru
if (sortProperty.Contains('.'))
{
// This is a navigation property currently not supported
prop = TypeDescriptor.GetProperties(typeof(TvRequests)).Find("Title", true);
}
allRequests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase)
? allRequests.OrderBy(x => prop.GetValue(x)).ToList()
: allRequests.OrderByDescending(x => prop.GetValue(x)).ToList();
So the sortProperty is passed into my method and is a string and can be something like title or date and it works. But if I attempt to access a child property of my TvRequests object it will not work e.g. requestedUser.username
This is a trimmed down version of my objects that I'm referring to in this question:
public class TvRequests
{
public string Title { get; set; }
[ForeignKey(nameof(RequestedUserId))]
public OmbiUser RequestedUser { get; set; }
}
public class OmbiUser
{
public string Username;
}
My question is how would I be able to access any children properties like the above dynamically?
Use EF.Property.
// Get the string name of the property here
string propertyName = "Title";
allRequests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase)
? allRequests.OrderBy(x => EF.Property<string>(x, propertyName)).ToList()
: allRequests.OrderByDescending(x => EF.Property<string>(x, propertyName)).ToList();
Something along the lines may work (untested, but compiles)
// static for caching & performance
static private MethodInfo efPropertyGenericMethod = typeof(EF).GetTypeInfo().GetDeclaredMethod("Property");
Expression SortBy<TEntity>(Type type, string propertyName)
{
var xParam = Expression.Parameter(typeof(TEntity), "x");
// set T generic type here for EF.Property<T>
var efPropertyMethod = efPropertyGenericMethod.MakeGenericMethod(type);
// Creates a Lambda
Expression lambda = Expression.Lambda(
// Calls a method. First parameter is null for static calls
Expression.Call(null,
efPropertyMethod, // our cosntructed generic Version of EF.Property<T>
xParam, // Pass the x Parameter
Expression.Constant(propertyName, typeof(string)) // the propertyName asconstant
),
xParam
);
return lambda;
};
To be used as
allRequests.OrderBy(SortBy<TvRequests>(propertyType, propertyName))
Please note that that SortBy isn't called within the lambda. The below would be wrong (there's no x => in the line above).
allRequests.OrderBy(x => SortBy<TvRequests>(propertyType, propertyName))
What it does? SortBy generates an expression tree equivalent of x => EF.Property<T>(x, "MyPropertyName").
Edit:
Updated the method, so x is also passed to EF.Property(x, propertyName)
you can try something like this..
public static class QueryableExtensions
{
public enum SortDirection { ASC,DESC}
static LambdaExpression CreateExpression(Type type, string propertyName)
{
var param = Expression.Parameter(type, "x");
Expression body = param;
body = propertyName.Split('.')
.Select(prop => body = Expression.PropertyOrField(body, prop))
.Last();
return Expression.Lambda(body, param);
}
public static IQueryable<T> SortBy<T>(this IQueryable<T> source,string expressionField,SortDirection sortDirection = SortDirection.ASC)
{
var lambdaExpression = CreateExpression(typeof(T), expressionField) as dynamic;
return sortDirection == SortDirection.ASC ? Queryable.OrderBy(source,lambdaExpression) : Queryable.OrderByDescending(source, lambdaExpression);
}
}
types
public class TvRequests
{
public string Title { get; set; }
public OmbiUser RequestedUser { get; set; }
public DateTime Date { get; set; }
}
public class OmbiUser
{
public string Username;
public DateTime Date { get; set; }
}
using
List<TvRequests> reqList = new List<TvRequests>();
reqList.Add(new TvRequests {
Title = "A",
Date = DateTime.Now.AddDays(-1),
RequestedUser = new OmbiUser
{
Username = "A",
Date = DateTime.Now.AddDays(-1)
}
});
reqList.Add(new TvRequests
{
Title = "C",
Date = DateTime.Now.AddDays(1),
RequestedUser = new OmbiUser
{
Username = "C",
Date = DateTime.Now.AddDays(1)
}
});
reqList.Add(new TvRequests
{
Title = "B",
Date = DateTime.Now,
RequestedUser = new OmbiUser
{
Username = "B",
Date = DateTime.Now
}
});
foreach (var item in reqList.AsQueryable().SortBy("Date", SortDirection.DESC))
Debug.WriteLine(item.Title);
foreach (var item in reqList.AsQueryable().SortBy("RequestedUser.Date"))
Debug.WriteLine(item.Title);
foreach (var item in reqList.AsQueryable().SortBy("RequestedUser.UserName",SortDirection.DESC))
Debug.WriteLine(item.Title);

Method that return a query from a query in argument

Presentation
I have a ContactProfileModel entity class with some properties :
FirstName
LastName
BirthDate etc..
I have other Entities who have a ContactProfileModel foreignkey. Example : RegistrationModel.Contact.
Needs
I would like to create a method whith the Following signature :
public static Expression<Func<TModel, string>> Contact<TModel>(Expression<Func<TModel, ContactProfileModel>> contact)
And use it this way :
DisplayQuery.Contact<RegistrationModel>(m => m.ContactProfile))
As an equivalent of
m => m.ContactProfile.FirstName + " " + m.ContactProfile.FirstName + " " + m.ContactProfile.BirthDate.ToShortTimeString()
Objective
The objective is to return a linq query where result is a string and contains different informations of the contact. Example : "John Doe (10/10/90)"
Note
I have discussed with some people who told me to use Expression.Call and Expression.Property but unfortunatly I do not have enough knowledge to use it properly.
Here I expose my problem without extra details, but I have my reasons to create my method only this way.
Thanks in advance.
Here's a full working implementation: the code runs and outputs what you'd expect.
I'm slightly short on time, so I'm going to leave it as this. If you want clarification, ask in the comments and I'll do my best to answer.
public class Program
{
private static readonly MethodInfo stringConcatMethod = typeof(string).GetMethod("Concat", new[] { typeof(string[]) });
private static readonly MethodInfo toShortTimeStringMethod = typeof(DateTime).GetMethod("ToShortTimeString");
private static readonly PropertyInfo firstNameProperty = typeof(ContactProfileModel).GetProperty("FirstName");
private static readonly PropertyInfo lastNameProperty = typeof(ContactProfileModel).GetProperty("LastName");
private static readonly PropertyInfo birthDateProperty = typeof(ContactProfileModel).GetProperty("BirthDate");
public static void Main()
{
var result = Contact<RegistrationModel>(x => x.ContactProfile);
// Test it
var model = new RegistrationModel()
{
ContactProfile = new ContactProfileModel()
{
FirstName = "First",
LastName = "Last",
BirthDate = DateTime.Now,
}
};
var str = result.Compile()(model);
}
public static Expression<Func<TModel, string>> Contact<TModel>(Expression<Func<TModel, ContactProfileModel>> contact)
{
// We've been given a LambdaExpression. It's got a single
// parameter, which is the 'x' above, and its body
// should be a MemberExpression accessing a property on
// 'x' (you might want to check this and throw a suitable
// exception if this isn't the case). We'll grab the
// body of the LambdaExpression, and use that as the
// 'm.ContactProfile' expression in your question.
// At the end, we'll construct a new LambdaExpression
// with our body. We need to use the same ParameterExpression
// given in this LambdaExpression.
var modelParameter = contact.Parameters[0];
var propertyAccess = (MemberExpression)contact.Body;
// <contact>.FirstName
var firstNameAccess = Expression.Property(propertyAccess, firstNameProperty);
// <contact>.LastName
var lastNameAccess = Expression.Property(propertyAccess, lastNameProperty);
// <contact>.BirthDate
var birthDateAccess = Expression.Property(propertyAccess, birthDateProperty);
// <contact>.BirthDate.ToShortTimeString()
var birthDateShortTimeStringCall = Expression.Call(birthDateAccess, toShortTimeStringMethod);
// string.Concat(new string[] { <contact>.FirstName, " ", etc }
var argsArray = Expression.NewArrayInit(typeof(string), new Expression[]
{
firstNameAccess,
Expression.Constant(" "),
lastNameAccess,
Expression.Constant(" "),
birthDateShortTimeStringCall
});
var concatCall = Expression.Call(stringConcatMethod, argsArray);
// Turn it all into a LambdaExpression
var result = Expression.Lambda<Func<TModel, string>>(concatCall, modelParameter);
// Note: if you inspect 'result.DebugView' in a debugger at this
// point, you'll see a representation of the expression we've built
// up above, which is useful for figuring out where things have gone
// wrong.
return result;
}
}
public class ContactProfileModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
public class RegistrationModel
{
public ContactProfileModel ContactProfile { get; set; }
}
It might be that EF doesn't like the call to String.Concat - in that case, you might have to use a set of Expression.Add calls there instead.
First answer on StackOverflow, so be kind ;)
I tried to solve the problem, but expressions are not easy to work with. Thanks canton7 for the answer. I edited my answer in order to show the solution if you want to use the .ToString() method in expressions.
public class ContactProfileModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
public override string ToString()
{
return $"{FirstName} {LastName} {BirthDate.ToShortDateString()}";
}
}
public class RegistrationModel
{
public ContactProfileModel ContactProfile { get; set; }
}
public class Program
{
static void Main(string[] args)
{
var registration = new RegistrationModel
{
ContactProfile = new ContactProfileModel
{
FirstName = "John",
LastName = "Doe",
BirthDate = DateTime.Now
}
};
var expression = Contact<RegistrationModel>(m => m.ContactProfile);
Console.WriteLine(expression.Compile()(registration));
Console.ReadKey();
}
public static Expression<Func<TModel, string>> Contact<TModel>(Expression<Func<TModel, ContactProfileModel>> contact)
{
var propertyAccess = (MemberExpression)contact.Body;
var toString = typeof(ContactProfileModel).GetMethod("ToString");
var toStringValue = Expression.Call(propertyAccess, toString);
return Expression.Lambda<Func<TModel, string>>(toStringValue, contact.Parameters[0]);
}
}

How to dynamically get a property value in which is in another property with Linq where statement

I have found some direction for this problem but have not found anything which I can apply to this problem.
I want to filter lists of different types by stated properties they hold. I can use linq to dynamically filter a List by Test.id but I cant manage to filter a List through MyClass.Name
I have these classes.
public class Test
{
public int Id { get; set; }
public MyClass myclass { get; set; }
}
public class MyClass
{
public string Name { get; set; }
}
This is what I'm trying to do.
static void Main(string[] args)
{
var source = new List<Test> {
new Test { Id = 1,myclass = new MyClass() { Name = "bob" } },
new Test { Id = 2,myclass= new MyClass() { Name = "joe" } } };
var x = myFilter(source,"Name", "bob");
Console.WriteLine(x.Count());
}
public static IEnumerable<T> myFilter<T>(List<T> source, string propertyName, string searchString)
{
// get the myclass property then the stated property(Name) value within it
searchString = searchString.ToLower();
return source.Where(s => (s.GetType().GetProperty("myclass")
.GetType().GetProperty(propertyName)
.GetValue(s.GetType().GetProperty("myclass"),null).ToString() ?? " ")
.ToLower().Contains(searchString));
}
The count return 0 when I am expecting 1. for Test.MyClass.Name = "bob"
Is there a solution for this or is there a better way to do it besides reflection?
Thanks
you need to use the PropertyType of the returned myclass property:
public static IEnumerable<T> myFilter<T>(List<T> source, string propertyName, string searchString)
{
// get the myclass property then the stated property(Name) value within it
searchString = searchString.ToLower();
return source.Where(s => (s.GetType().GetProperty("myclass")
.PropertyType.GetProperty(propertyName)
.GetValue(s.GetType().GetProperty("myclass").GetValue(s)).ToString() ?? " ")
.ToLower().Contains(searchString));
}
You should be able to use the following:
var count = source.Count(test =>
string.Compare(test.myClass.Name, "Bob",
StringComparison.CurrentCultureIgnoreCase) == 0);
This will compare the string value of the Name Property and only count where the name is equal to "bob" and it will ignore the case.
If you want to return the Test object instead then you can use the following
var results = source.Where(test =>
string.Compare(test.myClass.Name, "Bob",
StringComparison.CurrentCultureIgnoreCase) == 0);

Comparing two objects from the same type

I'm trying to get all the fields that had been modified, comparing two objects from the same type.
For example:
public class Order
{
public int OrderNumber {get;set;}
public DateTime OrderDate {get;set};
public string Something {get;set};
}
Then, I save a new Order:
Order order1 = new Order;
order1.OrderNumber = 1;
order1.OrderDate = DateTime.Now;
order1.Something = string.Empty;
Save(order1)
After that, somebody tries to change some information from this order and I'm trying to find out the best way to get all the fields that were changed and save into a Log.
This must work for any type of two objects;
Should be a method like
public something ReturnFields(TObject objectSaved, TObject objectChanged)
Can anyone help me?
You can use reflection to get the properties on an object, and build a series of expressions to compare each property. That way you can execute them, and for those that aren't equal, return their names to the caller.
It would need extending though if the property types themselves are not all value types as in your example, otherwise it would only be doing a check of reference equality.
public static class PropertyCompare<T>
{
public readonly static Func<T, T, List<string>> ChangedProps;
private class PropertyComparer<T>
{
public Func<T, T, bool> Compare { get; set; }
public string PropertyName { get; set; }
}
static PropertyCompare()
{
PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
var firstObject = Expression.Parameter(typeof(T), "a");
var secondObject = Expression.Parameter(typeof(T), "b");
PropertyComparer<T>[] propertyComparers = new PropertyComparer<T>[properties.Length];
for (int i = 0; i < properties.Length; i++)
{
PropertyInfo thisProperty = properties[i];
Expression arePropertiesEqual = Expression.Equal(Expression.Property(firstObject, thisProperty), Expression.Property(secondObject, thisProperty));
Expression<Func<T, T, bool>> equalityFunc = Expression.Lambda<Func<T, T, bool>>(arePropertiesEqual, firstObject, secondObject);
PropertyComparer<T> comparer = new PropertyComparer<T>()
{
Compare = equalityFunc.Compile(),
PropertyName = properties[i].Name
};
propertyComparers[i] = comparer;
}
ChangedProps = new Func<T,T,List<string>>((a,b) =>
{
List<string> changedFields = new List<string>();
foreach (PropertyComparer<T> comparer in propertyComparers)
{
if (comparer.Compare(a, b))
continue;
changedFields.Add(comparer.PropertyName);
}
return changedFields;
});
}
}
public class Order
{
public int OrderNumber {get;set;}
public DateTime OrderDate {get;set; }
public string Something {get; set; }
}
static void Main(string[] args)
{
Order myOrder1 = new Order() { OrderDate = DateTime.Today, OrderNumber = 1, Something = "bleh" };
Order myOrder2 = new Order() { OrderDate = DateTime.Today.AddDays(1), OrderNumber = 1, Something = "bleh" };
List<string> changedFields = PropertyCompare<Order>.ChangedProps(myOrder1, myOrder2);
Console.ReadKey();
}
If you're using a log like txt you can make your function return's a string, like this bellow:
public string ReturnFields(TObject objectSaved, TObject objectChanged)
{
var sb = new StringBuilder();
if(!objectSaved.Name.Equals(objectChanged.Name)
{
sb.Append("Name was changed from " + objectSaved.Name +" to: " + objectChanged.Name)
}
if(!objectSaved.OrderDate.Equals(objectChanged.OrderDate)
{
sb.Append("The date whas changed from " + objectSaved.OrderDate+" to: " + objectChanged.OrderDate)
}
return sb.ToString();
}
It's just a simple way, you can read a little about Linq expressions to do it to.

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