Send lambda expression from client to server - c#

I have a method that helps to dynamically build a query on a client:
public virtual IList<TEntity> Fill(Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
includeProperties = includeProperties ?? "";
var qctx = new TQueryContext
{
QueryType = filter == null ? CommonQueryType.FillAll : CommonQueryType.FillWhere,
Filter = filter,
OrderBy = orderBy
};
qctx.Includes.AddRange(includeProperties.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries));
_detachedServerRepo.Read(qctx);
return qctx.Entities;
}
I want to send qctx to a server repo which might be on another machine. Since a TQueryContext will be typed from QueryContextBase defined in part as below I can't serialize it.
public class QueryContextBase<TEntity, TKey>
where TEntity : StateTrackedObject
where TKey : IEquatable<TKey>
{
public TKey ID { get; set; }
public string Alf { get; set; }
public List<TEntity> Entities { get; set; }
public List<string> Includes { get; set; }
public Expression<Func<TEntity, bool>> Filter { get; set; }
public Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> OrderBy { get; set; }
}
How can I create similar properties to Filter and OrderBy so I can serialize them and then build up the query in the server repo as below:
protected override void FillWhere(TQueryContext qctx)
{
qctx.Entities.AddRange(this.Fill(qctx.Filter, qctx.OrderBy,
qctx.GetIncludesAsString()));
}
protected override void FillAll(TQueryContext qctx)
{
qctx.Entities.AddRange(this.Fill(null, qctx.OrderBy, qctx.GetIncludesAsString()));
}
public virtual IEnumerable<TEntity> Fill(Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
includeProperties = includeProperties ?? "";
try
{
IQueryable<TEntity> querySet = DbSet;
if (filter != null)
{
querySet = querySet.Where(filter);
}
foreach (var includeProperty in includeProperties.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
querySet = querySet.Include(includeProperty.Trim());
}
return (orderBy == null) ? querySet.ToList() : orderBy(querySet).ToList();
}
catch (Exception ex)
{
return ex.ThrowDalException<IEnumerable<TEntity>>(OperationType.Read, ex.Message, ex);
}
}

You're right not to want to reinvent the wheel. Check out Serialize.Linq.
You could solve your problem with this library as below:
In your client repo:
public virtual IList<TEntity> Fill(Expression<Func<TEntity, bool>> filter = null,
Expression<Func<IQueryable<TEntity>,
IOrderedQueryable<TEntity>>> orderBy = null,
string includeProperties = "") {
includeProperties = includeProperties ?? "";
try
{
var qctx = new TQueryContext
{
QueryType = filter == null ? CommonQueryType.FillAll : CommonQueryType.FillWhere,
FilterNode = filter == null ? null : filter.ToExpressionNode(),
OrderByNode = orderBy == null ? null : orderBy.ToExpressionNode()
};
And then in your QueryContext just add the extra property and convert:
public ExpressionNode FilterNode { get; set; }
public Expression<Func<TEntity, bool>> Filter
{
get {
return FilterNode == null ? null : FilterNode.ToBooleanExpression<TEntity>();
}
}
public ExpressionNode OrderByNode { get; set; }
public Expression<Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>> OrderBy
{
get {
return OrderByNode == null ? null : OrderByNode.ToExpression<Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>>();
}
}

It is not possible to serialize a method... any method (lambda, delegate, ...), thus it is not possible to send a method from client to server.
Best you can do is to send a filter to your service, construct the query there and return the result to the client. That is the usual way.
So in your case instead of passing a Func which filters the data pass the values which the filter uses.
To elaborate that, consider this example of server-side method:
DataType[] GetData(Filter filter, Ordering ordering)
{
var data = GetDataQuerySomeHow(); //for example in EF Context.Table
//filter data according to the filter
if (!string.IsNullOrEmpty(filter.FulltextProperty))
{
data = data.Where(a => a.StringProperty.Contains(filter.FulltextProperty));
}
//do similar thing for ordering
return data.ToArray();
}
Filter and ordering:
public class Filter
{
public string FulltextProperty { get; set; }
//some other filtering properties
}
public class Ordering
{
public string ColumnName { get; set; }
public bool Ascending { get; set; }
}
Client
Filter filter = new Filter()
{
//fill-in whatever you need
};
Ordering ordering = new Ordering(); //also fill in
var data = GetData(filter, ordering);
//display data somewhere

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

Always getting null when trying to call entity using includeProperties

I have a simple get like this:
var tc = _pService.Listar(includeProperties: "Estatus,CatalogosRegistro");
if (tc != null)
{
List<VehiculoGetViewModel> tiposcargas = new List<VehiculoGetViewModel>();
foreach (var item in tc)
{
var carga = new VehiculoGetViewModel()
{
ID = item.ID,
Nombre = item.Nombre,
NombreEstatus = item.Estatus.Nombre,
NombreContenedor = item.CatalogosRegistro.Nombre
};
tiposcargas.Add(carga);
}
}
As you can see I have includeProperties where I use Estatus and CatalogoRegistro tables.
So when I call NombreEstatus = item.Estatus.Nombre, item.Estatus.Nombre get value correctly but item.CatalogosRegistro always come null
View Model:
public class VehiculoGetViewModel
{
public Int64 ID { get; set; }
public string Nombre { get; set; }
public string NombreEstatus { get; set; }
public Estatus Estatus { get; set; }
public string NombreContenedor { get; set; }
public CatalogoRegistro CatalogosRegistro { get; set; }
}
Model
[Table("TiposVehiculo", Schema="adm")]
public class TipoVehiculo: Entidad<Int32>
{
[StringLength(255)]
public string Nombre { get; set; }
[StringLength(255)]
public int EstatusID { get; set; }
public Estatus Estatus { get; set; }
public CatalogoRegistro CatalogosRegistro { get; set; }
}
Listar implementation:
public virtual IEnumerable<T> Listar(
Expression<Func<T, bool>> filter = null,
Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,
string includeProperties = "")
{
IQueryable<T> query = _dbset;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).AsEnumerable<T>();
}
else
{
return query.AsEnumerable<T>();
}
}
Strange thing is I don´t have any problems using Estatus and I use same structure to call CatalogoRegistro but it always come null, can some one know what is wrong there? Regards
Update:
After checing query of var tc = _pService.Listar it just throwing all values of inner join with CatalogosRegistros
And that is because I don´t have CatalogosRegistros_ID I just have an TipoContenedorID column. Why EF change name of my ID?
So if I change to TipoContenedorID I get correct values:
Since you don't pass a filter or order by to Listar you don't really even need it.
var tiposcargas = _pService.Select(item => new VehiculoGetViewModel()
{
ID = item.ID,
Nombre = item.Nombre,
NombreEstatus = item.Estatus.Nombre,
NombreContenedor = item.CatalogosRegistro.Nombre
}).ToList();
This way if Estatus or CatalogosRegistro are null because there isn't a matching row in those tables it will just give you null for the Nombre. You may have to use ?? if NombreEstatus and NomberContenedor are not nullable.
Personally I'd drop using the Include and AsEnumerable and and just return the resulting IQueryable and let the caller deal with Includes or just using navigation properties in their select.
public virtual IQueryable<T> Listar(
Expression<Func<T, bool>> filter = null,
Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,)
{
IQueryable<T> query = _dbset;
if (filter != null)
{
query = query.Where(filter);
}
if (orderBy != null)
{
return orderBy(query);
}
return query;
}

Create Default Value For Expression<Func<TDto, object>[] In Runtime By Knowing the Type Only

i want to call ProjectTo dynamically
for example on this overload:
public static IQueryable ProjectTo(this IQueryable source, params Expression<Func<TDestination, object>>[] membersToExpand);
like this
Expression.Call(typeof(AutoMapper.QueryableExtensions.Extensions), "ProjectTo", new[] { DtoType },'What goes here as arguments???')
This is the overload that i want to call
public static MethodCallExpression Call(Type type, string methodName, Type[] typeArguments, params Expression[] arguments);
Based on this link:
https://github.com/AutoMapper/AutoMapper/issues/2234#event-1175181678
Update
When i have a parameter like this
IEnumerable<Expression> membersToExpand = new Expression<Func<TDto, object>>[] { };
and pass it to the function like this it works :
Expression.Call(typeof(Extensions), "ProjectTo", new[] { typeof(TDto) }, body, Expression.Constant(membersToExpand));
But when the membersToExpand parameter is null it throw the exception
No generic method 'ProjectTo' on type 'AutoMapper.QueryableExtensions.Extensions' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic
So let me clear The Question:
The 'membersToExpand' have to be not null (OK i know that), when it's not null the 'Expression.Call' can find the suitable overload and it will success But the problem is when the 'membersToExpand' is null so i have to create the default 'membersToExpand' and i have to create it dynamically, i can't create it like this
Expression.Constant(null, typeof(Expression<Func<TDto, object>>[]))
How to create this dynamicaly:
typeof(Expression<Func<TDto, object>>[]
when i just know the 'typeof(TDto)' ??
To reproduce the problem Just copy and Paste and then Run:
class Program
{
static void Main(string[] args)
{
Mapper.Initialize(cfg =>
{
cfg.CreateMap<UserGroupDto, UserGroup>();
cfg.CreateMap<UserGroup, UserGroupDto>()
.ForMember(dest => dest.UserGroupDetails, conf =>
{
conf.MapFrom(ent => ent.UserGroupDetails);
conf.ExplicitExpansion();
});
cfg.CreateMap<UserGroupDetailDto, UserGroupDetail>();
cfg.CreateMap<UserGroupDetail, UserGroupDetailDto>()
.ForMember(dto => dto.UserGroupName, conf => conf.MapFrom(ent => ent.UserGroup.Title));
});
// Someone Wants To Call WITH Members To Expand
Start(new Expression<Func<UserGroupDto, object>>[] { e => e.UserGroupDetails });
Console.WriteLine("===============================================");
// Someone Wants To Call WITHOUT Members To Expand (It's a pain to pass an empty 'new Expression<Func<UserGroupDto, object>>[] { }' so it's better to create the default, dynamically when it's null )
Start(new Expression<Func<UserGroupDto, object>>[] { });
// Someone Wants To Call WITHOUT Members To Expand and pass it null and it will THROW Exception
// Start(null);
Console.ReadLine();
}
static void Start(IEnumerable<Expression> membersToExpand)
{
using (var db = new MyDbContext())
{
IEnumerable projected = CallProjectToDynamically(db.UserGroups.AsQueryable(), typeof(UserGroupDto), membersToExpand);
foreach (var item in projected.OfType<UserGroupDto>())
{
Console.WriteLine(" UserGroupID => " + item.ID);
Console.WriteLine(" UserGroupTitle => " + item.Title);
if (item.UserGroupDetails != null)
item.UserGroupDetails.ToList().ForEach(d =>
{
Console.WriteLine(" UserGroupDetailID => " + d.ID);
Console.WriteLine(" UserGroupDetailTitle => " + d.Title);
Console.WriteLine(" UserGroupDetailUserGroupName => " + d.UserGroupName);
});
}
}
}
static IQueryable CallProjectToDynamically<T>(IQueryable<T> source, Type dtoType, IEnumerable<Expression> membersToExpand)
{
// What i want is this:
// if(membersToExpand == null)
// Create The default dynamically For 'params Expression<Func<TDestination, object>>[] membersToExpand'
var param = Expression.Parameter(typeof(IQueryable<T>), "data");
var body = Expression.Call(typeof(Extensions), "ProjectTo", new[] { dtoType }, param, Expression.Constant(membersToExpand));
return (Expression.Lambda<Func<IQueryable<T>, IQueryable>>(body, param).Compile())(source);
}
}
public partial class MyDbContext : DbContext
{
public MyDbContext()
: base("name=MyDbContext")
{
Database.SetInitializer(new MyDbContextDBInitializer());
}
public virtual DbSet<UserGroup> UserGroups { get; set; }
public virtual DbSet<UserGroupDetail> UserGroupMembers { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<UserGroup>()
.HasMany(e => e.UserGroupDetails)
.WithRequired(e => e.UserGroup)
.HasForeignKey(e => e.UserGroupID);
}
}
public class MyDbContextDBInitializer : DropCreateDatabaseAlways<MyDbContext>
{
protected override void Seed(MyDbContext context)
{
IList<UserGroup> defaultStandards = new List<UserGroup>
{
new UserGroup() { Title = "First UserGroup", UserGroupDetails = new [] { new UserGroupDetail { Title = "UGD for First UserGroup" } } },
new UserGroup() { Title = "Second UserGroup", UserGroupDetails = new [] { new UserGroupDetail { Title = "UGD for Second UserGroup" } } },
new UserGroup() { Title = "Third UserGroup", UserGroupDetails = new [] { new UserGroupDetail { Title = "UGD for Third UserGroup" } } }
};
foreach (UserGroup std in defaultStandards)
context.UserGroups.Add(std);
base.Seed(context);
}
}
public partial class UserGroup
{
public UserGroup()
{
UserGroupDetails = new HashSet<UserGroupDetail>();
}
public long ID { get; set; }
public string Title { get; set; }
public virtual ICollection<UserGroupDetail> UserGroupDetails { get; set; }
}
public partial class UserGroupDetail
{
public long ID { get; set; }
public long UserGroupID { get; set; }
public string Title { get; set; }
public virtual UserGroup UserGroup { get; set; }
}
public partial class UserGroupDto
{
public long ID { get; set; }
public string Title { get; set; }
public IEnumerable<UserGroupDetailDto> UserGroupDetails { get; set; }
}
public partial class UserGroupDetailDto
{
public long ID { get; set; }
public string Title { get; set; }
public long UserGroupID { get; set; }
public string UserGroupName { get; set; }
}
Sincerely.
var call = Expression.Call(typeof(AutoMapper.QueryableExtensions.Extensions), "ProjectTo", new[] { typeof(Bar) },
Expression.Constant(source), Expression.Constant(null, typeof(Expression<Func<Bar, object>>[])));
But I think you're wasting your time. In the latest version, you're not even allowed to pass null.
I solve this problem by wrapping code into generic metod. When I use ProjectTo<T> I don't need to pass second argument. Here is my helper, it takes the type and list of fields and returns DataTable. DB2 is DbContext.
public static class DTODataHelper
{
public static DataTable GetDataOfType(DB2 db, string[] fields, Type type)
{
var method = typeof(DTODataHelper).GetMethod("GetData").MakeGenericMethod(type);
var result = method.Invoke(null, new object[] { db, fields }) as DataTable;
return result;
}
public static DataTable GetData<T>(DB2 db, string[] fields)
{
var dtoType = typeof(T);
var source = Mapper.Configuration.GetAllTypeMaps().Where(i => i.DestinationType == dtoType).Select(i => i.SourceType).FirstOrDefault();
if (source == null)
throw new HMException("Не найден источник данных");
var dbSet = db.Set(source);
var querable = dbSet.AsQueryable();
var list = dbSet.ProjectTo<T>().ToList();
return GetDataTable(list, fields);
}
public static DataTable GetDataTable<T>(IEnumerable<T> varlist, string[] fields)
{
DataTable dtReturn = new DataTable();
PropertyInfo[] oProps = null;
var hasColumnFilter = fields != null && fields.Length > 0;
if (varlist == null) return dtReturn;
foreach (T rec in varlist)
{
if (oProps == null)
{
oProps = rec.GetType().GetProperties();
var excludedProperties = new List<PropertyInfo>();
foreach (PropertyInfo pi in oProps)
{
if (hasColumnFilter && !fields.Contains(pi.Name))
{
excludedProperties.Add(pi);
continue;
}
Type colType = pi.PropertyType;
if (colType.IsGenericType && colType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
colType = colType.GetGenericArguments()[0];
}
var piName = pi.GetCustomAttributes().OfType<DisplayNameAttribute>().FirstOrDefault()?.DisplayName ?? pi.Name;
dtReturn.Columns.Add(new DataColumn(piName, colType));
}
if (excludedProperties.Count > 0)
{
oProps = oProps.Except(excludedProperties).ToArray();
}
}
DataRow dr = dtReturn.NewRow();
foreach (PropertyInfo pi in oProps)
{
var piName = pi.GetCustomAttributes().OfType<DisplayNameAttribute>().FirstOrDefault()?.DisplayName ?? pi.Name;
dr[piName] = pi.GetValue(rec, null) == null ? DBNull.Value : pi.GetValue(rec, null);
}
dtReturn.Rows.Add(dr);
}
return dtReturn;
}
}

Using dynamic linq extensions with jqgrid on one to many relationships

Im not sure if the title of the question is correct if not let me know and i will change it. Okay so firstly im using jgrid in a asp.net application. I have a helper that sets up the page size, rows and does the sorting. The issue is i can only sort on parameters for the current table it does not work when the parameter name is from a separate table connected through a relationship.
The linq extension is for orderBy and thenBy, they both take a string parameter and sort order parameter. I have tried setting the string parameter as "WaterSample.Name", watersample being another table from a relationship and name a parameter in that table but unfortunately i cannot do it this way in c# since i am using an anonymous type. So my question is can i make the line extensions search through the relationships to find match the property name. Sorry if this is a bit confusing im still fairly new to linq.
i have included the class below that contains the extensions.
namespace Helpers
{
[ModelBinder(typeof(GridModelBinder))]
[Serializable]
public class JqGridSettings
{
public int PageIndex { get; set; }
public int PageSize { get; set; }
public string SortColumn { get; set; }
public string SortOrder { get; set; }
public string SortColumn2 { get; set; }
public string SortOrder2 { get; set; }
public string SortColumn3 { get; set; }
public string SortOrder3 { get; set; }
public JqGridSettings()
{
}
/// <summary>
/// Initializes a new instance of the GridSettings class.
/// </summary>
/// <param name="stringData">The string that represents the a GridSettings object.</param>
public JqGridSettings(string stringData)
{
var tempArray = stringData.Split(new[] { "#;" }, StringSplitOptions.None);
PageSize = int.Parse(tempArray[0]);
PageIndex = int.Parse(tempArray[1]);
SortColumn = tempArray[2];
SortOrder = tempArray[3];
}
/// <summary>
/// Build a string used to cache the current GridSettings object.
/// </summary>
/// <returns>A string that represents the current GridSettings object.</returns>
public override string ToString()
{
return string.Format("{0}#;{1}#;{2}#;{3}", PageSize, PageIndex, SortColumn,SortOrder);
}
public IQueryable<T> LoadGridData<T>(IQueryable<T> dataSource, out int count)
{
var query = dataSource;
//
// Sorting and Paging by using the current grid settings.
//
query = query.OrderBy<T>(SortColumn, SortOrder);
if (String.IsNullOrEmpty(SortColumn2) == false)
{
query = query.ThenBy<T>(SortColumn2, SortOrder2);
}
if (String.IsNullOrEmpty(SortColumn3) == false)
{
query = query.ThenBy<T>(SortColumn3, SortOrder3);
}
count = query.Count();
//
if (PageIndex < 1)
PageIndex = 1;
//
var data = query.Skip((PageIndex - 1) * PageSize).Take(PageSize);
return data;
}
}
public static class LinqExtensions
{
public static IQueryable<T> OrderBy<T>(this IQueryable<T> query, string sortColumn, string direction)
{
var methodName = string.Format("OrderBy{0}", direction.ToLower() == "asc" ? "" : "descending");
var parameter = Expression.Parameter(query.ElementType, "p");
var memberAccess = sortColumn.Split('.').Aggregate<string, MemberExpression>(null, (current, property) => Expression.Property(current ?? (parameter as Expression), property));
var orderByLambda = Expression.Lambda(memberAccess, parameter);
var result = Expression.Call(typeof(Queryable),methodName,new[]{query.ElementType, memberAccess.Type},query.Expression,Expression.Quote(orderByLambda));
return query.Provider.CreateQuery<T>(result);
}
public static IQueryable<T> ThenBy<T>(this IQueryable<T> query, string sortColumn, string direction)
{
var methodName = string.Format("ThenBy{0}", direction.ToLower() == "asc" ? "" : "descending");
var parameter = Expression.Parameter(query.ElementType, "p");
var memberAccess = sortColumn.Split('.').Aggregate<string, MemberExpression>(null, (current, property) => Expression.Property(current ?? (parameter as Expression), property));
var orderByLambda = Expression.Lambda(memberAccess, parameter);
var result = Expression.Call(typeof(Queryable), methodName, new[] { query.ElementType, memberAccess.Type }, query.Expression, Expression.Quote(orderByLambda));
return query.Provider.CreateQuery<T>(result);
}
}
public class GridModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
try
{
var request = controllerContext.HttpContext.Request;
return new JqGridSettings
{
PageIndex = int.Parse(request["page"] ?? "1"),
PageSize = int.Parse(request["rows"] ?? "50"),
SortColumn = request["sidx"] ?? "",
SortOrder = request["sord"] ?? "asc",
};
}
catch
{
//
// For unexpected errors use the default settings!
//
return null;
}
}
}
}
Thank you for any help or suggestions.

Linq IQueryable Generic Filter

I am looking for a generic Filter for the searchText in the query of any Column/Field mapping
public static IQueryable<T> Filter<T>(this IQueryable<T> source, string searchTerm)
{
var propNames = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(e=>e.PropertyType == typeof(String)).Select(x => x.Name).ToArray();
//I am getting the property names but How can I create Expression for
source.Where(Expression)
}
Here I am giving you an example scenario
Now From my HTML5 Table in Asp.net MVC4 , I have provided a Search box to filter results of entered text , which can match any of the below columns/ Menu class Property values , and I want to do this search in Server side , how can I implement it.
EF Model Class
public partial class Menu
{
public int Id { get; set; }
public string MenuText { get; set; }
public string ActionName { get; set; }
public string ControllerName { get; set; }
public string Icon { get; set; }
public string ToolTip { get; set; }
public int RoleId { get; set; }
public virtual Role Role { get; set; }
}
You can use Expressions:
private static Expression<Func<T, bool>> GetColumnEquality<T>(string property, string term)
{
var obj = Expression.Parameter(typeof(T), "obj");
var objProperty = Expression.PropertyOrField(obj, property);
var objEquality = Expression.Equal(objProperty, Expression.Constant(term));
var lambda = Expression.Lambda<Func<T, bool>>(objEquality, obj);
return lambda;
}
public static IQueryable<T> Filter<T>(IQueryable<T> source, string searchTerm)
{
var propNames = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(e => e.PropertyType == typeof(string))
.Select(x => x.Name).ToList();
var predicate = PredicateBuilder.False<T>();
foreach(var name in propNames)
{
predicate = predicate.Or(GetColumnEquality<T>(name, searchTerm));
}
return source.Where(predicate);
}
Combined with the name PredicateBuilder from C# in a NutShell. Which is also a part of LinqKit.
Example:
public class Foo
{
public string Bar { get; set; }
public string Qux { get; set; }
}
Filter<Foo>(Enumerable.Empty<Foo>().AsQueryable(), "Hello");
// Expression Generated by Predicate Builder
// f => ((False OrElse Invoke(obj => (obj.Bar == "Hello"), f)) OrElse Invoke(obj => (obj.Qux == "Hello"), f))
void Main()
{
// creates a clause like
// select * from Menu where MenuText like '%ASD%' or ActionName like '%ASD%' or....
var items = Menu.Filter("ASD").ToList();
}
// Define other methods and classes here
public static class QueryExtensions
{
public static IQueryable<T> Filter<T>(this IQueryable<T> query, string search)
{
var properties = typeof(T).GetProperties().Where(p =>
/*p.GetCustomAttributes(typeof(System.Data.Objects.DataClasses.EdmScalarPropertyAttribute),true).Any() && */
p.PropertyType == typeof(String));
var predicate = PredicateBuilder.False<T>();
foreach (var property in properties )
{
predicate = predicate.Or(CreateLike<T>(property,search));
}
return query.AsExpandable().Where(predicate);
}
private static Expression<Func<T,bool>> CreateLike<T>( PropertyInfo prop, string value)
{
var parameter = Expression.Parameter(typeof(T), "f");
var propertyAccess = Expression.MakeMemberAccess(parameter, prop);
var like = Expression.Call(propertyAccess, "Contains", null, Expression.Constant(value,typeof(string)));
return Expression.Lambda<Func<T, bool>>(like, parameter);
}
}
You need to add reference to LinqKit to use PredicateBuilder and AsExpandable method
otherwise won't work with EF, only with Linq to SQL
If you want Col1 like '%ASD%' AND Col2 like '%ASD%' et, change PredicateBuilder.False to PredicateBuilder.True and predicate.Or to predicate.And
Also you need to find a way to distinguish mapped properties by your own custom properties (defined in partial classes for example)

Categories