EF Append to an IQueryable a Where that generates an OR - entity-framework

I'm trying to achieve dynamic filtering on a table. My UI has filters that can be enabled or disabled on demand, and as you can imagine, my query should be able to know when to add filters to the query.
What I have so far is that I check if the filter object has a value, and if it does it adds a where clause to it. Example:
var q1 = DBContext.Table1
if (!string.IsNullOrEmpty(filterModel.SubjectContains))
q1 = q1.Where(i => i.Subject.Contains(filterModel.SubjectContains));
if (filterModel.EnvironmentId != null)
q1 = q1.Where(i => i.EnvironmentId == filterModel.EnvironmentId);
if (filterModel.CreatedBy != null)
q1 = q1.Where(i => i.CreatedByUserId == filterModel.CreatedBy);
var final = q1.Select(i => new
{
IssuesId = i.IssuesId,
Subject = i.Subject,
EnvironmentId = i.EnvironmentId,
CreatedBy = i.CreatedByUser.FullName,
});
return final.ToList();
The code above generates T-SQL that contains a WHERE clause for each field that uses AND to combine the conditions. This is fine, and will work for most cases.
Something like:
Select
IssueId, Subject, EnvironmentId, CreatedById
From
Table1
Where
(Subject like '%stackoverflow%')
and (EnvironmentId = 1)
and (CreatedById = 123)
But then I have a filter that explicitly needs an IssueId. I'm trying to figure out how the EF Where clause can generate an OR for me. I'm looking something that should generate a Tsql that looks like this:
Select
IssueId, Subject, EnvironmentId, CreatedById
From
Table1
Where
(Subject like '%stackoverflow%')
and (EnvironmentId = 1)
and (CreatedById = 123)
or (IssueId = 10001)

Found a solution for this that doesn't have to do multiple database call and works for me.
//filterModel.StaticIssueIds is of type List<Int32>
if (filterModel.StaticIssueIds != null)
{
//Get all ids declared in filterModel.StaticIssueIds
var qStaticIssues = DBContext.Table1.Where(i => filterModel.StaticIssueIds.Contains(i.IssuesId));
//Let's get all Issues that isn't declared in filterModel.StaticIssueIds from the original IQueryable
//we have to do this to ensure that there isn't any duplicate records.
q1 = q1.Where(i => !filterModel.StaticIssueIds.Contains(i.IssuesId));
//We then concatenate q1 and the qStaticIssues.
q1 = q1.Concat(qStaticIssues);
}
var final = q1.Select(i => new
{
IssuesId = i.IssuesId,
Subject = i.Subject,
EnvironmentId = i.EnvironmentId,
CreatedBy = i.CreatedByUser.FullName,
});
return final.ToList();

Related

EF Core 5.0 - The linq expression could not be translated. either rewrite the query

This question has been asked before and answered a few times, but not in a generalized way. Rather, the answers are specific to the asker (which makes sense)...but I use this pattern a lot, and I'm sure others do as well, so I'm looking for a more general answer. I know why this is happening, but I'm not sure what to do about it exactly. the offending line below is
RoleId = ur.First(xu => xu.UserId == x.Id).RoleId,
My understanding is that I have to convert this in some way before the projection, but that's where I'm stuck. Or use Contains() instead of First() (which is an implicit Where() filter), but I'm not sure how to rewrite it so the server will do the query, instead of the client. What is the best way forward?
var roles = await _roleManager.Roles.ToListAsync();
var rolesList = roles.Select(x => new {x.Id, x.Name}).ToList();
var rid = ur.First(xu => xu.UserId == 4).RoleId;
var ur = await _context.UserRoles.ToListAsync();
var appUsers = await _context.Users
.Select(x => new AppUserViewModel
{
Id = x.Id,
StaffId = x.StaffId,
Email = x.Email,
UserName = x.UserName,
PhoneNumber = x.PhoneNumber,
RoleId = ur.First(xu => xu.UserId == x.Id).RoleId,
RoleSelectListItems = rolesList.Select(yy => new SelectListItem
{
Value = yy.Id.ToString(),
Text = yy.Name
}).ToList()
})
.ToListAsync();
Actually you should work with IQueryable, not lists.
var roles = _roleManager.Roles.AsQueryable();
var ur = _context.UserRoles.AsQueryable();
var appUsers = await _context.Users
.Select(x => new AppUserViewModel
{
Id = x.Id,
StaffId = x.StaffId,
Email = x.Email,
UserName = x.UserName,
PhoneNumber = x.PhoneNumber,
RoleId = ur.FirstOrDefault(xu => xu.UserId == x.Id).RoleId,
RoleSelectListItems = roles.Select(yy => new SelectListItem
{
Value = yy.Id.ToString(),
Text = yy.Name
}).ToList()
})
.ToListAsync();
I got an answer to this. Linq sends itself down to the database, but certain expressions can't be interpreted by the translator which is why it throws this error. So if, for example, your Linq expression references a method to return a value, well, that method can't be passed down to your db, so you have to get that value before you use it in your Linq expression.
So in the example above, RoleId = ur.First(xu => xu.UserId == x.Id).RoleId, it's really two expressions, and the translator doesn't know what to do with that. The first part is "return to me the first object in the userRoles list where the id equals the id of the user" and the second part is "and now give me the RoleId". But you can't send that down - it can't be translated. So I'd have to get the RoleId from the userRoles list as a separate pair of transactions.
Get the role object, then separate out the RoleId value from that object.

How to convert an Int to a String inside a LINQ query

I am simply trying to derive an array[] using the following Linq query with EFF and Mysql:
DM_ItemGroup[] array =
(from item_grp in DbContext.hexa_item_group
join item_btn in DbContext.hexa_button
on item_grp.GroupID equals item_btn.ButtonID
where item_btn.ButtonType.Equals("G", StringComparison.Ordinal)
select new DM_ItemGroup
{
GroupID = SqlFunctions.StringConvert((decimal)item_grp.GroupID),
GroupName = item_grp.GroupName,
ButtonID = item_btn.ButtonID,
Default_TaxId = item_grp.Default_TaxId,
Out_Of_Sales = item_grp.Out_Of_Sales,
Sales_Seq = item_grp.Sales_Seq,
DataModel_Button = new DM_Button(),
}).ToArray<DM_ItemGroup>();
I initially tried .ToString() but it gave an exception. After trying SqlFunctions.StringConvert I am getting the following error:-
The specified method 'System.String StringConvert(System.Nullable`1[System.Decimal])'
on the type 'System.Data.Objects.SqlClient.SqlFunctions'
cannot be translated into a LINQ to Entities store expression.
How to convert the GroupID (which is a Int in my Table) to String (which is required by my DataModel)?
Thanks for down voting a perfectly legitimate question.Wish someone had answered the question with equal promptness.
I found the solution to my problem which is as follows:-
using (HexaEntities hh=new HexaEntities())
{
var cc = hh.hexa_item_group.ToDictionary(k => k.GroupID, k => k);
var lst = from l in cc
orderby l.Key
select new DM_ItemGroup {GroupID = l.Key.ToString(), GroupName = l.Value.GroupName, Default_TaxId=l.Value.Default_TaxId,ButtonID=l.Value.ButtonID.Value,Out_Of_Sales=l.Value.Out_Of_Sales,Sales_Seq=l.Value.Sales_Seq };
AllRecords = lst.ToList<DM_ItemGroup>();
}

mvc entity framework select with dynamic where clause

I am doing a asp.net-mvc with entity-framework App.
I have a select instruction, where I need to have a dymanic where condition.
It is a common case where you have a filter composed by a string like "aaaa bbbb cccc". I need to bring all data that contains all the filter string or part of it.
I doing part ot it witt Split function, but it is difficult to get all posible combinations.
I Would rather use a Store Procedure. But the porpose of it, is to use Entity Framework.
As far I did this.
public IEnumerable<UploadSearch> GetUploadsBySearch(string search)
{
IEnumerable<UploadSearch> viewModel = (from uploads in _db.Uploads
.Where(p => p.ProcessState_id == Security.APPROVED && p.Finder.Contains(search))
.OrderByDescending(p => p.UploadDate)
select new UploadSearch
{
User_id = uploads.User_id,
UserName = uploads.Users.Name,
UserLastName = uploads.Users.LastName,
});
And I Add a for instruction to loop throu the string,
string[] param = search.Replace(" "," ").Split(' ');
string _param = "";
int large = param.Length;
for (int i=0;i<large-1;i++)
{
_param +=param[i] ' ' + param[i + 1];
IEnumerable<UploadSearch> _viewModel = (from uploads in _db.Uploads
.Where(p => p.ProcessState_id == Security.APPROVED && p.Finder.Contains(_param))
.OrderByDescending(p => p.UploadDate)
select new UploadSearch
{
User_id = uploads.User_id,
UserName = uploads.Users.Name,
UserLastName = uploads.Users.LastName,
});
viewModel = viewModel.Union(_viewModel);
}
And I use a Union clause.
But There has to be another way to do it.
Any Ideas?
You should use LinqKit, it has a PredicateBuilder class that allows you to dynamically build queries
https://github.com/scottksmith95/LINQKit#predicatebuilder
IQueryable<Product> SearchProducts (params string[] keywords)
{
var predicate = PredicateBuilder.New<Product>();
foreach (string keyword in keywords)
{
string temp = keyword;
predicate = predicate.Or (p => p.Description.Contains (temp));
}
return dataContext.Products.Where (predicate);
}

How to improve query with two contexts - MVC5, LINQ n EF

I have two contexts. In one of them i have two views of which i get cods related to an entity from the another context. This query is taking too long time. How to improve it?
var negociacoes = _db.Negociacoes.Include(o=> o.User).ToArray();
var produtos = _oriDb.Vw_Produtos.ToArray();
var clientesVendedor = _oriDb.Vw_ClientesVendedores.ToArray();
var query = from n in negociacoes
join p in produtos on n.ProdutoId equals p.ProdutoId
join c in clientesVendedor on n.ClienteId equals c.codigo_entidade
select new NegociacaoView
{
NegociacaoId = n.NegociacaoId,
ProdutoId = n.ProdutoId,
Produto = p.descricao,
ClienteId = n.ClienteId,
Cliente = c.razao_social,
Rca = n.Rca,
Quantidade = n.Quantidade,
Preco = n.Preco,
Situacao = n.Situacao,
UserId = n.User.UserName,
Atendente = n.Atendente,
CondicaoId = n.CondicaoId,
DataCriacao = n.DataCriacao,
DataLiberacao = n.DataLiberacao,
Observacao = n.Observacao,
User = n.User
};
return query.ToList();
There are a couple of ways to speed this up:
It helps to run the smallest most efficient query first, then use those results to constrain the following queries.
Defining a select list so the database doesn't have to materialize every column will speed things up and use less memory.
Unfortunately, no matter how you do it in LINQ, you will end up with sql that uses large IN statements. A sproc would give you access to temp tables and joins that would be even better.
var negociacoes = _db.Negociacoes.Include(o=> o.User).ToArray();
//Use results of first query to constrain the second two. You could maybe combine the second two into one query.
var clientIds = negociacoes.Select(x => x.ClienteId);
var productIds = negociacoes.Select(x => x.ProdutoId);
var produtos = _oriDb.Vw_Produtos
.Where(x => productIds.Contains(x.ProdutoId))
//add a select. You're only using two columns from this table.
//.Select(x => new { })
.ToArray();
var clientesVendedor = _oriDb.Vw_ClientesVendedores
.Where(x => clientIds.Contains(x.codigo_entidade))
//add a select. You're only using two columns from this table.
//.Select(x => new { })
.ToArray();
var query = from n in negociacoes
join p in produtos on n.ProdutoId equals p.ProdutoId
join c in clientesVendedor on n.ClienteId equals c.codigo_entidade
select new NegociacaoView
{
NegociacaoId = n.NegociacaoId,
ProdutoId = n.ProdutoId,
Produto = p.descricao,
ClienteId = n.ClienteId,
Cliente = c.razao_social,
Rca = n.Rca,
Quantidade = n.Quantidade,
Preco = n.Preco,
Situacao = n.Situacao,
UserId = n.User.UserName,
Atendente = n.Atendente,
CondicaoId = n.CondicaoId,
DataCriacao = n.DataCriacao,
DataLiberacao = n.DataLiberacao,
Observacao = n.Observacao,
User = n.User
};
return query.ToList();

Entity Framework lambda query with groupby, sum and average

I have a entity called StockDetails using Entity Framework, see picture below
I want to fetch a list IEnumerable<StockDetail>, summarized by Reels, Qtyton, average date from Days (datetime) and grouping by the rest of the properties.
I'm building a datalayer (WCF Services) with Entity Framework as ORM, some of the services are old SQL queries I'm trying to convert to linq/lamdba expression. But I'm pretty new to how to write and want some help.
This is how I started the query in lambda, but I got stuck on the groupby/sum/average part.
public IEnumerable<StockDetail> ListStockDetailByCustomerNumber(int customerNumber)
{
var CustNo = customerNumber.ToString();
return _entities.StockDetails
.Where(x => x.Custno == CustNo)
.GroupBy(
x =>
new
{
x.Millcd,
x.Matercd,
x.Proddesc,
x.Grammage,
x.Reelwidth,
x.Ordercode,
x.Buyordno,
x.Whsedesc,
x.Co,
x.Finished,
x.Pm,
x.PurchaseOrder,
x.Diameter,
x.Rtadate,
x.Custno,
x.Reels,
x.Days,
x.Qtyton
})
.ToList();
}
Question solved:
public IEnumerable<StockDetail> ListStockDetailByCustomerNumber(int customerNumber)
{
var stockDetailsList = new List<StockDetail>();
var custNo = customerNumber.ToString();
var list = _entities.StockDetails
.Where(x => x.Custno == custNo )
.GroupBy(
x =>
new
{
x.Millcd,
x.Matercd,
x.Proddesc,
x.Grammage,
x.Reelwidth,
x.Ordercode,
x.Buyordno,
x.Whsedesc,
x.Co,
x.Finished,
x.Pm,
x.PurchaseOrder,
x.Diameter,
x.Rtadate,
x.Custno,
x.UpdDte
})
.Select(x => new
{
x.Key.Millcd,
x.Key.Matercd,
x.Key.Proddesc,
x.Key.Grammage,
x.Key.Reelwidth,
x.Key.Ordercode,
x.Key.Buyordno,
Reels = x.Sum(p => p.Reels),
Qtyton = x.Sum(p => p.Qtyton),
Day = x.Max(p => p.Days),
//Day = x.Average(p => p.Days.Ticks), // Want to calculate average datetime of date but linq dosn't support datetime.ticks
x.Key.Whsedesc,
x.Key.Co,
x.Key.Finished,
x.Key.Pm,
x.Key.PurchaseOrder,
x.Key.Diameter,
x.Key.Rtadate,
x.Key.Custno,
x.Key.UpdDte
});
foreach (var s in list)
{
stockDetailsList.Add(new StockDetail
{
Millcd = GetFriendlyNameForKey(s.Millcd),
Matercd = s.Matercd,
Proddesc = s.Proddesc,
Grammage = s.Grammage,
Reelwidth = s.Reelwidth,
Ordercode = s.Ordercode,
Buyordno = s.Buyordno,
Reels = s.Reels,
Qtyton = s.Qtyton,
Days = s.Day,
Whsedesc = s.Whsedesc,
Co = s.Co,
Finished = s.Finished,
Pm = s.Pm,
PurchaseOrder = s.PurchaseOrder,
Diameter = s.Diameter,
Rtadate = s.Rtadate,
Custno = s.Custno,
UpdDte = s.UpdDte
});
}
return stockDetailsList;
}
This is how the query looks in T-SQL
SELECT
Millcd, Matercd,
Proddesc, Grammage,
Reelwidth, Ordercode,
Buyordno,
SUM(Reels) as Reels,
SUM(Qtyton) as Qtyton,
Whsedesc, Co,
(cast(FLOOR(avg(cast(DateProd as float))) as datetime)) As Days,
Finished, Pm,
PurchaseOrder,
Diameter, Rtadate,
Custno, UpdDte
FROM StockDetail
WHERE custno = #custcode
GROUP BY Millcd, Matercd, Proddesc, Grammage, Reelwidth, Ordercode, Buyordno,
Whsedesc, Co, Finished, Pm, PurchaseOrder, Diameter, Rtadate, Custno, UpdDte
not sure if this will help you but you can add
Reels = (_entities.StockDetails
.Where(x => x.Custno == CustNo).Sum(x=>x.Reels))
instead of x.Reels in your select , and do the same with Qtyton
For your average use the average extension
your select will look something like .Select(x=>new {...}) after your where statement then the group by