I have a method with the following signatures that return a C# expression
Expression<Func<T, bool>> GetExpression<T>(IList<Filter> filters)
Then the following code that uses Dynamic LINQ
using (TestContext tEntities = new TestContext())
{
var filterExp = Exp.ExpressionBuilder.GetExpression<Client>(filters);
var filteredCollection = tEntities.Client.Where(filterExp);
IQueryable<Client> queryResult;
if (filterExp == null)
queryResult = tEntities.Client;
else
queryResult = tEntities.Client.Where(filterExp);
}
This is a simple scenario.
I have queries that are 50 lines long sometimes more. I want to avoid having the same code twice with only difference using the where clause.
Does anyone knows if I achieve something different?
from product in context.Product.Where(deleg)
.Include(x => x.Type)
.Include(x => x.Category)
.Include(x => x.WareHouse)
.Include(x => x.Photos)
join f in context.Favorite on product.Id equals f.ProductFid into fg
from fgi in fg.Where(f => f.UserFid == userId).DefaultIfEmpty()
orderby product.Id descending
select new ProductngDto()
{
ProductItem = product,
FavoriteId = fgi != null ? fgi.Id : (long?)null
}).Skip(page * pageSize).Take(pageSize);
Thanks in advance
One of the nice things about IQueryable<T> and IEnumerable<T> is the fact that they are so abstract, you can easily chain them together. One solution could be to structure your code like this:
using (TestContext tEntities = new TestContext())
{
var filterExp = Exp.ExpressionBuilder.GetExpression<Client>(filters);
var filteredCollection = tEntities.Client.Where(filterExp);
IQueryable<Client> queryResult = tEntities.Client;
if (filterExp != null)
{
queryResult = queryResult.Where(filterExp);
}
//do something else with queryResult
}
This way you can continue using queryResult without having to know, or even care, about whether or not filterExp was applied.
For the second example, shuffling it around could look something like this:
var query = from p in context.Product
.Include(x => x.Type)
.Include(x => x.Category)
.Include(x => x.WareHouse)
.Include(x => x.Photos);
if (deleg != null)
{
query = query.Where(deleg);
}
query = from product in query
join f in context.Favorite on product.Id equals f.ProductFid into fg
from fgi in fg.Where(f => f.UserFid == userId).DefaultIfEmpty();
orderby product.Id descending
select new ProductngDto()
{
ProductItem = product,
FavoriteId = fgi != null ? fgi.Id : (long?)null
}).Skip(page * pageSize).Take(pageSize);
Another option you have, is to check the filterExp for null and assign an "always true" lambda to it.
If you control the GetExpression<T> method, you could add it in there as a last step. If you don't control it, you could do the null checking in the methods where you use it's result.
This will allow you to keep your code looking a bit cleaner, but the trade-off is a small performance hit for having to evaluate the lambda all the time.
Depending on how your expressions are typed, this might be as easy as:
if (filterExp == null)
{
filterExp = (_) => true;
}
Related
I've looked at several possible solutions to this problem, and the ones I have tried do not seem to work. One solution was to use if statements for the optional filters, which doesn't work because I have multiple joins and the where clause is in the last join.
The optional parameters are: roleId, disciplineId, resourceId, and projectName.
try
{
IQueryable<ProjectPlanHeader> bob =
(
from h in context.ProjectPlanHeaders
join r in context.ProjectPlanRevisions on h.ProjectPlanHeaderId equals r.ProjectPlanHeaderId
join a in context.PlanActivityLineItems on r.PlanRevisionId equals a.PlanRevisionId
where ((roleId == null || a.RequiredRoleId == roleId) &&
(disciplineId == null || a.DisciplineId == disciplineId) &&
(resourceId == null || a.ActualResourceId == resourceId) &&
(h.ProjectPlanName.ToLower().Contains(projectName.ToLower()) || projectName == String.Empty))
select h
)
.Include(x => x.ProjectPlanRevisions)
.ThenInclude(y => y.PlanActivityLineItem)
.ThenInclude(z => z.PlannedHours)
.Include(x => x.ActualPlanRevisions)
.ThenInclude(y => y.ActualPlanActivities)
.ThenInclude(z => z.ActualHours);
var john = bob.ToList();
return bob;
}
catch (Exception ex)
{
return null;
}
I added the try/catch so I could see what was happening, as it was silently failing. What I found was a "Object not set to an instance of an object". That's never helpful, because I don't know what object it's talking about. Can someone please show me how to do this the right way?
UPDATE: Thanks for the responses I got, but unfortunately they don't work. The problem is that I end up getting multiple headers back when I filter. This happens because there are multiple revisions for each header, and I really only need the max rev. I tried changing the initial query so that only the max rev was included, and that still did not help. There does not appear to be a solution for this issue, so I will have to do it another way.
Rewrite query to do not use explicit joins, because you have navigation properties. Also because of JOINS you have duplicated records, you will discover it later.
var query = context.ProjectPlanHeaders
.Include(x => x.ProjectPlanRevisions)
.ThenInclude(y => y.PlanActivityLineItem)
.ThenInclude(z => z.PlannedHours)
.Include(x => x.ActualPlanRevisions)
.ThenInclude(y => y.ActualPlanActivities)
.ThenInclude(z => z.ActualHours)
.AsQueryable();
if (!string.IsNullOrEmpty(projectName))
{
// here we can combine query
query = query
.Where(h => h.ProjectPlanName.ToLower().Contains(projectName.ToLower()));
}
// check that we have to apply filter on collection
if (roleId != null || disciplineId != null || resourceId != null)
{
// here we have to do filtering as in original query
query = query
.Where(h => h.ProjectPlanRevisions
.Where(r => roleId == null || r.PlanActivityLineItem.RequiredRoleId == roleId)
.Where(r => disciplineId == null || r.PlanActivityLineItem.DisciplineId == disciplineId)
.Where(r => resourceId == null || r.PlanActivityLineItem.ActualResourceId == resourceId)
.Any()
);
}
var result = query.ToList();
Let me clarify my comment with an example:
So: An IQueryable is used to build up an expression tree. They are not evaluated until enummerated (e.g. ToList or FirstOrDefault). I.e. you can conditional add Where and Includes with little to no cost before ennumerating`
Thus you could do this,
IQueryable<ProjectPlanHeader> bob =
context.ProjectPlanHeader
.Include(x => x.ProjectPlanRevisions)
.ThenInclude(y => y.PlanActivityLineItem);
if (roleId != null) {
bob =
from h in bob
join r in context.ProjectPlanRevisions on h.Id equals r.ProjectPlanHeaderId
join a in context.PlanActivityLineItems on r.Id equals a.ProjectPlanRevisionId
where a.RequiredRoleId == roleId
select h;
}
// same for the rest.
var john = bob.ToList();
writing the chained filer is not easiest, but it works
Can anyone suggest to me which design pattern should I use to handle multiple if conditions for a search function.
The search function takes the product's name, type, and location. In my handler, I handle the input by using if conditions as the example below.
if (!string.isNullOrEmpty(ProductName) && !string.isNullOrEmpty(ProductType))
{
// Query product and return base on name and type.
var product = database.product
.Where(x => x.productname == productname)
.Where(x => x.producttype == producttype)
.ToList()
}
else if (!string.isNullOrEmpty(ProductName)
&& !string.isNullOrEmpty(ProductType)
&& !string.isNullOrEmpty(ProductLocation))
{
// Query product and return base on name and location.
var product = database.product
.Where(x => x.productname == productname)
.Where(x => x.ProductLocation == ProductLocation)
.ToList()
}
So, I ended up having multiples if conditions in my handler. Code starts to get bigger and bigger. In the future, when I may have new types of input. Especially, each if condition will have the same query function and only where the condition is added or removed base on inputs.
Is there a better way to handle inputs and remove duplicated query function?
It is not design pattern but common way when using LINQ
var query = database.product.AsQueryable();
if (!string.IsNullOrEmpty(productName))
query = database.product.Where(x => x.productname == productname);
if (!string.IsNullOrEmpty(productType))
query = database.product.Where(x => x.producttype == producttype);
var product = query.ToList();
Or via helper function:
public static class MyQueryableExtensions
{
public staic IQueryble<T> WhereIf<T>(this IQueryable<T> source, bool condiion, Expression<Func<T, bool> predicate)
{
if (condition)
source = source.Where(predicate);
return source;
}
}
var product = database.product
.WhereIf(!string.IsNullOrEmpty(productName), x => x.productname == productname)
.WhereIf(!string.IsNullOrEmpty(productType), x => x.producttype == producttype)
.ToList();
How would you write a linq query with the following SQL statement. I've tried several methods referenced on stackoverflow but they either don't work with the EF version I'm using (EF core 3.5.1) or the DBMS (SQL Server).
select a.ProductID, a.DateTimeStamp, a.LastPrice
from Products a
where a.DateTimeStamp = (select max(DateTimeStamp) from Products where a.ProductID = ProductID)
For reference, a couple that I've tried (both get run-time errors).
var results = _context.Products
.GroupBy(s => s.ProductID)
.Select(s => s.OrderByDescending(x => x.DateTimeStamp).FirstOrDefault());
var results = _context.Products
.GroupBy(x => new { x.ProductID, x.DateTimeStamp })
.SelectMany(y => y.OrderByDescending(z => z.DateTimeStamp).Take(1))
Thanks!
I understand you would like to have a list of the latest prices of each products?
First of all I prefer to use group by option even over 1st query
select a.ProductID, a.DateTimeStamp, a.LastPrice
from Products a
where a.DateTimeStamp IN (select max(DateTimeStamp) from Products group by ProductID)
Later Linq:
var maxDateTimeStamps = _context.Products
.GroupBy(s => s.ProductID)
.Select(s => s.Max(x => x.DateTimeStamp)).ToArray();
var results = _context.Products.Where(s=>maxDateTimeStamps.Contains(s.DateTimeStamp));
-- all assuming that max datetime stamps are unique
I've managed to do it with the following which replicates the correlated sub query in the original post (other than using TOP and order by instead of the Max aggregate), though I feel like there must be a more elegant way to do this.
var results = from x
in _context.Products
where x.DateTimeStamp == (from y
in _context.Products
where y.ProductID == x.ProductID
orderby y.DateTimeStamp descending
select y.DateTimeStamp
).FirstOrDefault()
select x;
I prefer to break up these queries into IQueryable parts, do you can debug each "step".
Something like this:
IQueryable<ProductOrmEntity> pocoPerParentMaxUpdateDates =
entityDbContext.Products
//.Where(itm => itm.x == 1)/*if you need where */
.GroupBy(i => i.ProductID)
.Select(g => new ProductOrmEntity
{
ProductID = g.Key,
DateTimeStamp = g.Max(row => row.DateTimeStamp)
});
//// next line for debugging..do not leave in for production code
var temppocoPerParentMaxUpdateDates = pocoPerParentMaxUpdateDates.ToListAsync(CancellationToken.None);
IQueryable<ProductOrmEntity> filteredChildren =
from itm
in entityDbContext.Products
join pocoMaxUpdateDatePerParent in pocoPerParentMaxUpdateDates
on new { a = itm.DateTimeStamp, b = itm.ProductID }
equals
new { a = pocoMaxUpdateDatePerParent.DateTimeStamp, b = pocoMaxUpdateDatePerParent.ProductID }
// where
;
IEnumerable<ProductOrmEntity> hereIsWhatIWantItems = filteredChildren.ToListAsync(CancellationToken.None);
That last step, I am putting in an anonymous object. You can put the data in a "new ProductOrmEntity() { ProductID = pocoMaxUpdateDatePerParent.ProductID }...or you can get the FULL ProductOrmEntity object. Your original code, I don't know if getting all columns of the Product object is what you want, or only some of the columns of the object.
I have this expression, which generates single query to the database:
db = new MyDataContext();
var productInCity = db.Products
.Where(n => n.id == 2)
.Select(k => new ProductInCityDto()
{
ProductName = k.ProductName,
CityName = k.Store.City.Name,
CountryName = k.Store.City.Country.Name
.
.
.
})
.FirstOrDefault();
I want to make this code cleaner, by putting the mapping in a function, extension method or in the object's constructor, something like this:
db = new MyDataContext();
var productInCity = db.Products
.Where(n => n.id == 2)
.Select(k => new ProductInCityDto(k))
.FirstOrDefault();
But, in this case, multiple queries to the DB are generated (I use LinqToSql Profiler).
Is there a way to isolate the mapping (Select statement) in order to achieve better code readability?
YES, if you look at the actual signature of the Select extension method on IQueryable you will find that it does not take a function but an Expression>.
So, just do that...
Expression<Func<Product, ProductInCityDto>> MyMappingExpression
{
get
{
return product => new ProductInCityDto
{
...
}
}
}
and then
db = new MyDataContext();
var productInCity = db.Products.Where(n => n.id == 2)
.Select(MyMappingExpression)
.FirstOrDefault();
If you need to use MyMappingExpression in process you will likely want to convert it to a
Func<Product, ProductInCityDto>
by calling the Expression.Compile() method.
Instead of creating mappings by hand, you may use AutoMapper. But if you do not want to create the mapping using a third party tool just change the query to the following;
var productInCity = new ProductInCity(
db.Products.Include("Store").SingleOrDefault(n => n.id == 2));
I have the following query that performs a "AND" on-demand:
var products = // Selecting a list of products in anywhere based in a filter...
foreach (var product in products)
{
query = query.Where(p => p.Code == product.Code); // with this way, this query make no sense because this happens in any scenario, never a code can be more than one code.
}
So, how can i do the same query but performing a "OR" on-demand (so that the query makes sense)?
You can use the facsimile of an IN for LINQ:
var productCodeList = products.Select(p => p.Code).ToList();
query = query.Where(p => productCodeList.Contains(p.Code));
It's basically saying:
SELECT *
FROM products
WHERE code IN (<list_of_codes>)
Using Contains:
var codes = products.Select(x => x.Code).ToArray();
query.Where(p => codes.Contains(p.Code));
Either use the Contains method ad Brad and Joe wrote, or (when that's not possible) use the PredicateBuilder:
var predicate = PredicateBuilder.False<Product>();
foreach (var product in products)
{
var code = product.Code;
predicate = predicate.Or(p => p.Code == code);
}
var products dataContext.Products.Where(predicate);