Following on from the excellent answer to my previous question:
Linq Entity Framework generic filter method
I am now trying to understand how I can apply something like recursion to my solution.
To recap, instead of multiple similar declarations of this method:
protected IQueryable<Database.Product> GetActiveProducts( ObjectSet<Database.Product> products ) {
var allowedStates = new string[] { "Active" , "Pending" };
return (
from product in products
where allowedStates.Contains( product.State )
&& product.Hidden == "No"
select product
);
}
I now have a single implementation that accepts an interface type (IHideable) to operate on:
protected IQueryable<TEntity> GetActiveEntities<TEntity>( ObjectSet<TEntity> entities ) where TEntity : class , Database.IHideable {
var allowedStates = new string[] { "Active" , "Pending" };
return (
from entity in entities
where allowedStates.Contains( entity.State )
&& entity.Hidden == "No"
select entity
);
}
This works well and the solution is clean and understandable ( big thanks to https://stackoverflow.com/users/263693/stephen-cleary ).
What I am trying to do now is to apply a similar (or the same?) method to any EntityCollections associated to an EntityObject that also happen to implement IHideable.
I currently make use of GetActiveEntities() like this:
var products = GetActiveEntities( Entities.Products );
return (
from product in products
let latestOrder = product.Orders.FirstOrDefault(
candidateOrder => (
candidateOrder.Date == product.Orders.Max( maxOrder => maxOrder.Date )
)
)
select new Product() {
Id = product.Id ,
Name = product.Name,
LatestOrder = new Order() {
Id = latestOrder.Id ,
Amount = latestOrder.Amount,
Date = latestOrder.Date
}
}
);
In this example I would like to have the Orders EntityCollection also filter by GetActiveEntities(), so that the latest order returned can never be a "hidden" one.
Is it possible to have all EntityCollections implementing IHideable to be filtered - maybe by applying some reflection/recursion inside GetActiveEntities() and calling itself? I say recursion because the best solution would go multiple levels deep walking through the entity graph.
This stuff is stretching my brain!
UPDATE #1 (moved my comments up to here)
Thanks Steve.
Making the method accept IQuerable as suggested gives this error:
'System.Data.Objects.DataClasses.EntityCollection<Database.Order>' does not contain a definition for 'GetActiveEntities' and no extension method 'GetActiveEntities' accepting a first argument of type 'System.Data.Objects.DataClasses.EntityCollection<Database.Order>' could be found (are you missing a using directive or an assembly reference?)
I presume this is because EntityCollection does not implement IQueryable.
I was able to get further by creating a second extension method that explicitly accepted EntityCollection and returned IEnumerable. That compiled but at runtime gave this error:
LINQ to Entities does not recognize the method 'System.Collections.Generic.IEnumerable`1[Database.Order] GetActiveEntities[Order](System.Data.Objects.DataClasses.EntityCollection`1[Database.Order])' method, and this method cannot be translated into a store expression.
I also tried calling AsQueryable() on the EntityCollection and returning IQueryable but the same error came back.
Edited to use ObjectSet instead of EntityCollection
I recommend using my solution, but as an extension method, which can be applied to any query:
public static IQueryable<TEntity> WhereActive<TEntity>(
this IQueryable<TEntity> entities)
where TEntity : class , Database.IHideable
{
var allowedStates = new string[] { "Active", "Pending" };
return (
from entity in entities
where allowedStates.Contains(entity.State)
&& entity.Hidden == "No"
select entity
);
}
This can then be used as such:
var products = Entities.Products.WhereActive();
return (
from product in products
let latestOrder = (
from order in Entities.Orders.WhereActive()
where order.ProductId == product.Id
orderby candidateOrder.Date descending
select order).FirstOrDefault()
select new Product()
{
Id = product.Id,
Name = product.Name,
LatestOrder = new Order()
{
Id = latestOrder.Id,
Amount = latestOrder.Amount,
Date = latestOrder.Date
}
}
);
Related
I'm trying to join a big table to a small list of data pairs using EFCore 2.1.1. I want this join to happen server-side, rather than trying to download the whole table, e.g translating to something like:
SELECT a.*
FROM Groups AS a
INNER JOIN (VALUES (1, 'admins'), (2, 'support'), (1, 'admins')) AS b(organization_id, name)
ON a.organization_id = b.organization_id AND a.name = b.name;
or something equivalent (e.g. using common table expressions). Is this possible? If so, how? Passing a list of objects to a LINQ .join seems to always get handled client-side.
Due to massive testing debt and the EFCore 3 breaking change on client-side evaluation, upgrading is not an option for us at this time (but answers relevant to newer versions may help us push management)
If you expect that EF Core 3.x can support this, you are wrong. If you plan to upgrade your application, better think about EF Core 6 and .net 6.
Anyway I know several options:
With extension method FilterByItems or similar
var items = ...
var query = context.Groups
.FilterByItems(items, (q, b) => q.organization_id == b.organization_id && q.name == i.name, true);
With third party extension inq2db.EntityFrameworkCore version 2.x, note that I'm one of the creators. It will generate exactly the same SQL as in question.
var items = ...
var query =
from g in context.Groups
join b in items on new { g.organization_id, g.name } equals new { b.organization_id, b.name }
select g;
var result = query.ToLinqToDB().ToList();
You could solve this problem with a Table Value Parameter.
First define a database type;
IF TYPE_ID(N'[IdName]') IS NULL
CREATE TYPE [IdName] AS TABLE (
[Id] int NOT NULL
[Name] nvarchar(max) NOT NULL
)
Then you can build an SqlParameter from an IEnumerable;
public class IdName
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
public static SqlParameter ToParameter(IEnumerable<IdName> values)
{
var meta = new SqlMetaData[]{
new SqlMetaData(nameof(Id), SqlDbType.Int),
new SqlMetaData(nameof(Name), SqlDbType.NVarChar, int.MaxValue)
};
return new SqlParameter()
{
TypeName = nameof(IdName),
SqlDbType = SqlDbType.Structured,
Value = values.Select(v => {
var record = new SqlDataRecord(meta);
record.SetInt32(0, v.Id);
record.SetString(1, v.Name);
return record;
})
};
}
}
Then define an EF Core query type, and you can turn that SqlParameter into an IQueryable;
public void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IdName>(e =>
{
e.HasNoKey();
e.ToView(nameof(IdName));
});
}
public static IQueryable<IdName> ToQueryable(DbContext db, IEnumerable<IdName> values)
=> db.Set<IdName>().FromSqlInterpolated($"select * from {ToParameter(values)}");
And now you can use that IQueryable in a Linq join.
I have AutoMapper configured in my .NET 5.0 project with a map between an entity (Setting) and its DTO (SettingByProfileDto). The said entity has a child collection of another entity (SettingValue) (one to many). The child collection (SettingValues) of the first entity is mapped to a single item (another DTO : SettingValueDto) inside the DTO because I only need a specific item from this list.
For the mapping configuration, I use the following lines :
int profileId = default;
profile.CreateMap<Setting, SettingByProfileDto>()
.ForMember(dto => dto.SettingValue, opt =>
{
opt.MapFrom(ss =>
ss.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == profileId)
);
});
When I want to retrieve the first entity I use the AutoMapper ProjectTo method in order to only request the fields the DTO has. I give to the ProjectTo method the value of the profileId parameter so the mapping can know on which Id the filter has to be done :
// ...
.Where(ss => ss.Id == request.Id)
.ProjectTo<SettingByProfileDto>(
_mapper.ConfigurationProvider,
new { profileId = request.ProfileId },
dest => dest.SettingValue
)
// ...
The result of the query and the mapping are both correct. However, the query sent to the database to fetch the results seems to be poorly optimized.
Here is the resulting query :
SELECT [s8].[Description], [s8].[DisplayName], [s8].[Id], [s8].[Name], CASE
WHEN (
SELECT TOP(1) [s].[Id]
FROM [SettingValue] AS [s]
WHERE ([s8].[Id] = [s].[SettingId]) AND ([s].[ProfileId] = #__profileId_1)) IS NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, (
SELECT TOP(1) [s2].[Id]
FROM [SettingValue] AS [s2]
WHERE ([s8].[Id] = [s2].[SettingId]) AND ([s2].[ProfileId] = #__profileId_1)), (
SELECT TOP(1) [s3].[ProfileId]
FROM [SettingValue] AS [s3]
WHERE ([s8].[Id] = [s3].[SettingId]) AND ([s3].[ProfileId] = #__profileId_1)), (
SELECT TOP(1) [s4].[SettingId]
FROM [SettingValue] AS [s4]
WHERE ([s8].[Id] = [s4].[SettingId]) AND ([s4].[ProfileId] = #__profileId_1)), (
SELECT TOP(1) [s7].[Value]
FROM [SettingValue] AS [s7]
WHERE ([s8].[Id] = [s7].[SettingId]) AND ([s7].[ProfileId] = #__profileId_1))
FROM [Setting] AS [s8]
WHERE [s8].[Id] = #__request_Id_0
ORDER BY (SELECT 1)
OFFSET #__p_2 ROWS FETCH NEXT #__p_3 ROWS ONLY
And here is the query expression that is generated by AutoMapper and then transformed to SQL :
DbSet<Setting>()
.AsNoTracking()
.Where(ss => ss.Id == __request_Id_0)
.Select(dtoSetting => new Object_1800281414___SettingValue_Description_DisplayName_Id_Name{
__SettingValue = dtoSetting.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == __profileId_1),
Description = dtoSetting.Description,
DisplayName = dtoSetting.DisplayName,
Id = dtoSetting.Id,
Name = dtoSetting.Name,
}
)
.Select(dtoLet => new SettingByProfileDto{
Description = dtoLet.Description,
DisplayName = dtoLet.DisplayName,
Id = dtoLet.Id,
Name = dtoLet.Name,
SettingValue = dtoLet.__SettingValue == null ? null : new SettingValueDto{
Id = dtoLet.__SettingValue.Id,
ProfileId = dtoLet.__SettingValue.ProfileId,
SettingId = dtoLet.__SettingValue.SettingId,
Value = dtoLet.__SettingValue.Value
}
}
)
.Skip(__p_2)
.Take(__p_3)
I tried to replace the FirstOrDefault clause in the mapping configuration by a Where (with the same condition) and in this case, the query generated will use a LEFT JOIN which avoid repeating one WHERE per field. However, with this way, I can't map the child collection to a single item but only another collection (of dto).
My questions are the following :
Is there a better (more efficient?) way to achieve what I want (only keep one item of the child collection) ?
Is the above query considered optimized ? If so, I would be able to continue with my current way of doing
Thanks for your help !
AutoMapper projections work by injecting Select calls with generated mapping selectors, or just selectors in the query expression tree where the destination type doesn't match the source type.
The problem is where are these injected. For instance, in your example it generates something like this (pseudo code, MapTo just marks the mapping injection point)
SettingValue = source.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == profileId)
.MapTo<SettingByProfileDto>()
Here with the predicate version of FirstOrDefault it has no choice (well, relatively, keep reading), but even if you rewrite it to the equivalent Where(predicate) + FirstOrDefault() chain, it still injects the mapping at the end
SettingValue = source.SettingValues
.Where(ssv => ssv.ProfileId == profileId)
.FirstOrDefault()
.MapTo<SettingByProfileDto>()
which is too late, and that's why EF Core generates inefficient query.
Now, one may consider this to be EF Core query translation defect. But if the mapping is injected before the FirstOrDefault() call
SettingValue = source.SettingValues
.Where(ssv => ssv.ProfileId == profileId)
.MapTo<SettingByProfileDto>()
.FirstOrDefault()
then Core produces optimal translation.
I didn't find a way to force AM to do that, and also it is good all that to happen transparently. So I wrote a little custom extension. What it does is to to plug into AutoMapper pipeline and transform appropriately the following Enumerable extension methods (both predicate and non predicate overloads) - First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault.
Here is the source code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AutoMapper.Internal;
using AutoMapper.QueryableExtensions;
using AutoMapper.QueryableExtensions.Impl;
namespace AutoMapper
{
public static class SingeResultQueryMapper
{
public static IMapperConfigurationExpression AddSingleResultQueryMapping(this IMapperConfigurationExpression config)
{
config.Advanced.QueryableBinders.Insert(0, new Binder());
config.Advanced.QueryableResultConverters.Insert(0, new ResultConverter());
return config;
}
static string[] TargetMethodNames => new[]
{
nameof(Enumerable.First),
nameof(Enumerable.FirstOrDefault),
nameof(Enumerable.Last),
nameof(Enumerable.LastOrDefault),
nameof(Enumerable.Single),
nameof(Enumerable.SingleOrDefault),
};
static HashSet<MethodInfo> TargetMethods { get; } =
(from method in typeof(Enumerable).GetTypeInfo().DeclaredMethods
join name in TargetMethodNames
on method.Name equals name
select method).ToHashSet();
static bool IsTarget(IMemberMap propertyMap) =>
propertyMap.SourceType != propertyMap.DestinationType &&
propertyMap.ProjectToCustomSource is null &&
propertyMap.CustomMapExpression?.Body is MethodCallExpression call &&
call.Method.IsGenericMethod &&
TargetMethods.Contains(call.Method.GetGenericMethodDefinition());
class ResultConverter : IExpressionResultConverter
{
public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, IMemberMap propertyMap)
=> IsTarget(propertyMap);
public ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, IMemberMap propertyMap, LetPropertyMaps letPropertyMaps)
=> new(propertyMap.CustomMapExpression.ReplaceParameters(propertyMap.CheckCustomSource(expressionResolutionResult, letPropertyMaps)));
}
class Binder : IExpressionBinder
{
public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result)
=> IsTarget(propertyMap);
public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary<ExpressionRequest, int> typePairCount, LetPropertyMaps letPropertyMaps)
{
var call = (MethodCallExpression)result.ResolutionExpression;
var selectors = configuration.ExpressionBuilder.CreateMapExpression(
new(propertyMap.SourceType, propertyMap.DestinationType, request.MembersToExpand, request),
typePairCount, letPropertyMaps.New());
if (selectors == null) return null;
var query = call.Arguments[0];
var method = call.Method.GetGenericMethodDefinition();
if (call.Arguments.Count > 1)
{
// Predicate version of the method
// Convert query.Method(predicate) to query.Where(predicate).Method()
query = Expression.Call(typeof(Enumerable), nameof(Enumerable.Where), new[] { propertyMap.SourceType }, query, call.Arguments[1]);
method = TargetMethods.First(m => m.Name == method.Name && m.GetParameters().Length == 1);
}
method = method.MakeGenericMethod(propertyMap.DestinationType);
foreach (var selector in selectors)
query = Expression.Call(typeof(Enumerable), nameof(Enumerable.Select), new[] { selector.Parameters[0].Type, selector.ReturnType }, query, selector);
call = Expression.Call(method, query);
return Expression.Bind(propertyMap.DestinationMember, call);
}
}
}
}
Just put it in a code file inside the project where you configure AutoMapper, and then enable it with the provided configuration helper extension method (similar to Expression Mapping AutoMapper extension) like this
var mapper = new MapperConfiguration(cfg => {
cfg.AddSingleResultQueryMapping();
// The rest ...
}).CreateMapper();
or with DI
services.AddAutoMapper(cfg => {
cfg.AddSingleResultQueryMapping();
}, /* assemblies with profiles */);
And that's all. Now your original DTO, mapping and ProjectTo will produce optimal SQL query translation (single LEFT OUTER JOIN).
I've some problems with a LINQ query in C#.
I have in the database the same tables that have the same structure.
So, today, I've been troubling with my LINQ query.
More details, I want to join some tables using predicates.
I have a function that has two parameters.
The first parameter is some kind of Context (For example, it may be ProductContext, CarContext, CatContext and etc).
The second parameter is a List<something> that I will join with my first parameter - Context.
I do not want a set of methods.
I've added the sample:
public Element[] GetByIds( MyPredicateContext, Guid[] ids)
{
return
from id in ids
join element in MyPredicateContext on id equals element.Id
select
new Element
{
Id = element.Id,
Description = element.JobDescription,
};
}
If the query is correct, one basic issue that I can see is the return type is Element array whereas you are trying to return IEnumerable. Maybe doing a .ToArray() on the result set might solve the problem.
Why not
return MyPredicateContext.Where(element=>ids.Contains(element.Id))
.Select(e=>new Element()
{
Id = element.Id,
Description = element.JobDescription
}).ToArray();
First of all you can't create a new IQueryable from an array this will revert to pulling everything in memory and filtering there. You are working with expressions and not c# code when you do LINQ with SQL, this will only work on in memory stuff (IEnumerable).
Your query will work in SQL if you do it like this
from element in MyPredicateContext
where ids.Contains(element.Id)
select new Element
{
Id = element.Id,
Description = element.JobDescription,
}
Given that the type of IQueryable where T is an Interface or class.
The end method will look something like this
public interface IElement
{
Guid Id { get; }
string JobDescription { get; }
}
public Element[] GetByIds<T>(IQueryable<T> myPredicateContext, Guid[] ids) where T:IElement
{
return (from element in myPredicateContext
where ids.Contains(element.Id)
select new Element
{
Id = element.Id,
Description = element.JobDescription,
}).ToArray();
}
There are ways to do it with no Generics but they are a bit more advanced and will be hard to maintain.
Here is a method that will work on all T types and proper IQueryable will produce good sql just as I pointed out is a bit more advanced and you will need to lookup how expression work.
public static Element[] GetById<T, Tkey>(IQueryable<T> items,Tkey[] ids)
{
var type = typeof(T);
ParameterExpression param = Expression.Parameter(type);
var list = Expression.Constant(ids);
//The names of the properties you need to get if all models have them and are named the same and are the same type this will work
var idProp = Expression.Property(param, "Id");
var descriptionProp = Expression.Property(param, "JobDescription");
var contains = typeof(Enumerable).GetMethods().First(m => m.Name == "Contains" && m.GetParameters().Count() == 2).MakeGenericMethod(typeof(Tkey));
var where = Expression.Lambda<Func<T, bool>>(Expression.Call(contains, list, idProp), param);
return (items.
Where(where).
Select(Expression.Lambda<Func<T, Element>>(
Expression.MemberInit(
Expression.New(typeof(Element)),
Expression.Bind(typeof(Element).GetProperty("Id"), idProp),
Expression.Bind(typeof(Element).GetProperty("Description"), descriptionProp)),
param))).ToArray();
}
Call GetById(items, new Guid[] { Guid.NewGuid() })
Having the following example:
var myIds = db.Table1.Where(x=>x.Prop2 == myFilter).Select(x=>x.Id).ToList();
var results = db.Table2.Where(x=> myIds.Contains(x.T1)).ToList();
This part is straight forward.
However, now I am facing a "slight" change where my "filter list" has 2 properties instead of only one:
// NOTE: for stackoverflow simplification I use a basic query to
// get my "myCombinationObject".
// In reality this is a much more complex case,
// but the end result is a LIST of objects with two properties.
var myCombinationObject = db.Table3.Where(x=>x.Prop3 == myFilter)
.Select(x=> new {
Id1 = x.T1,
Id2 = x.T2
}).ToList();
var myCombinationObjectId1s = myCombinationObject.Select(x=>xId1).ToList();
var myCombinationObjectId2s = myCombinationObject.Select(x=>xId2).ToList();
// step#1 - DB SQL part
var resultsRaw = db.Tables.Where( x=>
myCombinationObjectId1s.Contains(x.Prop1)
|| myCombinationObjectId2s.Contains(x.Prop2))
.ToList();
// step#2 - Now in memory side - where I make the final combination filter.
var resultsFiltered = resultsRaw.Where( x=>
myCombinationObject.Contains(
new {Id1 = x.Prop1, Id2 = x.Prop2 }
).ToList();
My question: is it even possible to merge the step#2 in the step#1 (query in linq to entities) ?
I've managed once to do what you want, however it is pretty hard and requires changing entity model a bit. You need an entity to map type
new {Id1 = x.Prop1, Id2 = x.Prop2 }
So you need enity having 2 properties - Id1 and Id2. If you have one - great, if not then add such entity to your model:
public class CombinationObjectTable
{
public virtual Guid Id1 { get; set; }
public virtual Guid Id2 { get; set; }
}
Add it to your model:
public DbSet<CombinationObjectTable> CombinationObjectTable { get; set; }
Create new migration and apply it database (database will have now additional table CombinationObjectTable). After that you start to build a query:
DbSet<CombinationObjectTable> combinationObjectTable = context.Set<CombinationObjectTable>();
StringBuilder wholeQuery = new StringBuilder("DELETE * FROM CombinationObjectTable");
foreach(var obj in myCombinationObject)
{
wholeQuery.Append(string.Format("INSERT INTO CombinationObjectTable(Id1, Id2) VALUES('{0}', '{1}')", obj.Id1, obj.Id2);
}
wholeQuery.Append(
db.Tables
.Where( x=>
myCombinationObjectId1s.Contains(x.Prop1)
|| myCombinationObjectId2s.Contains(x.Prop2))
.Where( x=>
combinationObjectTable.Any(ct => ct.Id1 == x.Id1 && ct.Id2 == x.Id2)
).ToString();
);
var filteredResults = context.Tables.ExecuteQuery(wholeQuery.ToString());
Thanks to this your main query stays written in linq. If you do not want to add new table to your db this is as well achievable. Add new class CombinationObjectTable to model, generate new migration to add it and afterwards remove code creating that table from migration code. After that apply migration. This way the db schema won't be changed but EF will think that there is CombinationObjectTable in database. Instead of it you will need to create a temporary table to hold data:
StringBuilder wholeQuery = new StringBuilder("CREATE TABLE #TempCombinationObjectTable(Id1 uniqueidentifies, Id2 uniqueidentifier);");
And when you invoke ToString method on your linq query change CombinationObjectTable to #TempCombinationObjectTable:
...
.ToString()
.Replace("CombinationObjectTable", "#TempCombinationObjectTable")
Other thing worth considering would be using query parameters to pass values in INSERT statements instead of just including them in query yourself - this is of course achievable with EF as well. This solution is not fully ready to apply, rather some hint in which direction you may go for the solution.
Can you do something like this:
var result=
db.Tables
.Where(t=>
db.Table3
.Where(x=>x.Prop3 == myFilter)
.Any(a=>a.T1==t.Prop1 || a.T2==t.Prop2)
).ToList();
If you simply want to avoid the intermediate result (and also creating a second intermediary list) you can do the following
var resultsFiltered = db.Tables.Where( x=>
myCombinationObjectId1s.Contains(x.Prop1)
|| myCombinationObjectId2s.Contains(x.Prop2))
.AsEnumerable() // everything past that is done in memory but isn't materialized immediately, keeping the streamed logic of linq
.Where( x=>
myCombinationObject
.Contains(new {Id1 = x.Prop1, Id2 = x.Prop2 })
.ToList();
Let me first explain what I'm trying to accomplish.
I'm working with a C# ASP.NET MVC 5 project using Entity Framework to communicate with a SQL Server database. Most of the queries utilizes linq for its queries. In various places on the frontend site I'm displaying lists of records and need to provide the means of searching these records via a search bar. The initial idea right now is to allow the user to enter a search phrase with keywords being separated by spaces, and those keywords are used to match any combination of fields in the records of a table.
For example, say my search is "John Doe" against a user table. Consider these being the records in this table:
uFirstName uLastName
---------- ----------
Johnny Doe
John Doe
Jane Doe
The first two records should be returned.
Here's an example method I would call to return the results I expect:
public static List<UserModel> GetUserList(string terms)
{
using (DBConnection dbcontext = new DBConnection())
{
var termlist = (terms == "") ? new List<string>() : terms.Split(' ').ToList();
var linqList = (from u in dbcontext.Users
where
(
(terms == "") ||
(termlist.Any(_s => u.uLastName.Contains(_s))) ||
(termlist.Any(_s => u.uFirstName.Contains(_s)))
)
select new { u.uLastName, u.uFirstName });
return linqList.ToList().ConvertAll<UserModel> ( u => new UserModel { LastName = u.uLastName, FirstName = u.uFirstName } );
}
}
In my project I'm utilizing this search bar in various places being used to search against a variety of tables that obviously have different fields. What I would like to do is create a helper method that allows me to pass in the "terms" string and have it matched against a list of field values within the linq statement generically. Here's an example pseudo method that shows what I would like to change the above method to:
public static List<UserModel> GetUserList(string terms)
{
using (DBConnection dbcontext = new DBConnection())
{
var linqList = (from u in dbcontext.Users
where SearchTermMatch(terms, new List<string>() { u.uLastName, u.uFirstName }) == true
select new { u.uLastName, u.uFirstName });
return linqList.ToList().ConvertAll<UserModel>(u => new UserModel { LastName = u.uLastName, FirstName = u.uFirstName });
}
}
And this is what the helper method would look like:
public static bool SearchTermMatch(string terms, List<string> fieldvalues)
{
if (terms == "") return true;
else
{
var termlist = terms.Split(' ').ToList();
var foundlist = new List<bool>();
foreach (string value in fieldvalues)
foundlist.Add(termlist.Any(s => value.Contains(s)));
return foundlist.Any(f => f == true);
}
}
Even though this compiles fine, at runtime it produces the following error:
LINQ to Entities does not recognize the method 'Boolean SearchTermMatch(System.String, System.Collections.Generic.List`1[System.String])' method, and this method cannot be translated into a store expression.
From all my searching on how to get this working, it's clear I need to utilize Expressions, but I can't for the life of me understand how those work. What I do understand is that Entity Framework wants to convert the linq statements into a query that SQL can understand, and my helper method isn't equipped to do so.
Ultimately what I want to accomplish is to build a helper method that I can later expand upon with more advanced searching techniques. I figure if I start simple with a search on all relevant fields based on a keyword split, I can later add more complexity that I would only have to do to this helper method and all my search bars will benefit from those advancements.
So I guess what I'm looking for is your help on how I can create this helper method that I can use throughout my various linq statements in my project.
Ok, I found a solution to my question. It's not completely ideal, but it gets the job done.
Let me first give reference to the sources I'm using for my solution. I first referred to this answer as the starting point:
https://stackoverflow.com/a/27993416/4566281
This answer referred to a source that I ended up using in my project. If you're using Visual Studio, you can find the package in NuGet, just search for "neinlinq", or get it from this GitHub repository:
https://github.com/axelheer/nein-linq
The only reason I don't consider this my ideal solution is that I was hoping to stick completely to the libraries in .NET / MVC. There's nothing wrong with using a 3rd party library, and in this case, it got the job done for me. But I was hoping to accomplish this as native as possible, and within reason.
So on to my code solution, as I hope this will help someone else in some capacity.
My "helper" function(s) ended up being this (don't forget to include "using NeinLinq;")
[InjectLambda]
public static bool SearchTermMatch(List<string> termlist, List<string> fieldvalues)
{
throw new NotImplementedException();
}
public static Expression<Func<List<string>, List<string>, bool>> SearchTermMatch()
{
return (t,f) =>
(
(t.Count() == 0) ||
(t.Count(_t => f.Any(_f => _f.Contains(_t)) || _t == "") == t.Count())
);
}
And, my linq statement ended up being the following:
public static List<UserModel> GetUserList(string terms)
{
using (DBConnection dbcontext = new DBConnection())
{
var termlist = (terms == "") ? new List<string>() : terms.Split(' ').ToList();
var linqList = (from u in dbcontext.Users
where SearchTermMatch(termlist, new List<string>() { u.uLastName, u.uFirstName })
select new { u.uLastName, u.uFirstName });
return linqList.ToList().ConvertAll<UserModel>(u => new UserModel { LastName = u.uLastName, FirstName = u.uFirstName });
}
}
I also didn't like that I have to construct the "termlist" before the linq statement in order to make the comparisons I wanted. Ideally I'd like to have the "SearchTermMatch" expression to construct the list through something similar to Split so all I had to do was pass in the string "terms", but I couldn't figure out how to accomplish that in the expression. If someone has an idea on how to do that please let me know. I could then have the flexibility to establish my own set of search rules in the expression instead of having the calling linq statement make the list.
So, to come full circle on how this accomplishes my sitution, I can now repurpose SearchTermMatch for all my search bar scenarios. Take for example this statement:
var linqList = (from p in Person
join a in Address on p.AddressID equals a.AddressID
select new { p.ContactName, p.EmailAddress, a.Street, a.City, a.State, a.Zipcode });
I can now easily update it to the following to handle my search bar call:
var termlist = (terms == "") ? new List<string>() : terms.Split(' ').ToList();
var linqList = (from p in Person
join a in Address on p.AddressID equals a.AddressID
where SearchTermMatch(termlist, new List<string>() { p.ContactName, p.EmailAddress, a.Street, a.City, a.State, a.Zipcode })
select new { p.ContactName, p.EmailAddress, a.Street, a.City, a.State, a.Zipcode });