Using Microsoft.CodeAnalysis.CSharp.Scripting I have created a generic method to turn a string into a predicate:
public static Func<T, bool> CreatePredicate<T>(string command)
{
var options = ScriptOptions.Default.AddReferences(typeof(T).Assembly);
Func<T, bool> predicate = CSharpScript.EvaluateAsync<Func<T, bool>>(command, options).Result;
return predicate;
}
The problem is that this is very slow. It takes 3-4 seconds to generate the predicate.
Is there a way to turn a string into a predicate faster using Roslyn? (I am aware of alternative of manually creating expression using Expression class, this question is specifically at doing this with Roslyn)
Here is stadalone example:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
namespace DynamicLambda
{
class Program
{
static void Main(string[] args)
{
var albums = new List<Album>
{
new Album { Quantity = 10, Artist = "Betontod", Title = "Revolution" },
new Album { Quantity = 50, Artist = "The Dangerous Summer", Title = "The Dangerous Summer" },
new Album { Quantity = 200, Artist = "Depeche Mode", Title = "Spirit" },
};
var albumFilter = "album => album.Quantity > 20 && album.Quantity < 200";
var sw = new Stopwatch();
sw.Start();
var predicate = CreatePredicate<Album>(albumFilter);
sw.Stop();
var filteredAlbums = albums.Where(predicate).ToList();
}
public static Func<T, bool> CreatePredicate<T>(string command)
{
var options = ScriptOptions.Default.AddReferences(typeof(T).Assembly);
Func<T, bool> predicate = CSharpScript.EvaluateAsync<Func<T, bool>>(command, options).Result;
return predicate;
}
}
public class Album
{
public int Quantity { get; set; }
public string Title { get; set; }
public string Artist { get; set; }
}
}
Related
I am using CSharpScript.EvaluateAsync to generate a predicate from a string. Using string and int variables it works fine but if I try to pass in an enum variable in the string to convert it throws the error:
Message "(1,26): error CS0103: The name 'Status' does not exist in the current context" string
Here is the standalone reproduction:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
namespace PredicateParser
{
class Program
{
static void Main(string[] args)
{
var albums = new List<Album>
{
new Album { Quantity = 10, Artist = "Betontod", Title = "Revolution", Status = Status.Offline},
new Album { Quantity = 50, Artist = "The Dangerous Summer", Title = "The Dangerous Summer", Status = Status.Offline },
new Album { Quantity = 200, Artist = "Depeche Mode", Title = "Spirit", Status = Status.Online },
};
var albumFilter1 = "album => album.Quantity > 20 && album.Quantity < 200"; //works fine
var albumFilter2 = "album => album.Status == Status.Online"; //Throws exception
var predicate1 = CreatePredicate<Album>(albumFilter1);
var predicate2 = CreatePredicate<Album>(albumFilter2);
var filteredAlbums1 = albums.Where(predicate1).ToList();
var filteredAlbums2 = albums.Where(predicate2).ToList();
}
public static Func<T, bool> CreatePredicate<T>(string command)
{
var options = ScriptOptions.Default.AddReferences(typeof(T).Assembly);
Func<T, bool> predicate = CSharpScript.EvaluateAsync<Func<T, bool>>(command, options).Result;
return predicate;
}
}
public class Album
{
public int Quantity { get; set; }
public string Title { get; set; }
public string Artist { get; set; }
public Status Status { get; set; }
}
public enum Status
{
Online,
Offline
}
}
How can I get it to work with enums?
var options = ScriptOptions.Default.AddReferences(typeof(T).Assembly).AddImports(nameof(PredicateParser));
You forgot to add "using namespace" :)
In this case, there's no error if you fully qualify it with the namespace.
"album => album.Status == PredicateParser.Status.Online"
I really don't like to hard code the name of properties of my models. So I came up with this code so far. My code is working fine and it does exactly what I want but in an ugly way. I'm pretty sure it will be problematic soon. So any help to improve it and make it work in the right way is appreciated. I'm looking to fine better way to extract selected property names without converting expression body to string. Any change to any part of this class is fine with me. Even changing usage as long as I don't hard code my property names.
What is the better way to extract selected properties name of a model?
Here is my code:
public class Selector<T> : IDisposable
{
Dictionary<string, Func<T, object>> Selectors = new Dictionary<string, Func<T, object>>();
public Selector(params Expression<Func<T, object>>[] Selector)
{
foreach (var select in Selector)
{
//string MemberName = CleanNamesUp(select.Body.ToString());
//Func<T, object> NewSelector = select.Compile();
#region Ugly Part 1
Selectors.Add(CleanNamesUp(select.Body.ToString()), select.Compile());
#endregion
}
}
#region I am Doing This So I can Use Using(var sl = new Selector<T>())
public void Dispose()
{
Selectors.Clear();
Selectors = null;
}
#endregion
#region Ugly Part 2
private string CleanNamesUp(string nameStr)
{
string name = nameStr.Split('.')[1];
if (name.Contains(","))
{
name = name.Split(',')[0];
}
return name;
}
#endregion
public Dictionary<string, object> GetFields(T Item)
{
Dictionary<string,object> SetFieldList = new Dictionary<string, object>();
foreach(var select in Selectors)
{
SetFieldList.Add( select.Key , select.Value(Item));
}
return SetFieldList;
}
public List<Dictionary<string, object>> GetFields(IEnumerable<T> Items)
{
List<Dictionary<string, object>> SetFieldListMain = new List<Dictionary<string, object>>();
foreach (var item in Items)
{
Dictionary<string, object> SetFieldList = new Dictionary<string, object>();
foreach (var select in Selectors)
{
SetFieldList.Add(select.Key, select.Value(item));
}
SetFieldListMain.Add( SetFieldList);
}
return SetFieldListMain;
}
internal List<string> GetKeys()
{
return new List<string>(this.Selectors.Keys);
}
}
This is my model:
public class User
{
public int Id { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public bool IsEnabled { get; set; }
public bool IsLocked { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime LockedAt { get; set; }
}
And I am using it like this:
User user1 = new User();
user1.Email = "testDev#gmail.com";
user1.UserName = "dora";
user1.Password = "123456";
var UpObject = new Selector<User>( x => x.UserName, x => x.Password, x => x.Email, x => x.IsEnabled );
Dictionary<string,object> result = UpObject.GetFields(user1);
You can avoid parsing the expressions as string if you instead parse them as System.Linq.Expressions.
Full code sample follows, but not exactly for your code, I used DateTime instead of the generic T, adapting should just be find&replace:
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace ExprTest
{
class Program
{
static void Main(string[] args)
{
#region Usage
Expression<Func<DateTime, object>> propertySelector = x => x.Day;
Expression<Func<DateTime, object>> methodSelector = x => x.AddDays(1.5);
Expression[] inputSelectors = new Expression[] { propertySelector, methodSelector };
#endregion
//These are your final Selectors
Dictionary<string, Func<DateTime, object>> outputSelectors = new Dictionary<string, Func<DateTime, object>>();
//This would be in your Selector<T> constructor, replace DateTime with T.
//Instead of CleanNamesUp you would decide which part to use by extracting the appropriate Expression argument's Name.
foreach (Expression<Func<DateTime, object>> selectorLambda in inputSelectors)
{
Expression selectorExpression = selectorLambda.Body;
string name = null;
while (string.IsNullOrEmpty(name))
{
switch (selectorExpression)
{
#region Refine expression
//Necessary for value typed arguments, which get boxed by Convert(theStruct, object)
case UnaryExpression unary:
selectorExpression = unary.Operand;
break;
//add other required expression extractions
#endregion
#region Select expression key/name
case MemberExpression fieldOrProperty:
name = fieldOrProperty.Member.Name;
break;
case MethodCallExpression methodCall:
name = methodCall.Method.Name;
break;
//add other supported expressions
#endregion
}
}
outputSelectors.Add(name, selectorLambda.Compile());
}
//Set a breakpoint here to explore the outputSelectors
}
}
}
There could be a library for this, but i don't know about any, except PredicateBuilder for when you need to unify lambda arguments into one lambda expression.
I think maybe you forgot an important keyword 'nameof'. With the keyword, the code will be like this:
class User
{
public string Name { get; set; }
public string Address { get; set; }
public string Tel { get; set; }
}
static Dictionary<string, object> GetFieldsOf<T>(T item, params string[] args)
{
var properties = args.Select(property => typeof(T).GetProperty(property));
return properties.ToDictionary(property => property.Name, property => property.GetValue(item));
}
static void Main(string[] args)
{
var user = new User { Name = "Abel", Address = "Unknown", Tel = "XXX-XXX" };
var result = GetFieldsOf(user, nameof(User.Name), nameof(User.Address));
}
This code will result in some performance problems caused by reflection. But fortunately, you can avoid these by emitting a small segement of code.
//MSIL
ldarg.0
call Property.GetMethod
ret
And replace it with proerpty.GetValue. These code can be generated and cached per type, which is still worthwhile.
I have this serviceLayer Method that is used by my Web API proyect to return data to clients:
public IEnumerable<Contactos_view> ListarVistaNew(int activos, string filtro, int idSector, int idClient, string ordenar, int registroInic, int registros)
{
using (var myCon = new AdoNetContext(new AppConfigConnectionFactory(EmpresaId)))
{
using (var rep = base_getRep(myCon))
{
return rep.Listar(activos, filtro, idSector, idClient, ordenar, registroInic, registros);
}
}
}
Now the question is: How can I return only desired property of class Contactos_view? This class contains 20 properties, and my Idea is to add a parameter of type string[] Fields so client can select only the desired propeties.
Is it possible? what would be the returned type of ListarVistaNew in that case?
Thank you!
You can dynamically create and populate expando objects.
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
namespace ClientSelectsProperties
{
public class OriginalType
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
class Program
{
// this simulates your original query result - it has all properties
private static List<OriginalType> queryResult = new List<OriginalType> {
new OriginalType { Id = 1, Name = "one", Description = "one description" },
new OriginalType { Id = 2, Name = "two", Description = "two description" }
};
// "hardcoded" property value readers, go crazy here and construct them dynamically if you want (reflection, code generation...)
private static Dictionary<string, Func<OriginalType, object>> propertyReaders = new Dictionary<string, Func<OriginalType, object>> {
{ "Id", t => t.Id },
{ "Name", t => t.Name },
{ "Description", t => t.Description }
};
static void Main(string[] args)
{
// your client only wants Id and Name
var result = GetWhatClientWants(new List<string> { "Id", "Name" });
}
private static List<dynamic> GetWhatClientWants(List<string> propertyNames)
{
// make sure your queryResult is in-memory collection here. Body of this select cannot be executed in the database
return queryResult.Select(t =>
{
var expando = new ExpandoObject();
var expandoDict = expando as IDictionary<string, object>;
foreach (var propertyName in propertyNames)
{
expandoDict.Add(propertyName, propertyReaders[propertyName](t));
}
return (dynamic)expando;
}).ToList();
}
}
}
Is it possible to create a generic search method where key is unknown? for e.g Key for the List will be passed to the parameter and it performs a like search and return the filtered List.
Code should be something like:
public List<T> LikeSearch<T>(List<T> AllData,T key, string searchString)
{
List<T> _list = new List<T>();
//Perform the search on AllData based on searchString passed on the key
//given
return _list;
}
Uses will be like:
Example 1
List<Users> _users = LikeSearch<Users>(AllUsers,'Name','sam');
Where AllUsers is the list of 100 users.
Example 2
List<Customers> _cust = LikeSearch<Customers>(AllCustomers,'City','London');
Where AllCustomers is the list of 100 Customers.
Please sugest
Assuming key always refers to a public property implemented by whatever type T is, you could do the following:
public static List<T> LikeSearch<T>(this List<T> data, string key, string searchString)
{
var property = typeof(T).GetProperty(key, BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance);
if (property == null)
throw new ArgumentException($"'{typeof(T).Name}' does not implement a public get property named '{key}'.");
//Equals
return data.Where(d => property.GetValue(d).Equals(searchString)).ToList();
//Contains:
return data.Where(d => ((string)property.GetValue(d)).Contains(searchString)).ToList();
}
I think this link will help you ... Questions are different but you could find your answer there.. For reference i am again posting here the answer ...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Linq.Expressions;
using System.Reflection;
namespace Rextester
{
public class Program
{
public static void Main(string[] args)
{
List<Demo> all= new List<Demo>();
all.Add(new Demo{Name="a"});
all.Add(new Demo{Name="ab"});
all.Add(new Demo{Name="abc"});
all.Add(new Demo{Name="cba"});
all.Add(new Demo{Name="bac"});
all.Add(new Demo{Name="ddd"});
var t= Filter(all,"Name","a");
Console.WriteLine(t.Count);
}
public static List<T> Filter<T>(List<T> Filterable, string PropertyName, object ParameterValue)
{
ConstantExpression c = Expression.Constant(ParameterValue);
ParameterExpression p = Expression.Parameter(typeof(T), "xx");
MemberExpression m = Expression.PropertyOrField(p, PropertyName);
MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var containsMethodExp = Expression.Call(m, method, c);
var Lambda= Expression.Lambda<Func<T, bool>>(containsMethodExp, p);
//var Lambda = Expression.Lambda<Func<T, Boolean>>(Expression.Equal(c, m), new[] { p });
Func<T, Boolean> func = Lambda.Compile();
return Filterable.Where(func).ToList();
}
}
public class Demo
{
public string Name{get;set;}
}
}
With the linq method Where
list.Where(x => x.YourKey.Contains(searchString))
Example 1
List<Users> _users = AllUsers.Where(x => x.Name.Contains("sam"));
Example 2
List<Customers> _cust = AllCustomers.Where(x => x.City.Contains("London"));
You can write a method like this otherwise:
public List<T> LikeSearch<T>(List<T> list, Func<T, string> getKey, string searchString)
{
return list.Where(x => getKey(x).Contains(searchString)).ToList();
}
And you can use it like this:
Example 1
List<Users> _users = LikeSearch(AllUsers, x => x.Name, "sam");
Example 2
List<Customers> _cust = LikeSearch(AllCustomers, x => x.City, "London");
EDIT: here a small benchmark about solutions proposed here
I benchmarked only the Contains version of everyone.
With this we can see (depending on your computer and stars...):
InBetween OneProperty: 00:00:00.0026050
Moumit OneProperty: 00:00:00.0013360
Mine OneProperty: 00:00:00.0010390
The two different classes are here to test if the number of properties change something
InBetween LotProperties: 00:00:00.0026378
Moumit LotProperties: 00:00:00.0012155
Mine LotProperties: 00:00:00.0010021
I'm really surprised how Moumit's solution is fast, I expected it to be slower with the compile at runtime. But nevertheless, we can see that GetProperty and GetValue are really slow.
The benchmark code:
static void Main(string[] args)
{
int size = 10000;
Dictionary<string, List<long>> time = new Dictionary<string, List<long>>()
{
{"InBetween OneProperty", new List<long>() },
{"Moumit OneProperty", new List<long>() },
{"Mine OneProperty", new List<long>() },
{"InBetween LotProperties", new List<long>() },
{"Moumit LotProperties", new List<long>() },
{"Mine LotProperties", new List<long>() },
};
List<OneProperty> oneProperties = new List<OneProperty>();
List<LotProperties> lotProperties = new List<LotProperties>();
for (int i = 0; i < size; ++i)
{
oneProperties.Add(new OneProperty() { Key = i.ToString() });
lotProperties.Add(new LotProperties() { Key = i.ToString() });
}
Stopwatch sw = new Stopwatch();
for (int i = 0; i < 1000; ++i)
{
sw.Start();
InBetween.LikeSearch(oneProperties, "Key", "999");
sw.Stop();
time["InBetween OneProperty"].Add(sw.Elapsed.Ticks);
sw.Reset();
sw.Start();
Moumit.Filter(oneProperties, "Key", "999");
sw.Stop();
time["Moumit OneProperty"].Add(sw.Elapsed.Ticks);
sw.Reset();
sw.Start();
Mine.LikeSearch(oneProperties, x => x.Key, "999");
sw.Stop();
time["Mine OneProperty"].Add(sw.Elapsed.Ticks);
sw.Reset();
sw.Start();
InBetween.LikeSearch(lotProperties, "Key", "999");
sw.Stop();
time["InBetween LotProperties"].Add(sw.Elapsed.Ticks);
sw.Reset();
sw.Start();
Moumit.Filter(lotProperties, "Key", "999");
sw.Stop();
time["Moumit LotProperties"].Add(sw.Elapsed.Ticks);
sw.Reset();
sw.Start();
Mine.LikeSearch(lotProperties, x => x.Key, "999");
sw.Stop();
time["Mine LotProperties"].Add(sw.Elapsed.Ticks);
sw.Reset();
}
foreach (string key in time.Keys)
Console.WriteLine($"{key}: {new TimeSpan((long)time[key].Average())}");
Console.ReadKey();
}
class OneProperty
{
public string Key { get; set; }
}
class LotProperties
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
public string D { get; set; }
public string E { get; set; }
public string F { get; set; }
public string G { get; set; }
public string H { get; set; }
public string I { get; set; }
public string J { get; set; }
public string K { get; set; }
public string L { get; set; }
public string M { get; set; }
public string N { get; set; }
public string O { get; set; }
public string P { get; set; }
public string Q { get; set; }
public string R { get; set; }
public string S { get; set; }
public string T { get; set; }
public string U { get; set; }
public string V { get; set; }
public string W { get; set; }
public string X { get; set; }
public string Y { get; set; }
public string Z { get; set; }
public string Key { get; set; }
}
I have this program which I extend the LINQ Expression to get result using any property name and value as arguments. My extension is working fine for one property only however I have a need that I will look up and pass two or more properties as filters. Can anyone help me extend my LINQ? Below are my code implementations. I used Console Application.
Based on my sample code for the list of users I just call
GetByPropertyName("PropertyName", "Value");
For example
var user = GetByPropertyName("FirstName", "James");
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
class Program
{
static List<User> _users = new List<User>();
static void Main(string[] args)
{
var users = new List<User> {
new User { FirstName = "John", MiddleName = "Hall", LastName = "Long", Email="john.long#gmail.com"},
new User { FirstName = "John", MiddleName = "Wine", LastName = "Crawford", Email="john.crawford#gmail.com" },
new User { FirstName = "James", MiddleName = "Cage", LastName = "Hall", Email="james.hall#hotmail.com" },
new User { FirstName = "Larry", MiddleName = "Wine", LastName = "Crawford", Email="larry.crawford#gmail.com" },
new User { FirstName = "Jennifer", MiddleName = "Wine", LastName = "Long", Email="jennifer.long#gmail.com"}
};
//works okay for one property name
_users = users;
var user = GetByPropertyName("FirstName", "James");
//works okay for one property name
_users = users;
var user1 = GetByPropertyName("Email", "james.hall#hotmail.com");
//NEED HELP
//For GetByPropertyNames two or more properties
var filters = new Dictionary<object, object>();
filters.Add("FirstName", "John");
filters.Add("Email", "john.long#gmail.com");
var user2 = GetByPropertyNames(filters);
}
public static User GetByPropertyName(object propertyName, object value)
{
var result = _users.AsQueryable().Where(propertyName.ToString(), value).FirstOrDefault();
return result;
}
public static User GetByPropertyNames(Dictionary<object, object> filters)
{
var result = _users.AsQueryable().Where(filters).FirstOrDefault();
return null;
}
}
public class User
{
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
}
Extensions.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace ConsoleApplication1
{
public static class Extensions {
public static IQueryable<T> Where<T>(this IQueryable<T> source, string propertyName, object value) {
return (IQueryable<T>)Where((IQueryable)source, propertyName, value);
}
public static IQueryable Where(this IQueryable source, string propertyName, object value) {
var x = Expression.Parameter(source.ElementType, "x");
var selector = Expression.Lambda(
Expression.Equal(
Expression.PropertyOrField(x, propertyName),
Expression.Constant(value)
), x
);
return source.Provider.CreateQuery(
Expression.Call(typeof(Queryable), "Where", new Type[] { source.ElementType }, source.Expression, selector)
);
}
//NEED HELP
//TO DO
public static IQueryable<T> Where<T>(this IQueryable<T> source, Dictionary<object, object> filters)
{
return (IQueryable<T>)Where((IQueryable)source, filters);
}
public static IQueryable Where(this IQueryable source, Dictionary<object, object> filters)
{
var x = Expression.Parameter(source.ElementType, "x");
//NEED HELP
//expression for _users.FirstOrDefault(x=>x.Filter1==Filter1Value && x.Filter2==Filter2Value && so on and so fort depends on how many filters are passed as arguments);
//var selector = Expression.Lambda(
// Expression.Equal(
// Expression.PropertyOrField(x, propertyName),
// Expression.Constant(value)
// ), x
//);
//return source.Provider.CreateQuery(
// Expression.Call(typeof(Queryable), "Where", new Type[] { source.ElementType }, source.Expression, selector)
//);
//remove line below
return null;
}
}
}
Drop your IQueryable Where(this IQueryable source, Dictionary<object, object> filters) and update your generic version as follows:
public static IQueryable<T> Where<T>(this IQueryable<T> source, Dictionary<object, object> filters)
{
foreach (var kv in filters)
{
source = source.Where(kv.Key.ToString(), kv.Value);
}
return source;
}
Adding multiple Where() statements implies AND logic. If you wanted OR logic that would be more painful.