I have a piece of Linq that queries an EntityFramework context in my web controller and returns the result, as follows:
[HttpGet]
public IActionResult GetRoutingRules()
{
var query = (from rr in _context.RoutingRules
join dest in _context.RoutingZones on rr.DestinationZoneId equals dest.ZoneId
join origin in _context.RoutingZones on rr.OriginZoneId equals origin.ZoneId
join hub in _context.RoutingHub on rr.HubId equals hub.HubId
select new RoutingRulesDto(rr) { DestinationZoneName = dest.ZoneName, OriginZoneName = origin.ZoneName, HubName = hub.HubName });
return Ok(query);
}
I want a new method that will take a "filter" object, where I can narrow down the results of the above. My filter object looks like this:
public class RoutingSearchFilterDto
{
public int BrandId { get; set; }
public int? ServiceType { get; set; }
public long? OriginZoneId { get; set; }
public long? DestinationZoneId { get; set; }
public int? RuleRanking { get; set; }
public bool? IsRuleActive { get; set; }
}
The minimum info that needs to be set in this class is BrandId. All other properties are options in the filter.
I need to write a new controller method that will utilise this, something like:
[HttpPost("filtered")]
public IActionResult GetFilteredRoutingRules([FromBody] RoutingSearchFilterDto filter)
{
...
}
How do I linq query on properties that could potentially be null? Essentially, a dynamic query depending on the properties set in the filter object.
NOTE: I want this to affect the select statement that the EF runs, not just let EF get all the data, then filter the data set - the point of this is to make the db call more efficient.
Filter object might be sent where BrandId = 1, IsRuleActive = 1. Equally, it could be BrandId = 1, ServiceType = 3 (and therefore IsRuleActive is null so shouldn't be in the linq where clause).
I've tried this:
var param = (Expression.Parameter(typeof(RoutingRules), "rr"));
Expression combinedExpr = null;
if (filter.BrandId != null)
{
var exp = Expression.Equal(Expression.Property(param, "BrandId"), Expression.Constant(filter.BrandId));
combinedExpr = exp;
}
if (filter.DestinationZoneId != null)
{
var exp = Expression.Equal(Expression.Property(param, "DestinationZoneId"), Expression.Constant(filter.DestinationZoneId));
combinedExpr = (combinedExpr == null ? exp : Expression.AndAlso(combinedExpr, exp));
}
if (filter.OriginZoneId != null)
{
var exp = Expression.Equal(Expression.Property(param, "OriginZoneId"), Expression.Constant(filter.OriginZoneId));
combinedExpr = (combinedExpr == null ? exp : Expression.AndAlso(combinedExpr, exp));
}
if (filter.EshopServiceType != null)
{
var exp = Expression.Equal(Expression.Property(param, "EshopServiceType"), Expression.Constant(filter.EshopServiceType));
combinedExpr = (combinedExpr == null ? exp : Expression.AndAlso(combinedExpr, exp));
}
if (filter.IsRuleActive != null)
{
var exp = Expression.Equal(Expression.Property(param, "IsRuleActive"), Expression.Constant(filter.IsRuleActive, typeof(bool?)));
combinedExpr = (combinedExpr == null ? exp : Expression.AndAlso(combinedExpr, exp));
}
if (filter.RuleRanking != null)
{
var exp = Expression.Equal(Expression.Property(param, "RuleRanking"), Expression.Constant(filter.RuleRanking));
combinedExpr = (combinedExpr == null ? exp : Expression.AndAlso(combinedExpr, exp));
}
if (combinedExpr == null)
combinedExpr = Expression.Default(typeof(bool));
var compiled = Expression.Lambda<Func<RoutingRules, bool>>(combinedExpr, param).Compile();
var results = (from rr in _context.RoutingRules.Where(compiled)
join dest in _context.RoutingZones on rr.DestinationZoneId equals dest.ZoneId
join origin in _context.RoutingZones on rr.OriginZoneId equals origin.ZoneId
join hub in _context.RoutingHub on rr.HubId equals hub.HubId
where rr.BrandId == 21
select new RoutingRulesDto(rr) { DestinationZoneName = dest.ZoneName, OriginZoneName = origin.ZoneName, HubName = hub.HubName });
But the Where clause isn't applied to the generated Sql, it seems to pull back all records, then apply the where in memory, which isn't what I need.
Thanks in advance for any pointers!!
You can build an expression tree for this, but have you considered:
IQueryable<...> query = ...;
if (routingSearchFilter.ServiceType != null)
query = query.Where(e => e.ServiceType == routingSearchFilter.ServiceType);
if (...)
query = query.Where(....);
The EF engine is smart enough to combine the Where clauses (with AND of course).
Edit:
It wasn't clear if you wanted to filter on the joined result or only on the first table. In that case it would continue like
var result = (from rr in query
join dest in _context.RoutingZones on rr.DestinationZoneId equals dest.ZoneId
join ...
select new RoutingRulesDto(rr) .... ).ToSometing();
But I'm a little wary about that RoutingRulesDto(rr) constructor parameter.
If you use the fluent API for LINQ, you can conditionally add Where clauses.
var query = _content.RoutingRules.Where(r => r.BrandId == filter.BrandId);
if (filter.OriginZoneId != null) {
query = query.Where(r => r.OriginZoneId == filter.OriginZoneId);
}
if (filter.EshopServiceType != null) {
query = query.Where(r => r.EshopServiceType == filter.EshopServiceType);
}
// etc...
var result = query.ToArray();
Just to have my final solution in black and white, here's what I had in the end:
[HttpPost("filtered")]
public IActionResult GetFilteredRoutingRules([FromBody] RoutingSearchFilterDto filter)
{
// Query to be build on the routing rules table.
IQueryable<RoutingRules> query = _context.RoutingRules;
// Populate the linked foreign key entities.
query.Include(x => x.Hub).Include(y => y.DestinationZone).Include(z => z.OriginZone);
// Build dynamic where statements.
if (filter.BrandId != null)
query = query.Where(r => r.BrandId == filter.BrandId);
if (filter.OriginZoneId != null)
query = query.Where(r => r.OriginZoneId == filter.OriginZoneId);
if (filter.DestinationZoneId != null)
query = query.Where(r => r.DestinationZoneId == filter.DestinationZoneId);
if (filter.IsRuleActive != null)
query = query.Where(r => r.IsRuleActive == filter.IsRuleActive);
if (filter.RuleRanking != null)
query = query.Where(r => r.RuleRanking == filter.RuleRanking);
// If you want to add paging:
query = query.Skip(filter.PageSize * filter.PageNumber).Take(filter.PageSize);
// Perform select on the table and map the results.
var result = query.Select(r => new RoutingRulesDto
{
RoutingRuleId = r.RoutingRuleId,
BrandId = r.BrandId,
LastMileCarrierCode = r.LastMileCarrierCode,
CashOnDelivery = r.CashOnDelivery,
CreationTime = r.CreationTime,
CurrencyCode = r.CurrencyCode,
CurrencyDescription = Enum.Parse(typeof(Enumerations.CurrencyCode), r.CurrencyCode),
DestinationZoneId = r.DestinationZoneId,
EddFromDay = r.EddFromDay,
EddToDay = r.EddToDay,
ServiceType = r.ServiceType,
ServiceTypeName = Enum.Parse(typeof(Enumerations.ServiceType), r.EshopServiceType),
IsPickUpAvailable = r.IsPickUpAvailable,
LastUpdateTime = r.LastUpdateTime,
LastUpdateUser = r.LastUpdateUser,
OriginZoneId = r.OriginZoneId,
RuleRanking = r.RuleRanking,
SignOnDelivery = r.SignOnDelivery,
TermsOfDelivery = r.TermsOfDelivery,
TermsOfDeliveryName = Enum.Parse(typeof(Enumerations.TermsOfDelivery), r.TermsOfDelivery),
ValueOfGoods = r.ValueOfGoods,
WeightLowerLimit = r.WeightLowerLimit,
WeightUpperLimit = r.WeightUpperLimit,
FirstMileCarrierCode = r.FirstMileCarrierCode,
HubId = r.HubId,
IsInsuranceAvailable = r.IsInsuranceAvailable,
IsRuleActive = r.IsRuleActive,
HubName = r.Hub.HubName,
DestinationZoneName = r.DestinationZone.ZoneName,
OriginZoneName = r.OriginZone.ZoneName,
});
// The SQL produced includes the joins and where clauses as well as only
// selecting the column names that are required in the flattened return object.
return Ok(result);
}
Thanks for the help guys!
Related
I want to do something like this
public string GetSiteTitleFromChangeHistory(int siteId)
{
var changeHistories = changeHistoryRepository.GetAll(c => c.SiteRegistryId == siteId);
var value = changeHistories.firstOrDefault(r=>r.State="Active")
return value.Title;
}
public IQueryable<PendingReconfirmation> GetSitesForBusinessReconfirmationReminder(IList<StateStatus> stateStatusMappingIds,
string country, int reminderDay)
{
return from reg in repositorySpositeRegistry.GetAll(x => x.SiteUrlcountryCode != null
&& x.SiteUrlcountryCode.ToLower() == country.ToLower())
select new PendingReconfirmation()
{
Id = reg.Id,
SiteTitle = GetSiteTitleFromChangeHistory(reg.Id!).ToString() ?? reg.SiteTitle,
};
}
Repository.GetAll look like this
public IQueryable<T> GetAll(Expression<Func<T, bool>>? filter = null)
{
var query = entities.AsQueryable();
if (filter != null)
query = query.Where(filter);
return query;
}
But I am getting error
The client projection contains a reference to a constant expression of '' through the instance method 'GetSiteTitleFromChangeHistory'. This could potentially cause a memory leak; consider making the method static so that it does not capture constant in the instance.
Any help will be highly appreciated
Make this function GetSiteTitleFromChangeHistory as STATIC.
Is there a specific reason you split up GetSiteTitleFromChangeHistory into a separate method?
Otherwise you should be able to do a subquery like so, provided the queryable used for the subquery is based on a dbset in the same efcontext:
public IQueryable<PendingReconfirmation> GetSitesForBusinessReconfirmationReminder(IList<StateStatus> stateStatusMappingIds,
string country, int reminderDay)
{
return from reg in repositorySpositeRegistry.GetAll(x => x.SiteUrlcountryCode != null
&& x.SiteUrlcountryCode.ToLower() == country.ToLower())
select new PendingReconfirmation()
{
Id = reg.Id,
SiteTitle = changeHistoryRepository.GetAll(c => c.SiteRegistryId == reg.Id && c.State == "Active").FirstOrDefault().Title ?? reg.Title
};
}
I am trying to use linq2db.EntityFrameworkCore for some of its windowing functions, such as RANK().
Below is my implementation:
var abc = (from tl in _repo.Context.TransferLink
join tlt in _repo.Context.TransferLinkType on new { TLinkId = tl.TransferLinkTypeId, EType = "Deviance" } equals new { TLinkId = tlt.TransferLinkTypeId, EType = tlt.EnumTransferLinkType }
//let duplicateCount = _repo.Context.TransferLink.Where(tl1 => tl1.SecondaryTransferId != null && tl.SecondaryTransferId != null &&
//tl1.SecondaryTransferId == tl.SecondaryTransferId.Value).Count()
where
(allTransferIds.Contains(tl.PrimaryTransferId) || allTransferIds.Contains(tl.SecondaryTransferId)) &&
!tl.Archived
select new
{
TransferLinkId = tl.TransferLinkId,
TransferLinktypeId = tl.TransferLinkTypeId,
PrimaryTransferId = tl.PrimaryTransferId,
SecondaryTransferId = tl.SecondaryTransferId,
DuplicateCount = Sql.Ext.Count(tl.TransferLinkId)
.Over()
.PartitionBy(tl.SecondaryTransferId)
.ToValue()
UpdatedDate = tl.UpdatedDate,
RankVal = Sql.Ext.Rank()
.Over()
.PartitionBy(tl.TransferLinkTypeId, tl.SecondaryTransferId)
.OrderByDesc(tl.UpdatedDate)
.ThenBy(tl.TransferLinkId)
.ToValue()
}).ToList();
This code throws the exception:
Rank is server-side method
I have tried searching for a solution, but could not find any.
Any idea?
For using linq2db.EntityFrameworkCore you have to switch to library's LINQ provider. It can be done by simple ToLinqToDB() call.
var query = /* some EF Core query */
query = query.ToLinqToDB();
var result = query.ToList();
i am not before dev pc. i just started working with EF. so curious to know can we pass column name dynamically for where clause.
see a screen shot for searching grid.
i just compose a sample query. please tell me does it work?
public ActionResult Index(String ColumnName,String SearchText)
{
private CustomersEntities db = new CustomersEntities();
var customer = (from s in db.Customers
select new CustomerDTO
{
CustomerID = s.CustomerID,
CompanyName = s.CompanyName,
ContactName = s.ContactName,
ContactTitle = s.ContactTitle,
Address = s.Address
})
.Where(s => s.Field<string>(ColumnName).ToUpper().Contains(SearchText.ToUpper());
return View(customer);
}
thanks
public ActionResult Index(string ColumnName, string SearchText)
{
var arg = Expression.Parameter(typeof(Customer), "x");
var strType = typeof(string);
var ToUpperMeth = strType.GetMethods().Where(x => x.Name == nameof(string.ToUpper)
&& x.GetParameters().Count() == 0).Single();
var ContainsMeth = strType.GetMethods().Where(x => x.Name == nameof(string.Contains)
&& x.GetParameters().Count() == 1).Single();
var exprVal = Expression.Constant(SearchText);
var toUpExprVal = Expression.Call(exprVal, ToUpperMeth);
var exprProp = Expression.Property(arg, ColumnName);
var toUpExpr = Expression.Call(exprProp, ToUpperMeth);
var contExpr = Expression.Call(toUpExpr, ContainsMeth, toUpExprVal);
var predicate = Expression.Lambda<Func<Customer, bool>>(contExpr, arg);
var customer = (from s in db.Customers
select new CustomerDTO
{
CustomerID = s.CustomerID,
CompanyName = s.CompanyName,
ContactName = s.ContactName,
ContactTitle = s.ContactTitle,
Address = s.Address
}).Where(predicate).ToList();
return View(customer);
}
You can create something like this in repository (if you use it)
public IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
{
return _context.Set<CustomersEntities>().Where(predicate);
}
and then
var result = _repository.FindBy(y => y.CompanyName.IndexOf(SearchText, StringComparison.OrdinalIgnoreCase) >= 0);
The basic pattern is to build up the query at runtime, selectively adding Where expressions before running the query and projecting the results into the DTO.
Like this:
public IList<CustomerDTO> FindCustomers(String ColumnName, String SearchText)
{
var query = from s in db.Customers select s;
if (ColumnName == "CompanyName")
{
query = query.Where(c => c.CompanyName == SearchText);
}
else if (ColumnName == "ContactName")
{
query = query.Where(c => c.ContactName == SearchText);
}
//. . .
else
{
throw new InvalidOperationException($"Column {ColumnName} not found or not supported for searching.");
}
var results = from c in query
select new CustomerDTO()
{
CustomerID = c.CustomerID,
CompanyName = c.CompanyName,
ContactName = c.ContactName,
ContactTitle = c.ContactTitle,
Address = c.Address
};
return results;
}
I have a search form where the user can enter one to many parameters (Data, Status, Type, ID, Summary, Description) and leave the rest blank.
Here's my Linq to SQL code for my basic search. Is there a way to check each parameter within the Linq for zero, null or empty string?
List<RequestStatusModel> objRequestStatus = new List<RequestStatusModel>();
var query = from r in SimCareDB.Requests
where r.CustomerID == 31
select (new RequestStatusModel
{
RequestID = r.RequestID,
RequestTitle = r.RequestTitle,
DateAdded = r.DateAdded.ToString(),
DateChanged = r.DateChanged.ToString(),
RequestStatusID = r.StatusID
});
Thank you!
If it doesn't have to be in your linq statement you could just do it with classic if statements.
List<RequestStatusModel> objRequestStatus = new List<RequestStatusModel>();
var query = from r in SimCareDB.Requests
where r.CustomerID == 31
select (new RequestStatusModel
{
//...
});
if(data != null) //Replace with additional checks, if neccessary
{
query = query.where(x=> ...);
}
if(status != null)
{
query = query.where(x => ...)
}
If you want to only filter if certain criteria is passed, you should do something like this
var objRequestStatus = new List<RequestStatusModel>();
var query = from r in SimCareDB.Requests
where r.CustomerID == 31
if (String.IsNullOrEmpty(r.RequestID))
objRequestStatus = objRequestStatus.Where(x => x.RequestID == r.RequestID);
if (String.IsNullOrEmpty(r.RequestTitle))
objRequestStatus = objRequestStatus.Where(x => x.RequestTitle == r.RequestTitle);
//you other filters here
This sets up the expression to what you want based on which requests are passed
If you want to avoid all those ifs, you could do
List<RequestStatusModel> objRequestStatus = new List<RequestStatusModel>();
var query = from r in SimCareDB.Requests
where (r.CustomerID == 31) &&
(!String.IsNullOrEmpty(id) ? r.RequestID == id : true) &&
(!String.IsNullOrEmpty(status) ? r.StatusID == status : true)
/* And so on */
select (new RequestStatusModel
{
RequestID = r.RequestID,
RequestTitle = r.RequestTitle,
DateAdded = r.DateAdded.ToString(),
DateChanged = r.DateChanged.ToString(),
RequestStatusID = r.StatusID
});
I am able to filter the data with the following two parameters id1 and id2, and get accurate result of 10 records, from which have 9 with a price_type=cs and other with price-type=ms.
However, if I add price_type to the parameters id1 and id2 (id1=23456,567890&id2=6782345&price_type=ms), I get 3000 records instead of getting one record.
Am I missing something in the code. Any help would be very much appreciated.
var data = db.database_BWICs.AsQueryable();
var filteredData = new List<IQueryable<database_Data>>();
if (!string.IsNullOrEmpty(query.name))
{
var ids = query.name.Split(',');
foreach (string i in ids)
{
filteredData.Add(data.Where(c => c.Name != null && c.Name.Contains(i)));
}
}
if (!string.IsNullOrEmpty(query.id2))
{
var ids = query.id2.Split(',');
foreach (string i in ids)
{
filteredData.Add(data.Where(c => c.ID2!= null && c.ID2.Contains(i)));
}
}
if (!string.IsNullOrEmpty(query.id1))
{
var ids = query.id1.Split(',');
foreach (string i in ids)
{
filteredData.Add(data.Where(c => c.ID1!= null && c.ID1.Contains(i)));
}
}
if (query.price_type != null)
{
var ids = query.price_type.Split(',');
foreach (string i in ids)
{
filteredData.Add(data.Where(c => c.Type.Contains(i)));
}
}
if (filteredData.Count != 0)
{
data = filteredData.Aggregate(Queryable.Union);
}
Updated Code:
var data = db.database_BWICs.AsQueryable();
if (!string.IsNullOrEmpty(query.name))
{
var ids = query.name.Split(',');
data = data.Where(c => c.Name != null && ids.Contains(c.Name));
}
if (query.price_type != null)
{
var ids = query.price_type.Split(',');
data = data.Where(c => ids.Contains(c.Cover));
}
if (!String.IsNullOrEmpty(query.id1))
{
var ids = query.id1.Split(',');
data = data.Where(c => c.ID1!= null && ids.Contains(c.ID1));
}
Because you don't add filter to restrict, every filter adds datas to result.
It means you make OR between your filters, not AND.
And your usage of contains looks rather strange too : you're using String.Contains, while I would guess (maybe wrong) that you want to see if a value is in a list => Enumerable.Contains
You should rather go for something like this (withoud filteredData)
var data = db.database_BWICs.AsQueryable();
if (!string.IsNullOrEmpty(query.name))
{
var ids = query.name.Split(',');
data = data.Where(c => c.Name != null && ids.Contains(c.Name)));
}
//etc.
if (query.price_type != null)
{
var ids = query.price_type.Split(',');
data = data.Where(c => ids.Contains(c.Type));
}
EDIT
Well, if you wanna mix and or conditions, you could go for PredicateBuilder
Then your code should look like that (to be tested).
//manage the queries with OR clause first
var innerOr = Predicate.True<database_BWICs>();//or the real type of your entity
if (!String.IsNullOrEmpty(query.id1))
{
var ids = query.id1.Split(',');
innerOr = innerOr.Or(c => c.ID1!= null && ids.Contains(c.ID1));
}
if (!String.IsNullOrEmpty(query.id2))
{
var ids = query.id2.Split(',');
innerOr = innerOr.Or(c => c.ID2!= null && ids.Contains(c.ID2));
}
//now manage the queries with AND clauses
var innerAnd = Predicate.True<database_BWICs>();//or the real type of your entity
if (query.price_type != null)
{
var ids = query.price_type.Split(',');
innerAnd = innerAnd.And(c => ids.Contains(c.Type));
}
//etc.
innerAnd = innerAnd.And(innerOr);
var data = db.database_BWICs.AsQueryable().Where(innerAnd);