Using Full-Text Search in Linq / ODataController - c#

I am working on an application which serves data via OData. I am using ASP.Net and ODataControllers querying via EF -- the data is backed by a SQLServer database.
On the front-end website which visualizes this data, the user can search results -- a $filter is dynamically created on the front end and an OData request is sent (allowing server-side filtering).
On the database table backing the data which is eventually served via OData, full-text search is enabled, but it appears in the pipeline OData filter -> Linq query -> SQL query, a LIKE search is used instead of the full-text Contains() method.
Is there any way that anyone knows of to make this use the full-text capabilities in a reasonably elegant way?
Presumably I can do a lot of fumbling about with a custom IODataPathHandler and / or IODataPathTemplateHandler and / or some other things to intercept the points in the pipeline, but I'd rather try to avoid that if possible.
Any advice?

OData's contains function is meant to perform a simple substring match. The OData spec defines the $search query option for full-text search, but Web API does not currently support $search. (There is an open issue.)
Your best bet is probably a custom query option (e.g., /Customers?fulltextsearch=contains(Name, 'Arianne')), but you'll have to write all of the code to parse the option, etc.
If you are determined to map OData contains to T-SQL CONTAINS, then you will need to intercept the translation done by Linq to Entities. Look at the source code for the existing ContainsTranslator and work backwards.

Use an interceptor and a custom EnableQueryAttribute for that purpose:
Define a FtsInterceptor class as described in the article and add it to your context - DbInterception.Add(new FtsInterceptor()).
Define a subclass of the EnableQueryAttribute class and override the ApplyQuery method adding FullTextPrefix (-FTSPREFIX-) for all parameters of the OData contains function:
public class FullTextSearchAttribute : EnableQueryAttribute
{
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
if (queryOptions.Filter == null)
return queryOptions.ApplyTo(queryable);
const string pattern = "contains\\([%20]*[^%27]*[%20]*,[%20]*%27(?<Value>[^%27]*)";
var matchEvaluator = new MatchEvaluator(match =>
{
var value = match.Groups["Value"].Value;
return match.Value.Replace($"%27{value}", $"%27-FTSPREFIX-{value}");
});
var request = new HttpRequestMessage(HttpMethod.Get,
Regex.Replace(queryOptions.Request.RequestUri.AbsoluteUri,
pattern,
matchEvaluator,
RegexOptions.IgnoreCase));
return new ODataQueryOptions(queryOptions.Context, request).ApplyTo(queryable);
}
}
Use the attribute in your code:
[FullTextSearchAttribute]
public IQueryable<YourDomainClass> Get()
{
//Query
}

Related

HotChocolate - Enable Pagination mixed with Get All Results at the same Query Type

We have a very simple scenario where I need to return data from the database.
Some actions require pagination on the data, but others do not. For this second case, I just need to return all the data (with no pagination).
Does anyone know a way for creating a single query type to solve both situations instead of two query types?
Trying to find a good way, we needed to remove all the attributes to enable HotChocolate for returning all Users on the database, and another query for the pagination.
First Query Type
[UseDbContext(typeof(MyContext)), UseOffsetPaging, UseFiltering, UseSorting]
public IQueryable<User> Users(ClaimsPrincipal claims, [Service] MyContext context)
{
return context.Users;
}
Second Query Type (that we want to remove, and mix with a single query, mixed with the pagination features)
[UseDbContext(typeof(MyContext))]
public IQueryable<User> GetAllUsers(ClaimsPrincipal claims, [Service] MyContext context)
{
return context.Users;
}
Is there a way to expose a single query type for both scenarios? Pagination and Get All?
Thank you!
In the early versions of the "OffsetPaging" functionality it was easily achievable: if you don't set up page size and offset in your query then literally the whole list is returned.
In the newer versions, it is also possible to omit setting up the paging parameters, but some default parameters will be used anyway. As far as I understand, there's no direct way to make the return list unlimited having "OffsetPaging" turned on. As for me, I find that change strange and not always convenient, but now it is the way it is.
How do we solve that issue on our project? We just set up the default paging options to very high values:
.AddQueryType<QueryType>()
.SetPagingOptions(new PagingOptions { MaxPageSize = int.MaxValue - 1, DefaultPageSize = int.MaxValue - 1, IncludeTotalCount = true })
That allows us to omit the paging parameters in requests and, at the same time, be sure that the whole list will be returned.

MongoDb C# Driver Issue when trying to filter by enum value

I need some help, I'm new to MongoDb, and using the 2.4.4 MongoDb.Driver for .Net.
I've created a class to strongly type my collection and one of its fields is an enum, I've managed to make the query return a string instead of an int on that field when using find by adding a BsonRepresentation decorator.
My current issue is when trying to filter by that enum field, I'm trying to do the following:
return await _context.Contacts
.Find(x=> x.EnumField.ToString().Contains(searchTextParam)).ToListAsync();
So that I can filter by that field's text value, but that's throwing a runtime error:
System.ArgumentException: Unsupported filter: {document}{EnumField}.ToString().Contains("searchValue").
Thanks in advance,
Jorge
Generally speaking, the LINQ integration in the driver does not support any and every kind of LINQ statement, and in my experience, using .ToString() on a property is one of those scenarios that is not supported (i.e. cannot be parsed by the driver to be converted into a MongoDB query).
Taking inspiration from https://stackoverflow.com/a/34033905/159446, you might want to do something like this:
// assuming your class is also called Contacts
var filter = Builders<Contacts>.Filter.Regex(x => x.EnumField,
BsonRegularExpression.Create(searchTextParam));
return await _context.Contacts
.Find(filter).ToListAsync();
Augmenting the other answer, we can use FilterDefinitions with queryable using Inject() method. So just use regex filter on enum using builder and then inject it inside where linq method and we have queryable which will form a correct query.
In sample code below _.EnumType is the enum
var searchText = "SomeValue".ToLower();
var confidentialityFilter = Builders<MyType>.Filter.Regex(_ =>
_.EnumType, #$ "/{searchText}/is");
query = query.Where(_ =>
_.SomeTextField
.ToLower()
.Contains(searchText) || confidentialityFilter.Inject());
return query;

Parsing ODataQueryOptions<type> without EF/Nhibernate

I have a project with a large codebase that uses an in-house data access layer to work with the database. However, we want to support OData access to the system. I'm quite comfortable with expression trees in C#. How do I get at something I can parse here in order to get the structure of their actual query?
Is there a way to get an AST out of this thing that I can turn into sql code?
Essentially, you need to implement you own Query Provider which known how to translate the expression tree to an underlying query.
A simplified version of a controller method would be:
[ODataRoute("foo")]
public List<Foo> GetFoo(ODataQueryOptions<Foo> queryOptions)
{
var queryAllFoo = _myQueryProvider.QueryAll<Foo>();
var modifiedQuery = queryOptions.ApplyTo(queryAllFoo);
return modifiedQuery.ToList();
}
However!
This is not trivial, it took me about 1 month to implement custom OData query processing
You need to build the EDM model, so the WebApi OData can process and build right expression trees
It might involve reflection, creation of types at runtime in a dynamic assembly (for the projection), compiling lambda expressions for the best performance
WebAPI OData component has some limitations, so if you want to get relations working, you need to spend much more extra time, so in our case we did some custom query string transformation (before processing) and injecting joins into expression trees when needed
There are too many details to explain in one answer, it's a long way..
Good luck!
You can use ODataQueryOptions<T> to get abstract syntax trees for the $filter and $orderby query options. ($skip and $top are also available as parsed integers.) Since you don't need/want LINQ support, you could then simply pass the ASTs to a repository method, which would then visit the ASTs to build up the appropriate SQL stored proc invocation. You will not call ODataQueryOptions.ApplyTo. Here's a sketch:
public IEnumerable<Thing> Get(ODataQueryOptions<Thing> opts)
{
var filter = opts.Filter.FilterClause.Expression;
var ordering = opts.OrderBy.OrderByClause.Expression;
var skip = opts.Skip.Value;
var top = opts.Top.Value;
return this.Repository.GetThings(key, filter, ordering, skip, top);
}
Note that filter and ordering in the above are instances of Microsoft.OData.Core.UriParser.Semantic.SingleValueNode. That class has a convenient Accept<T> method, but you probably do not want your repository to depend on that class directly. That is, you should probably use a helper to produce an intermediate form that is independent of Microsoft's OData implementation.
If this is a common pattern, consider using parameter binding so you can get the various query options directly from the controller method's parameter list.

ASP.NET MVC3: Reusable method which returns Database query results

I want a method where I can call, that would query the database for my given query string. Which can be refferenced from different controllers/actions. (note, complexity of actual query is quite big).
So a few questions:
Where would the code go. A new controller? Helper?
How would I reference it (call it)?
What object, if following my current style, would be the return type.
public Something WebCostCentres()
{
using (var db = Database.OpenConnectionString(Mynamespace.Properties.Settings.Default.dbConnString,
"System.Data.SqlClient"))
{
//ViewBag.CostCentres = db.Query("SELECT DISTINCT CostCentreNo");
return db.Query("SELECT DISTINCT CostCentreNo");
}
}
I would create some kind of Service class for this.
You crate the service and call the method.
The same type as your Query method. IEnumerable<Something> would be an option. You might have to call ToList or ToArray to execute the query, because the connection might be closed.
The service layer is often called repository. Google for this and you will find tons of examples.
1.Where would the code go. A new controller? Helper?
A class. Service oriented architecure wise.
2.How would I reference it (call it)?
As local variable in the page, filled via your trusted IOC container.
3.What object, if following my current style, would be the return type
None. Our current style is outdated. Ever heard of LINQ? The IQueryable extensions to .NET? It is not like they are new. It should return either IEnumerable or IQueryable, and be in general generic. Or a specific type in case only one number is returned.
repository pattern is applicable too
See an example
http://mstecharchitect.blogspot.com/2009/08/aspnet-mvc-and-linq-to-sql-using.html

Dynamic LINQ with direct user input, any dangers?

I have a table in a ASP.NET MVC application that I want to be sortable (serverside) and filterable using AJAX. I wanted it to be fairly easy to use in other places and didn't feel like hardcoding the sorting and filtering into query expressions so I looked for a way to build the expressions dynamically and the best way to do this I found was with Dynamic LINQ.
User input from a URL like below is directly inserted into a dynamic Where or OrderBy.
/Orders?sortby=OrderID&order=desc&CustomerName=Microsoft
This would result in two expressions:
OrderBy("OrderID descending")
Where(#"CustomerName.Contains(""Microsoft"")")
While I understand that it won't be thrown at the database directly and inserting straight SQL in here won't work because it can't be reflected to a property and it's type-safe and all, I wonder if someone more creative than me could find a way to exploit it regardless. One exploit that I can think of is that it's possible to sort/filter on properties that are not visible in the table, but this isn't that harmful since they still wouldn't be shown and it can be prevented by hashing.
The only way I allow direct user input is with OrderBy and Where.
Just making sure, thanks :)
Because LINQ to SQL uses type-safe data model classes, you are protected from SQL Injection attacks by default. LINQ to SQL will automatically encode the values based on the underlying data type.
(c) ScottGu
But you can still get "divide by zero" there, so it is recommended to handle all unexpected exceptions and also limit length of the valid entries, JIC
Hum... I've just found at least one possible issue with the Dynamic Linq. Just exec this snippet 1000 times and watch for the CPU and memory consumption going high up (creating an easy way for the denial of service attack):
var lambda = DynamicExpression
.ParseLambda<Order, bool>("Customer=string.Format(\"{0,9999999}"+
"{0,9999999}{0,9999999}{0,9999999}{0,9999999}\",Customer)")
.Compile();
var arg = new Order
{
Total = 11
};
Console.WriteLine(lambda(arg));
I wrote a blog post on that.
Just a thought, but have you looked at ADO.NET Data Services? This provides a REST-enabled API much like the above with a lot of standard LINQ functionality built in.
I can't think of an interest dynamic LINQ exploit of the top of my head, but if this was me I'd be at least white-listing members (OrderID, CustomerName, etc) - but I'd probably write the Expression logic directly; it isn't especially hard if you are only supporting direct properties.
For example, here is Where (using your Contains logic):
static IQueryable<T> Where<T>(this IQueryable<T> source,
string member, string value)
{
var param = Expression.Parameter(typeof(T), "x");
var arg = Expression.Constant(value, typeof(string));
var prop = Expression.PropertyOrField(param, member);
MethodInfo method = typeof(string).GetMethod(
"Contains", new[] { typeof(string) });
var invoke = Expression.Call(prop, method, arg);
var lambda = Expression.Lambda<Func<T, bool>>(invoke, param);
return source.Where(lambda);
}
I've covered OrderBy previously, here.

Categories

Resources