I'm constructing a simple search function for a site. The aim is to allow the user to find 'swimmers' and add them to a list via a search box, using a query like "bob staff".
The first part of this I decided to tackle was allowing the user to search via a group name (In the database all swimmers are part of a group). The following is the code I have at the moment.
[HttpGet]
public JsonResult searchForSwimmers(string q)
{
//designed to return [{name='foo',id='bar'}]
String[] QueryTerms = q.Split(' '); //all the search terms are sep. by " "
var groupResults = _db.SwimGroups.Where(g => g.Name.ContainsAny(QueryTerms))
.OrderByDescending(g => g.Name.StartsWithAny(QueryTerms) ? 1 : 0)
.ThenBy( g => g)
.Select(g => new { name = g.Name, id = g.ID });
return Json(groupResults,JsonRequestBehavior.AllowGet);
}
On line 8, there is a method invoked called StartsWithAny. This is an extension method I defined in the following file:
public static class StringUtils
{
public static Boolean StartsWithAny(this String str, params String[] Fragments)
{
foreach (String fragment in Fragments)
{
if (str.StartsWith(fragment))
{
return true;
}
}
return false;
}
}
The idea is that if a Name starts with one of the terms then it should be ranked higher in relevancy. I recognize that this logic is naïve and has flaws however I thought it would be a good example to illustrate the problem I've having. The code compiles however when searchForSimmers is invoked in my cshtml page with the following: (using the tokenInput library)
<script type="text/javascript">
$(document).ready(function () {
$("#demo-input-local").tokenInput("/Admin/searchForSwimmers");
});
</script>
I get a 500 internal server error. The error message is as follows:
LINQ to Entities does not recognize the method 'Boolean ContainsAny(System.String, System.String[])' method, and this method cannot be translated into a store expression.
The ContainsAny method
public static Boolean ContainsAny(this String str, List<String> Fragments)
{
foreach (String fragment in Fragments)
{
if(str.Contains(fragment))
{
return true;
}
}
return false;
}
I've had a look around but couldn't find a solution to the problem. Any help would be greatly appreciated, cheers.
Check out my blog where I create a search extension method for IQueryable.
UPDATED: I have created a new blog post showing the extension method required to acheive your goal
http://www.ninjanye.co.uk/2013/04/generic-iqueryable-or-search-for.html
http://jnye.co/Posts/8/generic-iqueryable-or-search-for-multiple-search-terms-using-expression-trees
The problem is linq does not know how to translate your code into SQL.
By adding the following extension method:
public static class QueryableExtensions
{
public static IQueryable<T> Search<T>(this IQueryable<T> source, Expression<Func<T, string>> stringProperty, params string[] searchTerms)
{
if (!searchTerms.Any())
{
return source;
}
Expression orExpression = null;
foreach (var searchTerm in searchTerms)
{
//Create expression to represent x.[property].Contains(searchTerm)
var searchTermExpression = Expression.Constant(searchTerm);
var containsExpression = BuildContainsExpression(stringProperty, searchTermExpression);
orExpression = BuildOrExpression(orExpression, containsExpression);
}
var completeExpression = Expression.Lambda<Func<T, bool>>(orExpression, stringProperty.Parameters);
return source.Where(completeExpression);
}
private static Expression BuildOrExpression(Expression existingExpression, Expression expressionToAdd)
{
if (existingExpression == null)
{
return expressionToAdd;
}
//Build 'OR' expression for each property
return Expression.OrElse(existingExpression, expressionToAdd);
}
private static MethodCallExpression BuildContainsExpression<T>(Expression<Func<T, string>> stringProperty, ConstantExpression searchTermExpression)
{
return Expression.Call(stringProperty.Body, typeof(string).GetMethod("Contains"), searchTermExpression);
}
}
This will allow you to write the following lambda:
[HttpGet]
public JsonResult searchForSwimmers(string q)
{
//designed to return [{name='foo',id='bar'}]
String[] QueryTerms = q.Split(' '); //all the search terms are sep. by " "
var groupResults = _db.SwimGroups.Search(g => g.Name, queryTerms)
.OrderByDescending(g => g.Name.StartsWithAny(QueryTerms) ? 1 : 0)
.ThenBy( g => g)
.Select(g => new { name = g.Name, id = g.ID });
return Json(groupResults,JsonRequestBehavior.AllowGet);
}
That's because neither your ContainsAny or StartsWithAny extension methods can be translated into SQL.
As your database table is small (as noted in comments), just resolve your query, by calling .ToList() before you do the Where and OrderBy.
Try this:
var groupResults = _db.SwimGroups
.ToList() //evaluate the query, bring it into memory
.Where(g => g.Name.ContainsAny(QueryTerms))
.OrderByDescending(g => g.Name.StartsWithAny(QueryTerms) ? 1 : 0)
.ThenBy( g => g)
.Select(g => new { name = g.Name, id = g.ID });
Related
It's possible to declare a lambda function and immediately call it:
Func<int, int> lambda = (input) => { return 1; };
int output = lambda(0);
I'm wondering if it's possible to do so in one line, e.g. something like
int output = (input) => { return 1; }(0);
which gives a compiler error "Method name expected". Casting to Func<int, int> doesn't work either:
int output = (Func<int, int>)((input) => { return 1; })(0);
gives the same error, and for reasons mentioned below I'd like to avoid having to explicitly specify the input argument type (the first int).
You're probably wondering why I want to do this, instead of just embedding the code directly, e.g. int output = 1;. The reason is as follows: I've generated a reference for a SOAP webservice with svcutil, which because of the nested elements generates extremely long class names, which I'd like to avoid having to type out. So instead of
var o = await client.GetOrderAsync(request);
return new Order {
OrderDate = o.OrderDate,
...
Shipments = o.Shipment_Order == null ? new Shipment[0]
o.Shipment_Order.Select(sh => new Shipment {
ShipmentID = sh.ShipmentID,
...
Address = CreateAddress(sh.ReceiverAddress_Shipment);
}).ToArray()
};
and a separate CreateAddress(GetOrderResultOrderShipment_OrderShipmentShipment_Address address) method (real names are even longer, and I have very limited control about the form), I'd like to write
var o = await client.GetOrderAsync(request);
return new Order {
OrderDate = o.OrderDate,
...
Shipments = o.Shipment_Order == null ? new Shipment[0]
o.Shipment_Order.Select(sh => new Shipment {
ShipmentID = sh.ShipmentID,
...
Address = sh.ReceiverAddress_Shipment == null ? null : () => {
var a = sh.ReceiverAddress_Shipment.Address;
return new Address {
Street = a.Street
...
};
}()
}).ToArray()
};
I know I could write
Address = sh.ReceiverAddress_Shipment == null ? null : new Address {
Street = sh.ReceiverAddress_Shipment.Address.Street,
...
}
but even that (the sh.ReceiverAddress_Shipment.Address part) becomes very repetitive if there are many fields. Declaring a lambda and immediately calling it would be more elegant less characters to write.
Instead of trying to cast the lambda, I propose you use a small helper function:
public static TOut Exec<TIn, TOut>(Func<TIn, TOut> func, TIn input) => func(input);
which you could then use like this: int x = Exec(myVar => myVar + 2, 0);. This reads a lot nicer to me than the alternatives suggested here.
It's ugly, but it's possible:
int output = ((Func<int, int>)(input => { return 1; }))(0);
Anonymous functions, including lambda expressions, are implicitly convertible to a delegate that matches their signature, but this syntax requires the lambda to be enclosed in parentheses.
The above can be simplified as well:
int output = ((Func<int, int>)(input => 1))(0);
Lambda literals in C# have a curious distinction in that their meaning is dependent on their type. They are essentially overloaded on their return type which is something does not exist anywhere else in C#. (Numeric literals are somewhat similar.)
The exact same lambda literal can either evaluate to an anonymous function that you can execute (i.e. a Func/Action) or an abstract representation of the operations inside of the Body, kind of like an Abstract Syntax Tree (i.e. a LINQ Expression Tree).
The latter is, for example, how LINQ to SQL, LINQ to XML, etc. work: the lambdas do not evaluate to executable code, they evaluate to LINQ Expression Trees, and the LINQ provider can then use those Expression Trees to understand what the body of the lambda is doing and generate e.g. a SQL Query from that.
In your case, there is no way for the compiler to know wheter the lambda literal is supposed to be evaluated to a Func or a LINQ Expression. That is why Johnathan Barclay's answer works: it gives a type to the lambda expression and therefore, the compiler knows that you want a Func with compiled code that executes the body of your lambda instead of an un-evaluated LINQ Expression Tree that represents the code inside the body of the lambda.
You could inline the declaration of the Func by doing
int output = (new Func<int, int>(() => { return 1; }))(0);
and immediately invoking it.
You can also create the alias in the Select method
var o = await client.GetOrderAsync(request);
return new Order {
OrderDate = o.OrderDate,
...
Shipments = o.Shipment_Order == null ? new Shipment[0]
o.Shipment_Order.Select(sh => {
var s = sh.ReceiverAddress_Shipment;
var a = s.Address;
return new Shipment {
ShipmentID = sh.ShipmentID,
...
Address = s == null ?
null :
new Address {
Street = a.Street
...
}
};
}).ToArray()
};
or with the ?? operator
var o = await client.GetOrderAsync(request);
return new Order {
OrderDate = o.OrderDate,
...
Shipments = o.Shipment_Order?.Select(sh => {
var s = sh.ReceiverAddress_Shipment;
var a = s.Address;
return new Shipment {
ShipmentID = sh.ShipmentID,
...
Address = s == null ?
null :
new Address {
Street = a.Street
...
}
};
}).ToArray() ?? new Shipment[0]
};
If you don't mind violating a few of the extension methods design guidelines, extension methods combined with null-conditional operator ?. can take you reasonably far:
public static class Extensions
{
public static TOut Map<TIn, TOut>(this TIn value, Func<TIn, TOut> map)
where TIn : class
=> value == null ? default(TOut) : map(value);
public static IEnumerable<T> OrEmpty<T>(this IEnumerable<T> items)
=> items ?? Enumerable.Empty<T>();
}
will give you this:
return new Order
{
OrderDate = o.OrderDate,
Shipments = o.Shipment_Order.OrEmpty().Select(sh => new Shipment
{
ShipmentID = sh.ShipmentID,
Address = sh.ReceiverAddress_Shipment?.Address.Map(a => new Address
{
Street = a.Street
})
}).ToArray()
};
and if you mostly need arrays, then override ToArray extension method to encapsulate a few more method calls:
public static TOut[] ToArray<TIn, TOut>(this IEnumerable<TIn> items, Func<TIn, TOut> map)
=> items == null ? new TOut[0] : items.Select(map).ToArray();
resulting in:
return new Order
{
OrderDate = o.OrderDate,
Shipments = o.Shipment_Order.ToArray(sh => new Shipment
{
ShipmentID = sh.ShipmentID,
Address = sh.ReceiverAddress_Shipment?.Address.Map(a => new Address
{
Street = a.Street
})
})
};
I am getting the "The LINQ expression node type 'Invoke' is not supported in LINQ to Entities." error with this bit of code.
Function Call
IEnumerable<OrderListItem> orders;
orders = _service.GetAllForUser<OrderListItem>(userName, mapOrderToListItem);
var foo = orders.ToArray();
Functions
public IEnumerable<T> GetAllForUser<T>(string userName, Func<Order, T> mapper)
{
string displayName = _adService.GetDisplayName(userName);
return _repo.Query().Where(x => x.OrderedBy == userName)
.OrderBy(x => x.OrderDate)
.Select(x => mapper(x))
.AsEnumerable();
}
private OrderListItem mapOrderToListItem(Order order)
{
OrderListItem result = new OrderListItem
{
DeliveryLoc = order.OrderGroup.DeliveryLocation.Name,
Department = order.OrderGroup.Department.Name,
Id = order.Id,
OrderDate = order.OrderDate,
OrderedBy = order.OrderedBy,
Status = !order.ApprovedDate.HasValue ? "SUBMITTED" : !order.ReceivedDate.HasValue ? order.ApprovalStatus == ApprovalStatus.Approved ? "APPROVED" : "DENIED" : !order.FilledDate.HasValue ? "RECEIVED" : "FILLED"
};
return result;
}
however i do not receive the invoke error with this bit of code
Function Call
IEnumerable<ProductListItem> products;
products = _service.SearchAll<ProductListItem>(sSearch, 0, all, orderBy, sortDir, productListItemMapper);
var foo = products.ToArray();
Function
public IEnumerable<T> SearchAll<T>(string sSearch, int skip, int take, Func<Product, IComparable> sortCol, DAL.SortDir sortDir, Func<Product, T> mapper)
{
switch (sortDir)
{
case DAL.SortDir.asc:
return _repo.Query().Where(r => (r.Name.Contains(sSearch) || r.CatalogNumber.Contains(sSearch)))
.OrderBy(sortCol).Skip(skip).Take(take).Select(x => mapper(x)).AsEnumerable();
case DAL.SortDir.dsc:
return _repo.Query().Where(r => (r.Name.Contains(sSearch) || r.CatalogNumber.Contains(sSearch)))
.OrderByDescending(sortCol).Skip(skip).Take(take).Select(x => mapper(x)).AsEnumerable();
default:
throw new ArgumentException("sortDir");
}
}
private ProductListItem productListItemMapper(Product product)
{
ProductListItem output = new ProductListItem
{
Id = product.Id,
Location = product.WarehouseLocation.Name,
Name = product.Name,
Active = product.Active,
Number = product.CatalogNumber,
Units = product.UnitOfMeasure.Name,
UnitsId = product.UnitOfMeasureId,
LocationId = product.WarehouseLocationId
};
return output;
}
i feel like i'm just blind and missing something simple. Why does it work for products but not for orders?
Just for the error message I can think it's because you're trying to use the invoke on the sql server side, try to convert the result first to a collection (list/array) then use your function, so instead "select(e => mapper(e))" do first a ToList() then a select. This is because and IQueryable executes your commands on sql and sql doesn't know about your mapper function.
Edit: keep in mind ToList will retrieve all the records from the db.
I have the following code with linq query:
class Program
{
static void Main(string[] args)
{
var allProfessionals = new Collection<Professional>
{
new Professional { Name = "Bruno Paulovich Silva" },
new Professional { Name = "Ivan Silva Paulovich Bruno"},
new Professional { Name = "Camila Campos"}
};
var namesSearch = new[] {"bruno", "silva"};
var query = namesSearch.Aggregate(allProfessionals.AsQueryable(), (current, nome) => current.Where(oh => oh.Name.ToLower().Contains(nome.ToLower())));
foreach (var res in query.ToList())
{
Console.WriteLine(res.Name.ToLower());
}
}
}
The result is:
Bruno Paulovich Silva
Silva Paulovich Bruno
I wonder know how I could turn linq query Aggregate for a generic method where I can re-use at another time.
in the example below I show what I understand of a generic query that is uses in the project:
public IQueryable<T> QueryBy(Expression<Func<T, bool>> criteria)
{
return DbSet.Where(criteria);
}
ps: sorry for my bad english
Sticking to bare essentials and to the same output as your script, it would be something like:
class Program
{
static void Main(string[] args)
{
var allProfessionals =
new Collection<Professional>
{
new Professional {Name = "Bruno Paulovich Silva"},
new Professional {Name = "Ivan Silva Paulovich Bruno"},
new Professional {Name = "Camila Campos"}
};
var namesSearch = new[] {"bruno", "silva"};
var items = allProfessionals
.Select(x => x.Name)
.ContainsAll(namesSearch);
foreach (var res in items)
{
Console.WriteLine(res);
}
}
}
static class Extensions
{
public static IEnumerable<string> ContainsAll(this IEnumerable<string> haystacks, IEnumerable<string> needles)
{
var lowerNeedles = needles.Select(x => x.ToLower()).ToList();
var lowerHay = haystacks.Select(x => x.ToLower()).ToList();
// note that Regex may be faster than .Contains with larger haystacks
return lowerNeedles
.Where(hay => lowerHay.All(hay.Contains)); // or .Any(), depending on your requirements
}
}
Note that if you use LinqToSQL or similar technologies, table indexes may not be used. This may render the query very slow.
In order to make it fit with the mentioned QueryBy<T> it may look like:
var items = allProfessionals.QueryBy(
professional => namesSearch
.Select(needle => needle.ToLower()) // convert all to lower case
.All(hay => professional.Name.ToLower().Contains(hay))); // then try to search for a professional that matches all nameSearch.
Here is how I implement a method for LikeBy where an array of string is equivalent to a contains all.
public IQueryable<T> LikeBy(string[] strings, Func<IQueryable<T>, string, IQueryable<T>> criteria)
{
return strings.Aggregate(GetAll(), criteria);
}
Here the example of how implement:
public IQueryable<ViaturaFuncao> FindByNome(string names)
{
return Uow.Professional.LikeBy(names.Trim().Split(' '),
(professional, nameProfessional) =>
professional.Where(
f => f.Name.ToLower().Contains(nameProfessional.ToLower())));
}
PS: In the project we're using UnitOfWork and that is why has the UOW before the entity
I have a case in my application where the user can search for a list of terms. The search needs to make three passes in the following order:
One for an exact match of what they entered. Done, easy.
One where all the words (individually) match. Done, also easy.
One where any of the words match...how?
Essentially, how do I, in Linq to Sql, tell it to do this:
select * from stuff s where s.Title like '%blah%' || s.Title like '%woo&' || s.Title like '%fghwgads%' || s.Title like...
And so on?
This might be a tough one... I think you'd have to write your own operator.
(Update: Yep, I tested it, it works.)
public static class QueryExtensions
{
public static IQueryable<TEntity> LikeAny<TEntity>(
this IQueryable<TEntity> query,
Expression<Func<TEntity, string>> selector,
IEnumerable<string> values)
{
if (selector == null)
{
throw new ArgumentNullException("selector");
}
if (values == null)
{
throw new ArgumentNullException("values");
}
if (!values.Any())
{
return query;
}
var p = selector.Parameters.Single();
var conditions = values.Select(v =>
(Expression)Expression.Call(typeof(SqlMethods), "Like", null,
selector.Body, Expression.Constant("%" + v + "%")));
var body = conditions.Aggregate((acc, c) => Expression.Or(acc, c));
return query.Where(Expression.Lambda<Func<TEntity, bool>>(body, p));
}
}
Then you could call this with:
string[] terms = new string[] { "blah", "woo", "fghwgads" };
var results = stuff.LikeAny(s => s.Title, terms);
P.S. You'll need to add the System.Linq.Expressions and System.Data.Linq.SqlClient namespaces to your namespaces for the QueryExtensions class.
Suppose I have the two following Linq-To-SQL queries I want to refactor:
var someValue1 = 0;
var someValue2= 0;
var query1 = db.TableAs.Where( a => a.TableBs.Count() > someValue1 )
.Take( 10 );
var query2 = db.TableAs.Where( a => a.TableBs.First().item1 == someValue2)
.Take( 10 );
Note that only the Where parameter changes. There is any way to put the query inside a method and pass the Where parameter as an argument?
All the solutions posted in the previous question have been tried and failed in runtime when I try to enumerate the result.
The exception thrown was: "Unsupported overload used for query operator 'Where'"
Absolutely. You'd write:
public IQueryable<A> First10(Expression<Func<A,bool>> predicate)
{
return db.TableAs.Where(predicate).Take(10);
}
(That's assuming that TableA is IQueryable<A>.)
Call it with:
var someValue1 = 0;
var someValue2= 0;
var query1 = First10(a => a.TableBs.Count() > someValue1);
var query2 = First10(a => a.TableBs.First().item1 == someValue2);
I believe that will work...
The difference between this and the answers to your previous question is basically that this method takes Expression<Func<T,bool>> instead of just Func<T,bool> so it ends up using Queryable.Where instead of Enumerable.Where.
If you really want reusability you can try to write your own operators. E.g. instead of repeatedly writing:
var query =
Products
.Where(p => p.Description.Contains(description))
.Where(p => p.Discontinued == discontinued);
you can write simple methods:
public static IEnumerable<Product> ByName(this IEnumerable<Product> products, string description)
{
return products.Where(p => p.Description.Contains(description));
}
public static IEnumerable<Product> AreDiscontinued(IEnumerable<Product> products, bool isDiscontinued)
{
return products.Where(p => p.Discontinued == discontinued);
}
and then use it like this:
var query = Products.ByName("widget").AreDiscontinued(false);