Problem
I have following repository, which queries db and constructs custom objects:
public class PatientCardRepository
{
public PatientCardRepository(DbSet<PersonModel> people)
{
_people = people;
}
private DbSet<PersonModel> _people;
public IEnumerable<PatientCardObject> GetPatientCardDataWithVisits(int personId)
{
return _people.AsNoTracking()
.Where(person => person.Id == personId)
.Select(person => new PatientCardObject
{
Person = new Person // COMMON PART
{
FirstName = person.FirstName,
LastName = person.LastName,
Addresses = person.Addresses
.Where(address => address.IsCurrent && address.AddressTypeId == AddressType)
.Select(address => new Address
{
City = address.City,
Street = address.Street,
StreetNo = address.StreetNo,
ZipCode = address.ZipCode
}),
},
Visits = person.PatientVisits.Select(visit => new Visit
{
Description = visit.Description,
StartTime = visit.StartTime,
EndTime = visit.EndTime,
})
}).Take(100);
}
public IEnumerable<PatientCardObject> GetPatientCardData(int personId)
{
return _people.AsNoTracking()
.Where(person => person.Id == personId)
.Select(person => new PatientCardObject
{
Person = new Person // COMMON PART
{
FirstName = person.FirstName,
LastName = person.LastName,
Addresses = person.Addresses
.Where(address => address.IsCurrent && address.AddressTypeId == AddressType)
.Select(address => new Address
{
City = address.City,
Street = address.Street,
StreetNo = address.StreetNo,
ZipCode = address.ZipCode
}),
}
}).Take(100);
}
}
I want to extract the COMMON PART (get rid of the copy-paste).
Failed attempts
I have tried following solutions, but all failed:
Changing Select clause to multi-line expression:
public IQueryable<PatientCardObject> GetPatientCardDataWithVisits(int personId)
{
return _people.AsNoTracking()
.Where(person => person.Id == personId)
.Select(person =>
{
var p = new PatientCardObject();
p.Person = CreatePersonFromModel(person);
return p;
});
}
This fails because Select accepts only expression lambdas (multi-lines are not allowed)
Using Inlcudes in the first place, then Select after materialization.
public IEnumerable<PatientCardObject> GetPatientCardDataWithVisits(int personId)
{
var filteredPeople = (IEnumerable)(_people.AsNoTracking()
.Include(person => person.Address)
.Include(person => person.PatientVisits)
.Where(person => person.Id == personId));
return filteredPeople
.Select(person =>
{
var p = new PatientCardObject();
p.Person = CreatePersonFromModel(person);
return p;
}).Take(100);
}
This fails, because it selects too many rows and columns. In this example all addresses for person are selected, not only current (filtering is done after materialization)
So we'll start out by having a method to get all of the information that you need by using the method that selects the most information. From that, we'll modify it to return an IQueryable rather than an item, to allow for deferred execution:
private IQueryable<PatientCardObject> GetPatientCardDataWithVisitsHelper(int personId)
{
return _people.AsNoTracking()
.Where(person => person.Id == personId)
.Select(person => new PatientCardObject
{
Person = new Person // COMMON PART
{
FirstName = person.FirstName,
LastName = person.LastName,
Addresses = person.Addresses
.Where(address => address.IsCurrent && address.AddressTypeId == AddressType)
.Select(address => new Address
{
City = address.City,
Street = address.Street,
StreetNo = address.StreetNo,
ZipCode = address.ZipCode
}),
},
Visits = person.PatientVisits.Select(visit => new Visit
{
Description = visit.Description,
StartTime = visit.StartTime,
EndTime = visit.EndTime,
})
});
}
(This is just GetPatientCardDataWithVisits without the call to First and a different name.)
Then we'll just have two calls to it, one that returns the first item, another than "removes" the unneeded information:
public PatientCardObject GetPatientCardDataWithVisits(int personId)
{
return GetPatientCardDataWithVisitsHelper(personId).First();
}
public PatientCardObject GetPatientCardData(int personId)
{
return GetPatientCardDataWithVisitsHelper(personId)
.Select(person => new PatientCardObject
{
Person = person.Person,
Visits = person.Visits.Where(v => false),
}).First();
}
Your second example has almost got it right. Try this:
public IEnumerable<PatientCardObject> GetPatientCardDataWithVisits(int personId)
{
var filteredPeople = _people.AsNoTracking()
.Include(person => person.Address)
.Include(person => person.PatientVisits)
.Where(person => person.Id == personId)
.Take(100)
.AsEnumerable()
.Select(x => new PatientCardObject { Person = CreatePersonFromModel(x) });
return filteredPeople;
}
This will only retrieve 100 from the DB.
Related
I have a entity model
public class User{
public string FirstName {get;set;}
public string LastName {get;set;}
public string Department {get;set;}
}
So I want to search a text like "john smith" in database using entity framework core 3.1.
I am splitting the text before.
public async Task<IEnumerable<UserListViewModel>> Search(string search)
{
var terms = search.Split(" ");
var queryable = _context.Users.Where(s => terms.All(m => s.Department.ToLower().Contains(m)) ||
terms.All(m => s.FirstName.ToLower().Contains(m)) ||
terms.All(m => s.LastName.ToLower().Contains(m))).AsQueryable();
...........
...........
...........
}
but does it not work.
So how can I do it?
EF Core 3.x doesn't really support translation of All and Any in most cases, and your code is slightly wrong, I think what you really want is:
var queryable = _context.Users.Where(u => terms.All(m => u.Department.Contains(m) ||
u.FirstName.Contains(m) ||
u.LastName.Contains(m)));
Since this can't be translated, you need to reformat it into code that can.
With LINQKit you can use PredicateBuilder to create an extension that will remap the query into a series of && tests for each term:
// searchTerms - IEnumerable<TKey> where all must be in a row's key
// testFne(row,searchTerm) - test one of searchTerms against a row
// dbq.Where(r => searchTerms.All(s => testFne(r,s)))
public static IQueryable<T> WhereAll<T,TKey>(this IQueryable<T> dbq, IEnumerable<TKey> searchTerms, Expression<Func<T, TKey, bool>> testFne) {
var pred = PredicateBuilder.New<T>();
foreach (var s in searchTerms)
pred = pred.And(r => testFne.Invoke(r, s));
return dbq.Where((Expression<Func<T,bool>>)pred.Expand());
}
which you would use like:
var queryable = _context.Users
.WhereAll(terms,
(u,m) => u.Department.Contains(m) ||
u.FirstName.Contains(m) ||
u.LastName.Contains(m));
For "john smith", the extension method would create the equivalent of:
var queryable = _context.Users
.Where(u => (u.Department.Contains("john") ||
u.FirstName.Contains("john") ||
u.LastName.Contains("john")) &&
(u.Department.Contains("smith") ||
u.FirstName.Contains("smith") ||
u.LastName.Contains("smith"))
);
How about the following.
void Main()
{
var users = new List<User>
{
new User { FirstName = "John", LastName = "Smith", Department = "Web" },
new User { FirstName = "Aaliyah", LastName = "Lin", Department = "Warehouse" },
new User { FirstName = "Cristian", LastName = "Stone", Department = "Cleaning" },
new User { FirstName = "Kierra", LastName = "Davidson", Department = "Mobile" },
new User { FirstName = "Lizbeth", LastName = "Gregory", Department = "Web" }
};
var search = "Lizbeth Gregory";
var terms = search.ToLower().Split(' ');
users.Where(s => terms.All(m => s.Department.ToLower().Contains(m)) ||
(terms.Any(m => s.FirstName.ToLower().Contains(m))) ||
terms.Any(m => s.LastName.ToLower().Contains(m)))
.Dump();
}
public class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Department { get; set; }
}
I think you don't need terms.All. as "john smith" is a full name all of it is not gonna be found in a first or last name field.
I'm not sure if the following is possible.
var queryable = _context.Users.Where(s => terms.Contains(m => s.Department.ToLower().Contains(m)) &&
terms.Contains(m => s.FirstName.ToLower().Contains(m)) ||
terms.All(m => s.LastName.ToLower().Contains(m))).AsQueryable();
Though it's not quite accurate, it'd return "john john" as well this way, but it's rare.
An alternative solution would be to aggregate your search terms:
var terms = search
.Split(' ', StringSplitOptions.RemoveEmptyEntries);
query = terms
.Aggregate(query, (current, term) =>
current.Where(x =>
x.FirstName.Contains(term)
|| x.LastName.Contains(term)
|| x.Department.Contains(term)
)
);
is there a way to store the select clause of a linq statement as a variable?
I have the following class and the select clause is repeated can I store it as a variable somehow so I don't need to repeat it?
public class TractRepository : ITractRepository
{
private DataContext context;
public TractRepository(DataContext ctx) => context = ctx;
public async Task<IEnumerable<Tract>> GetAllAsync() =>
await context.Tracts.Include(p => p.ContractType).Include(p => p.ContractSubType).OrderByDescending(p => p.Id)
.Select(p => new Tract
{
Id = p.Id,
Acreage = p.Acreage,
Administrative = p.Administrative,
ContractType = new ContractType
{
Id = p.ContractType.Id,
ContractTypeName = p.ContractType.ContractTypeName
},
ContractSubType = new ContractSubType
{
Id = p.ContractSubType.Id,
ContractSubTypeName = p.ContractSubType.ContractSubTypeName
}
})
.ToListAsync();
public async Task<Tract> GetByIdAsync(long id) =>
await context.Tracts.Include(p => p.ContractType).Include(p => p.ContractSubType)
.Select(p => new Tract
{
Id = p.Id,
Acreage = p.Acreage,
Administrative = p.Administrative,
ContractType = new ContractType
{
Id = p.ContractType.Id,
ContractTypeName = p.ContractType.ContractTypeName
},
ContractSubType = new ContractSubType
{
Id = p.ContractSubType.Id,
ContractSubTypeName = p.ContractSubType.ContractSubTypeName
}
}).FirstOrDefaultAsync(p => p.Id == id);
}
You can extract the whole common IQueryable<T> to separate property/method. Since LINQ queries are not executed until enumerated (the so called deferred execution), it can be used as base for composing other queries, e.g.
private IQueryable<Tract> Query() => context.Tracts
//.Include(p => p.ContractType)
//.Include(p => p.ContractSubType)
.Select(p => new Tract
{
Id = p.Id,
Acreage = p.Acreage,
Administrative = p.Administrative,
ContractType = new ContractType
{
Id = p.ContractType.Id,
ContractTypeName = p.ContractType.ContractTypeName
},
ContractSubType = new ContractSubType
{
Id = p.ContractSubType.Id,
ContractSubTypeName = p.ContractSubType.ContractSubTypeName
}
});
(Includes are redundant (ignored) for projection (Select) queries)
then
public async Task<IEnumerable<Tract>> GetAllAsync() =>
await Query().OrderByDescending(p => p.Id).ToListAsync();
and
public async Task<Tract> GetByIdAsync(long id) =>
await Query().FirstOrDefaultAsync(p => p.Id == id);
I'm just wondering if there's a better way to write this code, basically the source object contains a mix of items with a boolean property however the destination object has two lists which should contain the true/false items independently.
I've written it in Linq and it works just fine but it feels as though there's a better way. Any suggestions?
void Main()
{
var s = new ResponseObject()
{
Results = new List<GroupedObject>()
{
new GroupedObject()
{
Name = "List A",
List=new List<DetailObject>()
{
new DetailObject{ Name = "Allowed", AllowedAccess = true},
new DetailObject{ Name = "Restricted", AllowedAccess = false}
}
},
new GroupedObject()
{
Name = "List B",
List=new List<DetailObject>()
{
new DetailObject{ Name = "Allowed", AllowedAccess = true},
new DetailObject{ Name = "Restricted", AllowedAccess = false}
}
}
}
};
var d = new ResponseViewModel();
d.AllowedResults = FilterObjectsByAccess(s.Results, true);
d.RestrictedResults = FilterObjectsByAccess(s.Results, false);
// Other stuff
}
public IEnumerable<GroupedObject> FilterObjectsByAccess(IEnumerable<GroupedObject> source, bool allowAccess)
{
return source.Where(i => i.List.Any(c => c.AllowedAccess == allowAccess))
.Select(i => new GroupedObject()
{
Name = i.Name,
List = i.List.Where(c => c.AllowedAccess == allowAccess)
});
}
public class ResponseObject
{
public IEnumerable<GroupedObject> Results { get; set; }
}
public class ResponseViewModel
{
public IEnumerable<GroupedObject> AllowedResults { get; set; }
public IEnumerable<GroupedObject> RestrictedResults { get; set; }
}
public class GroupedObject
{
public string Name { get; set; }
public IEnumerable<DetailObject> List { get; set; }
}
public class DetailObject
{
public string Name { get; set; }
public bool AllowedAccess { get; set; }
}
One change that may be worth benchmarking would be changing:
public IEnumerable<GroupedObject> FilterObjectsByAccess(IEnumerable<GroupedObject> source, bool allowAccess)
{
return source.Where(i => i.List.Any(c => c.AllowedAccess == allowAccess))
.Select(i => new GroupedObject()
{
Name = i.Name,
List = i.List.Where(c => c.AllowedAccess == allowAccess)
});
}
to:
public IEnumerable<GroupedObject> FilterObjectsByAccess(IEnumerable<GroupedObject> source, bool allowAccess)
{
return source
.Select(i => new GroupedObject()
{
Name = i.Name,
List = i.List.Where(c => c.AllowedAccess == allowAccess).ToList() // `ToList` here is optional - it is a trade-off between RAM and CPU
})
.Where(z => z.List.Any());
}
Your original code, with the use of Any then Where would enumerate i.List twice. The above change would likely improve that.
Another approach, which would likely involve even higher memory consumption could be to switch to using ToLookup:
var d = new ResponseViewModel
{
AllowedResults =
FilterObjectsByAccess(s.Results)
.Select(z => new GroupedObject() { Name = z.Name, List = z.GroupedList[false] })
.Where(z => z.List.Any()),
RestrictedResults =
FilterObjectsByAccess(s.Results)
.Select(z => new GroupedObject() { Name = z.Name, List = z.GroupedList[true] })
.Where(z => z.List.Any())
};
// Other stuff
}
public List<SpecialGroupedObject> FilterObjectsByAccess(IEnumerable<GroupedObject> source)
{
return source.Select(i => new SpecialGroupedObject()
{
Name = i.Name,
GroupedList = i.List.ToLookup(c => c.AllowedAccess)
}).ToList();
}
I can suggest you to use ToDictionary() like this:
var result = new[] {true, false}.ToDictionary(k => k,
v =>
s.Results.Where(w => w.List.Any(x => x.AllowedAccess == v))
.Select(c => new GroupedObject {Name = c.Name, List = c.List.Where(l => l.AllowedAccess == v)}));
var allowedResults = result[true];
var restrictedResults = result[false];
Or this:
var result = s.Results
.SelectMany(c => c.List, (b, c) => new {b.Name, DObj = c})
.GroupBy(g => g.DObj.AllowedAccess)
.ToDictionary(k=> k.Key,
c =>
new {
c.Key,
List =
c.GroupBy(cg => cg.Name)
.Select(
x => new GroupedObject {Name = x.Key, List = x.Select(l => l.DObj).ToList()})
.ToList()
});
Hi I can do this in method syntax but I'm trying to improve my lambda skills how can I do:
SELECT SUM([job_group_quota]) as 'SUM'
FROM [dbo].[tbl_job_session]
WHERE [job_group_job_number] = #jobnum
and [job_group_ID] like #sess
GROUP BY [job_group_job_number]
I've been messing around with it but can't get it right.
lnq.tbl_job_sessions.GroupBy(a => a.job_group_job_number == jnum)
.Select(b => new { b.job_group_quota}).Sum();
A general example:
query
.GroupBy(item => item.GroupKey)
.Select(group => group.Sum(item => item.Aggregate));
Few Group by Examples
public void GroupBy1()
{
var personList = dbEntities.People.GroupBy(m => m.PersonType).Select(m => new { PersonType = m.Key, Count = m.Count() });
}
public void GroupBy2()
{
var personList = dbEntities.People.GroupBy(m => new { m.PersonType, m.FirstName }).Select(m => new { PersonType = m.Key, Count = m.Count() });
}
public void GroupBy3()
{
var personList = dbEntities.People.Where(m => m.EmailPromotion != 0).GroupBy(m => new { m.PersonType, m.FirstName }).Select(m => new { PersonType = m.Key, Count = m.Count() });
}
public void GroupBy4()
{
var personList = dbEntities.People.GroupBy(m => new { m.PersonType, m.FirstName }).Where(m => m.Count() > 70).Select(m => new { PersonType = m.Key, Count = m.Count() });
}
public void GroupBy5()
{
var personList = dbEntities.People
.GroupBy(m =>
new
{
m.PersonType
}).Where(m => m.Count() > 70)
.Select(m =>
new
{
PersonType = m.Key,
Count = m.Count()
});
var list1 = dbEntities.People.
GroupBy(m => new { m.PersonType }).
Select(m =>
new
{
Type = m.Key,
Count = m.Count()
})
.Where(
m => m.Count > 70
&& m.Type.PersonType.Equals("EM")
|| m.Type.PersonType.Equals("GC"));
}
public void GroupBy6()
{
var list1 = dbEntities.People.
GroupBy(m => new { m.PersonType, m.EmailPromotion }).Select(m =>
new
{
Type = m.Key,
Count = m.Count()
})
.Where
(
m => m.Count > 70 && m.Type.EmailPromotion.Equals(0) &&
(
m.Type.PersonType.Equals("EM") ||
m.Type.PersonType.Equals("GC")
));
}
public void GroupBy7()
{
var list1 = dbEntities.People.
GroupBy(m => m.PersonType).
Select(c =>
new
{
Type = c.Key,
Total = c.Sum(p => p.BusinessEntityID)
});
}
public void GroupBy8()
{
var list1 = dbEntities.People.
GroupBy(m => m.PersonType).
Select(c =>
new
{
Type = c.Key,
Count = c.Count(),
Total = c.Sum(p => p.BusinessEntityID)
});
}
public void GroupBy9()
{
var list1 = dbEntities.People.
GroupBy(m => m.PersonType).
Select(c =>
new
{
Type = c.Key,
Max = c.Max(),
});
}
If you want to get a key-to-sum Dictionary result.
var allJobQuota = jobSessions.GroupBy(s => s.jobNumber)
.ToDictionary(g => g.Key, g => g.Sum(s => s.quota));
This example shows how to iterate the grouped values getting the key and totals, and how to get totals directly (like previous). Both using only lambda operator.
using System;
using System.Collections.Generic;
using System.Linq;
public class Person
{
public string Name { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
public int SomeValue { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> data = GetPopulatedData();
var totals = data.GroupBy(x =>
new { x.Name, x.City, x.ZipCode }).Select(y =>
y.Sum(i => i.SomeValue));
var groupsForIterate = data.GroupBy(x =>
new { x.Name, x.City, x.ZipCode });
Console.WriteLine("Totals: ");
foreach (var total in totals)
{
Console.WriteLine(total);
}
Console.WriteLine("Categories: ");
foreach (var categ in groupsForIterate)
{
// You can refer to one field like this: categ.Key.Ciduad
Console.WriteLine("Group" + categ.Key);
Console.WriteLine(categ.Sum(x => x.SomeValue));
}
//Output:
//Totals:
//1
//2
//1
//Categories:
//Group{ Name = Mark, City = BCN, ZipCode = 00000 }
//1
//Group{ Name = Mark, City = BCN, ZipCode = 000000 }
//2
//Group{ Name = John, City = NYC, ZipCode = 000000 }
//1
}
private static List<Person> GetPopulatedData()
{
List<Person> datos = new List<Person>()
{
new Person(){Name="Mark", City = "BCN",
ZipCode = "00000", SomeValue = 1}, // group A
new Person(){Name="Mark", City = "BCN",
ZipCode = "000000", SomeValue = 1}, // group B
new Person(){Name="Mark", City = "BCN",
ZipCode = "000000", SomeValue = 1}, // group B
new Person(){Name="John", City = "NYC",
ZipCode = "000000", SomeValue = 1}, // group C
};
return datos;
}
}
Sum Ficha_Venda and Entrada from Movimento:
var query = from bd in db.Movimento
where (movimento.Data != null ? bd.Data == movimento.Data : bd.Data == movimento.Data)
&& (bd.Loja == Loja)
group bd by bd.Data into t
select new {entrada = t.Sum(bd=> bd.Entrada), ficha = t.Sum(bd=> bd.Ficha_Venda)};
I'm starting to love Lambda expressions but I'm struggling to pass this wall:
public class CompanyWithEmployees {
public CompanyWithEmployees() { }
public Company CompanyInfo { get; set; }
public List<Person> Employees { get; set; }
}
My search:
List<CompanyWithEmployees> companiesWithEmployees = ws.GetCompaniesWithEmployees();
CompanyWithEmployees ces = companiesWithEmployees
.Find(x => x.Employees
.Find(y => y.PersonID == person.PersonID));
So, I want to get the Object "CompanyWithEmployees" that have that Person (Employee) that I'm looking for, but I'm getting "Cannot implicit convert 'Person' To 'bool')" which is correct, but if I'm not passing the Person object, how can the first Find executes?
Because you want to check for existance, perhaps try:
ces = companiesWithEmployees
.Find(x => x.Employees
.Find(y => y.ParID == person.ParID) != null);
This will check for any Person with the same ParID; if you mean the same Person instance (reference), then Contains should suffice:
ces = companiesWithEmployees
.Find(x => x.Employees.Contains(person));
Find() returns the found object. Use Any() to just check whether the expression is true for any element.
var ces = companiesWithEmployees
.Find(x => x.Employees
.Any(y => y.PersonID == person.PersonID));
ces = companiesWithEmployees
.First(x => x.Employees.Any(p=>p.PersonID == person.PersonID));
ces = companiesWithEmployees.Find( x => x.Employees.Find(...) );
.Find returns only one object, x.Employees.Find(..) returns Person.
.Find expects boolean parameter(i.e. the result of conditions), that's why there's a compiler error that says Cannot implicit convert 'Person' To 'bool'
.Where can return multiple objects, hence can iterate through all list.
use a combination of .Where and .Any in your case.
the following code will illustrate the difference between .Where, .Find, and .Any:
public partial class Form2 : Form {
public Form2() {
InitializeComponent();
var companiesWithEmployees = new List<CompanyWithEmployees>() {
new CompanyWithEmployees {
CompanyInfo = new Company { CompanyName = "Buen" },
Employees = new List<Person>() {
new Person { PersonID = 1976, PersonName = "Michael" },
new Person { PersonID = 1982, PersonName = "Mark" },
new Person { PersonID = 1985, PersonName = "Matthew" },
new Person { PersonID = 1988, PersonName = "Morris" }
}
},
new CompanyWithEmployees {
CompanyInfo = new Company { CompanyName = "Muhlach" },
Employees = new List<Person>() {
new Person { PersonID = 1969, PersonName = "Aga" },
new Person { PersonID = 1971, PersonName = "Nino" },
new Person { PersonID = 1996, PersonName = "Mark" }
}
},
new CompanyWithEmployees {
CompanyInfo = new Company { CompanyName = "Eigenmann" },
Employees = new List<Person>() {
new Person { PersonID = 1956, PersonName = "Michael" },
new Person { PersonID = 1999, PersonName = "Gabby" }
}
}
};
// just explicitly declared the types (instead of var) so the intent is more obvious
IEnumerable<CompanyWithEmployees> whereAreMichaels = companiesWithEmployees
.Where(cx => cx.Employees.Any(px => px.PersonName == "Michael"));
string michaelsCompanies = string.Join(", ", whereAreMichaels
.Select(cx => cx.CompanyInfo.CompanyName).ToArray());
MessageBox.Show("Company(s) with employee Michael : " + michaelsCompanies);
Person findAga = companiesWithEmployees
.Find(company => company.CompanyInfo.CompanyName == "Muhlach")
.Employees.Find(person => person.PersonName == "Aga");
if (findAga != null)
MessageBox.Show("Aga's ID : " + findAga.PersonID.ToString());
}
}
class CompanyWithEmployees {
public Company CompanyInfo { get; set; }
public List<Person> Employees { get; set; }
}
class Company {
public string CompanyName { get; set; }
}
class Person {
public int PersonID { get; set; }
public string PersonName { get; set; }
}
That's because you haven't specified a legitimate Find expression for your top level Find.
I'll show it here:
ces = companiesWithEmployees
.Find (x => x.Employees.Find(y => y.ParID == Person.ParID) /*condition is missing here*/);
So what is the condition for your initial find?
The easiest one would be
ces = companiesWithEmployees.FirstOrDefault(x =>
x.Employees.Any(y => y.PersonID == person.ParID));
without any null check