I had to write a function or functions that returns accounts by role, by currency, by company, by application type. I decided to make one function that according to the request model returns filtered accounts. My question: Is it a clean way to implement the function?
public List<ResponseModel> Get(RequestModel requestModel)
{
return (from role in Context.UserRoles
where
role.UserId == requestModel.UserId &&
role.Account.CompanyId == requestModel.CompanyId &&
(requestModel.RoleId == null ? true : role.RoleId == requestModel.RoleId) &&
(requestModel.Currencies.Count() == 0 ? true : requestModel.Currencies.Contains(role.Account.Currency)) &&
(requestModel.ApplicationTypes.Count() == 0 ? true : requestModel.ApplicationTypes.Contains(role.Account.Type)) &&
(requestModel.AccountUse == null ? true : role.Account.AccountUse == requestModel.AccountUse) &&
(requestModel.OperationTypeId == 0 ? true : role.OperationTypeId == requestModel.OperationTypeId) &&
!role.Account.IsDeleted &&
!role.Account.Company.IsDeleted
orderby role.Account.FormattedAccount
group role by role.Account into Accounts
select new ResponseModel
{
Id = Accounts.Key.Id.ToString(),
FormattedNumber = Accounts.Key.FormattedAccount,
Number = Accounts.Key.Number,
Currency = Accounts.Key.Currency,
Application = Accounts.Key.Application
}).ToList();
}
I was thinking write a store procedure that returns the same but there are almost 250 SP on the sql server and this function is used a lot.
I also thought split it in small functions but I don't know hot to do it.
If this helps:
public class RequestModel
{
public int AccountId { get; set; }
public int OperationTypeId { get; set; }
public string RoleId {get; set;}
public int CompanyId { get; set; }
public string UserId { get; set; }
public string AccountUse { get; set; }
public List<string> Currencies { get; set; }
public List<string> ApplicationTypes { get; set; }
}
Your solution is correct, easy to read and understand, and its execution side is probably as fast as it gets.
The only thing that I would adjust is the conditionals of the form
someCondition ? true : someOtherCondition
I would replace them with an equivalent
(someCondition || someOtherCondition)
to remove conditional expressions from the where clause. I would also replace collection.Count() == 0 with !collection.Any(), as follows:
return (from role in Context.UserRoles
where
role.UserId == requestModel.UserId &&
role.Account.CompanyId == requestModel.CompanyId &&
(requestModel.RoleId == null || role.RoleId == requestModel.RoleId) &&
(!requestModel.Currencies.Any() || requestModel.Currencies.Contains(role.Account.Currency)) &&
(!requestModel.ApplicationTypes.Any() || requestModel.ApplicationTypes.Contains(role.Account.Type)) &&
(requestModel.AccountUse == null || role.Account.AccountUse == requestModel.AccountUse) &&
(requestModel.OperationTypeId == 0 || role.OperationTypeId == requestModel.OperationTypeId) &&
!role.Account.IsDeleted &&
!role.Account.Company.IsDeleted
orderby role.Account.FormattedAccount
group role by role.Account into Accounts
select new ResponseModel
{
Id = Accounts.Key.Id.ToString(),
FormattedNumber = Accounts.Key.FormattedAccount,
Number = Accounts.Key.Number,
Currency = Accounts.Key.Currency,
Application = Accounts.Key.Application
}).ToList();
In order to keep the interpretation of various RequestModel properties inside the RequestModel class you could provide a Filter property, as follows:
public class RequestModel {
...
public Expression<Func<UserRole,bool>> Filter {
get {
return role =>
(role.UserId == UserId)
&& (role.Account.CompanyId == CompanyId)
&& (RoleId == null || role.RoleId == RoleId)
&& (!Currencies.Any() || Currencies.Contains(role.Account.Currency))
&& (!ApplicationTypes.Any() || ApplicationTypes.Contains(role.Account.Type))
&& (AccountUse == null || role.Account.AccountUse == AccountUse)
&& (OperationTypeId == 0 || role.OperationTypeId == OperationTypeId);
}
}
}
Use it in your where clause like this:
return (from role in Context.UserRoles.Where(requestModel.Filter)
where !role.Account.IsDeleted && !role.Account.Company.IsDeleted
orderby role.Account.FormattedAccount
group role by role.Account into Accounts
select new ResponseModel {
Id = Accounts.Key.Id.ToString(),
FormattedNumber = Accounts.Key.FormattedAccount,
Number = Accounts.Key.Number,
Currency = Accounts.Key.Currency,
Application = Accounts.Key.Application
}).ToList();
Note how all conditions related to requestModel are "hidden" inside the RequestModel instance. This would make future modifications to RequestModel easier, because you wouldn't need to make "parallel modifications" in other parts of the code.
Related
Scenario :
My multitenant project is based on ASPNetBoilerPlate.
I have a "WORK" entity which is IMayHaveTenant. Every tenant must see default Works which is in HOST And also His Works too every where. How I must do that?
I need some codes like : tenantId == id || tenantId is null
Wrong Answer :
using (CurrentUnitOfWork.DisableFilter(AbpDataFilters.MayHaveTenant)){ }
This returns other tenants Works too.
Create an interface, inherit IMayHaveTenant if you want the other built-in features that are for it:
public interface IMayHaveTenantSharedWithHost : IMayHaveTenant
{
}
Implement that interface:
public class Work : Entity, IMayHaveTenantSharedWithHost
{
public int? TenantId { get; set; }
// ...
}
Override CreateFilterExpression in your AbpDbContext subclass and handle that interface:
protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>()
where TEntity : class
{
Expression<Func<TEntity, bool>> expression = null;
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
Expression<Func<TEntity, bool>> softDeleteFilter = e => !IsSoftDeleteFilterEnabled || !((ISoftDelete) e).IsDeleted;
expression = expression == null ? softDeleteFilter : CombineExpressions(expression, softDeleteFilter);
}
// if (typeof(IMayHaveTenant).IsAssignableFrom(typeof(TEntity)))
if (typeof(IMayHaveTenantSharedWithHost).IsAssignableFrom(typeof(TEntity)))
{
Expression<Func<TEntity, bool>> mayHaveTenantFilter = e => !IsMayHaveTenantFilterEnabled || ((IMayHaveTenant)e).TenantId == CurrentTenantId || ((IMayHaveTenant)e).TenantId == null;
expression = expression == null ? mayHaveTenantFilter : CombineExpressions(expression, mayHaveTenantFilter);
}
else if (typeof(IMayHaveTenant).IsAssignableFrom(typeof(TEntity)))
{
Expression<Func<TEntity, bool>> mayHaveTenantFilter = e => !IsMayHaveTenantFilterEnabled || ((IMayHaveTenant)e).TenantId == CurrentTenantId;
expression = expression == null ? mayHaveTenantFilter : CombineExpressions(expression, mayHaveTenantFilter);
}
if (typeof(IMustHaveTenant).IsAssignableFrom(typeof(TEntity)))
{
Expression<Func<TEntity, bool>> mustHaveTenantFilter = e => !IsMustHaveTenantFilterEnabled || ((IMustHaveTenant)e).TenantId == CurrentTenantId;
expression = expression == null ? mustHaveTenantFilter : CombineExpressions(expression, mustHaveTenantFilter);
}
return expression;
}
Example: Filter a list of products that have a price based on fromPrice and toPrice. They could either both be supplied, or just one.
Find all products whose price is greater than fromPrice
Find all products whose price is less than toPrice
Find all products whose price is between fromPrice and toPrice
Product:
public class Product {
private String id;
private Optional<BigDecimal> price;
public Product(String id, BigDecimal price) {
this.id = id;
this.price = Optional.ofNullable(price);
}
}
PricePredicate:
public class PricePredicate {
public static Predicate<? super Product> isBetween(BigDecimal fromPrice, BigDecimal toPrice) {
if (fromPrice != null && toPrice != null) {
return product -> product.getPrice().isPresent() && product.getPrice().get().compareTo(fromPrice) >= 0 &&
product.getPrice().get().compareTo(toPrice) <= 0;
}
if (fromPrice != null) {
return product -> product.getPrice().isPresent() && product.getPrice().get().compareTo(fromPrice) >= 0;
}
if (toPrice != null) {
return product -> product.getPrice().isPresent() && product.getPrice().get().compareTo(toPrice) <= 0;
}
return null;
}
}
Filters:
return this.products.stream().filter(PricePredicate.isBetween(fromPrice, null)).collect(Collectors.toList());
return this.products.stream().filter(PricePredicate.isBetween(null, toPrice)).collect(Collectors.toList());
return this.products.stream().filter(PricePredicate.isBetween(fromPrice, toPrice)).collect(Collectors.toList());
Is there a way to improve my Predicate instead of having the if not null checks? Anything that can be done with optionals?
No, Optional is not designed to replace null checks.
But your code can be improved by avoiding duplication, and by avoiding to return null (which is clearly not a valid value for a Predicate) if both arguments are null:
public static Predicate<Product> isBetween(BigDecimal fromPrice, BigDecimal toPrice) {
Predicate<Product> result = product -> true;
if (fromPrice != null) {
result = result.and(product -> product.getPrice().isPresent() && product.getPrice().get().compareTo(fromPrice) >= 0);
}
if (toPrice != null) {
result = result.and(product -> product.getPrice().isPresent() && product.getPrice().get().compareTo(toPrice) <= 0);
}
return result;
}
You can use Apache Commons Lang, it offers null safe comparison:
ObjectUtils.compare(from, to)
null is assumed to be less than a non-value
Thanks in advance. I can get required output when using var but i want to get required output by using Distinct in List<>.
InventoryDetails.cs
public class InventoryDetails
{
public int? PersonalInventoryGroupId { get; set; }
public int? PersonalInventoryBinId { get; set; }
}
InventoryController.cs
[HttpGet("GetInventory")]
public IActionResult GetInventory(int id)
{
//Below code will return distinct record
var inventory = (from i in _context.TempTbl
where i.TempId == id
select new
{
PersonalInventoryBinId = i.PersonalInventoryBinId,
PersonalInventoryGroupId = i.PersonalInventoryGroupId,
}).ToList().Distinct().ToList();
//Below code is not doing distinct
List<InventoryDetails> inventory = (from i in _context.TempTbl
where i.TempId == id
select new InventoryDetails
{
PersonalInventoryBinId = i.PersonalInventoryBinId,
PersonalInventoryGroupId = i.PersonalInventoryGroupId,
}).ToList().Distinct().ToList();
}
If i use var as return type, then i am able to get distinct records. Could some one assist it.
Please try like this it may help.
IList<InventoryDetails> inventory = _context.InventoryDetails.Where(x=>x.TempId == id).GroupBy(p => new {p.PersonalInventoryGroupId, p.PersonalInventoryBinId } )
.Select(g => g.First())
.ToList();
You need to override Equals and GetHashCode.
First, let's see the AnonymousType vs InventoryDetails
var AnonymousTypeObj1 = new { PersonalInventoryGroupId = 1, PersonalInventoryBinId = 1 };
var AnonymousTypeObj2 = new { PersonalInventoryGroupId = 1, PersonalInventoryBinId = 1 };
Console.WriteLine(AnonymousTypeObj1.Equals(AnonymousTypeObj2)); // True
var InventoryDetailsObj1 = new InventoryDetails { PersonalInventoryBinId = 1, PersonalInventoryGroupId = 1 };
var InvertoryDetailsObj2 = new InventoryDetails { PersonalInventoryBinId = 1, PersonalInventoryGroupId = 1 };
Console.WriteLine(InventoryDetailsObj1.Equals(InvertoryDetailsObj2)); // False
You can see the Equals behave differently which make Distinct behave differently. The problem is not var you mentioned in your question but AnonoymizeType
To make Distinct works as you expect, you need to override Equals and GetHashCode
public class InventoryDetails
{
public int? PersonalInventoryGroupId { get; set; }
public int? PersonalInventoryBinId { get; set; }
public override bool Equals(object obj)
{
if (obj == null) return false;
if (obj is InventoryDetails)
{
if (PersonalInventoryGroupId == (obj as InventoryDetails).PersonalInventoryGroupId
&& PersonalInventoryBinId == (obj as InventoryDetails).PersonalInventoryBinId)
return true;
}
return false;
}
public override int GetHashCode()
{
int hash = 17;
hash = hash * 23 + PersonalInventoryBinId.GetHashCode();
hash = hash * 23 + PersonalInventoryGroupId.GetHashCode();
return hash;
}
}
Another approach would be
List<InventoryDetails> inventory = (from i in TempTbl
where i.TempId == id
select new InventoryDetails
{
PersonalInventoryBinId = i.PersonalInventoryBinId,
PersonalInventoryGroupId = i.PersonalInventoryGroupId,
}).AsQueryable().ToList().Distinct(new customComparer()).ToList();
public class customComparer:IEqualityComparer<InventoryDetails>
{
public bool Equals(InventoryDetails x, InventoryDetails y)
{
if (x.TempId == y.TempId && x.PersonalInventoryBinId == y.PersonalInventoryBinId
&& x.PersonalInventoryGroupId == y.PersonalInventoryGroupId)
{
return true;
}
return false;
}
public int GetHashCode(InventoryDetails obj)
{
return string.Concat(obj.PersonalInventoryBinId.ToString(),
obj.PersonalInventoryGroupId.ToString(),
obj.TempId.ToString()).GetHashCode();
}
}
As said in a comment by Ivan, you make your life difficult by calling ToList before Distinct. This prevents the SQL provider from incorporating the Distinct call into the generated SQL statement. But that leaves the question: what causes the difference?
The first query generates anonymous type instances. As per the C# specification, by default anonymous types (in C#) are equal when their properties and property values are equal (structural equality). Conversely, by default, reference types (like InventoryDetails) are equal when their reference (say memory address) is equal (reference equality or identity). They can be made equal by overriding their Equals and GetHashcode methods, as some people suggested to do.
But that's not necessary if you remove the first ToList():
var inventory = (from i in _context.TempTbl
where i.TempId == id
select new InventoryDetails
{
PersonalInventoryBinId = i.PersonalInventoryBinId,
PersonalInventoryGroupId = i.PersonalInventoryGroupId,
}).Distinct().ToList();
Now the whole statement until ToList() is an IQueryable that can be translated into SQL. The SQL is executed and the database returns a distinct result set of raw records from which EF materializes InventoryDetails objects. The C# runtime code was even never aware of duplicates!
This is my class
public class Employee
{
public virtual List<Salary> Salaries { get; set; }
public bool CanAddSalary(Salary salary)
{
var count = (from x in Salaries where x.Month == salary.Month
&& x.Year == salary.Year select x).Count();
return count == 0;
}
public void AddSalary(Salary salary)
{
if(CanAddSalary(salary))
{
Salaries.Add(salary);
}
}
}
Entity Framework loads Employee with Salaries properties.
CanAddSalary method just check to avoid duplicates (Nothing modified the Salaries Collection right?).
If CanAddSalary return true then I call AddSalary method.
I call Entity Framework Context.SaveChanges() and throw me and exception.
You should use == instead of = in your return statement, because otherwise you'll always get false from CanAddSalary method:
public bool CanAddSalary(Salary salary)
{
var count = (from x in Salaries where x.Month == salary.Month
&& x.Year == salary.Year select x).Count();
return count == 0;
}
Your Salaries should really be
ICollection<Salary>
and doing an Any() will be faster than doing a Count()
Your
return count = 0;
is wrong which means
Salaries.Add(salary);
is adding a salary that is already in the Salaries collection, that's probably the exception
Try this, if you're still getting the exception, then post that on here too so we can see the error
cheers
Stu
public class Employee
{
public virtual ICollection<Salary> Salaries { get; set; }
public bool CanAddSalary(Salary salary)
{
return !(from x in Salaries where x.Month == salary.Month
&& x.Year == salary.Year select x).Any();
}
public void AddSalary(Salary salary)
{
if(CanAddSalary(salary))
{
Salaries.Add(salary);
}
}
}
I am performing a select query using the following Linq expression:
Table<Tbl_Movement> movements = context.Tbl_Movement;
var query = from m in movements
select new MovementSummary
{
Id = m.DocketId,
Created = m.DateTimeStamp,
CreatedBy = m.Tbl_User.FullName,
DocketNumber = m.DocketNumber,
DocketTypeDescription = m.Ref_DocketType.DocketType,
DocketTypeId = m.DocketTypeId,
Site = new Site()
{
Id = m.Tbl_Site.SiteId,
FirstLine = m.Tbl_Site.FirstLine,
Postcode = m.Tbl_Site.Postcode,
SiteName = m.Tbl_Site.SiteName,
TownCity = m.Tbl_Site.TownCity,
Brewery = new Brewery()
{
Id = m.Tbl_Site.Ref_Brewery.BreweryId,
BreweryName = m.Tbl_Site.Ref_Brewery.BreweryName
},
Region = new Region()
{
Description = m.Tbl_Site.Ref_Region.Description,
Id = m.Tbl_Site.Ref_Region.RegionId
}
}
};
I am also passing in an IFilter class into the method where this select is performed.
public interface IJobFilter
{
int? PersonId { get; set; }
int? RegionId { get; set; }
int? SiteId { get; set; }
int? AssetId { get; set; }
}
How do I add these where parameters into my SQL expression? Preferably I'd like this done in another method as the filtering will be re-used across multiple repositories.
Unfortunately when I do query.Where it has become an IQueryable<MovementSummary>. I'm assuming it has become this as I'm returning an IEnumerable<MovementSummary>. I've only just started learning LINQ, so be gentle.
Answer:
private IQueryable<Tbl_Docket> BuildQuery(IQueryable<Tbl_Docket> movements, IMovementFilter filter)
{
if (filter != null)
{
if (filter.PersonId.HasValue) movements = movements.Where(m => m.UserId == filter.PersonId);
if (filter.SiteId.HasValue) ...
}
return movements;
}
Which is called like follows:
var query = from m in this.BuildQuery(movements, filter)
select new... {}
You have to call the where statement before you fire your select statement, e.g.:
IQueryable<Tbl_Movement> movements = context.Tbl_Movement;
if (filter != null)
{
if (filter.PersonId != null) movements = movements.Where(m => m....PersonId == filter.PersonId);
if (filter.RegionId != null) movements = movements.Where(m => m....RegionId == filter.RegionId);
if (filter.SiteId != null) movements = movements.Where(m => m...SiteId == filter.SiteId);
if (filter.AssetId != null) movements = movements.Where(m => m...AssetId == filter.AssetId);
}
var query = m from movements...
As opposed to using this IFilter class, you might want to consider a Fluent Pipe-based Repository structure, e.g.:
var movements = new MovementsPipe()
.FindSiteId(1)
.FindAssetIds(1, 2, 3)
.FindRegionId(m => m > 10)
.ToMovementSummaryList();
Hope this helps. Let me know if you have any questions.