Creating dynamic queries with Entity Framework across multiple tables? - entity-framework

I'm trying to create search functionality across a couple of tables, following the pattern in Creating dynamic queries with entity framework
I have 3 tables:
People:
pk ID
varchar FirstName
varchar LastName
fk AddressMap_ID
AddressMap:
pk ID
Address:
pk ID
varchar StreetName
varchar StreeNumber
fk AddressMap_ID
Multiple people can live at one address. I pass in a Search model, and populate the results property:
public class Search
{
public string streetname { get; set; }
public string streetnumber { get; set; }
public string fname { get; set; }
public string lname { get; set; }
public IEnumerable<Results> results { get; set; }
}
public class Results
{
public int AddressID { get; set; }
public string StreetNumber { get; set; }
public string StreetName { get; set; }
public IEnumerable<PeopleResults> people { get; set; }
}
public class PeopleResults
{
public int personID { get; set; }
public string First { get; set; }
public string Last { get; set; }
}
This works if I filter on an address, or name + address:
public void GetResults(Search model)
{
Entities _context;
_context = new Entities();
var addr = from a in _context.Addresses
select a;
addr = addr.Where(filter => filter.StreetNumber == model.streetnumber);
addr = addr.Where(filter => filter.StreetName == model.streetname);
addr = from a in addr
group a by a.AddressMap_ID into addrs
select addrs.FirstOrDefault();
var ppl = from p in addr.SelectMany(p => p.AddressMap.People) select p;
ppl = ppl.Where(filter => filter.FirstName.StartsWith(model.fname));
ppl = ppl.Where(filter => filter.LastName.StartsWith(model.lname));
model.results = from a in addr
select new Results
{
AddressID = a.ID,
StreetName = a.StreetName,
StreetNumber = a.StreetNumber,
people = from p in ppl
select new PeopleResults
{
First = p.FirstName,
Last = p.LastName
}
};
}
But if I just try to filter on a name, it returns a cartesian join - every single address with all of the people that matched.
There are 3 ways to search: filtering on address only, filter on address + name, or filter on name only.
So if someone search for "123 Main", the results should be
123 Main St SticksVille Joe Smith
Jane Smith
Mary Smith
123 Main St Bedrock Fred Flintstone
Wilma Flintstone
A search for "J Smith 123 Main" should return just:
123 Main St SticksVille Joe Smith
Jane Smith
And a search for just "J Smith" should return:
123 Main St SticksVille Joe Smith
Jane Smith
456 Another St Sometown Jerry Smith

Your query looks "symmetric" to me with respect to people and addresses, it only gets "asymmetric" in the final projected result. So, my idea is to express this symmetry in the query as far as possible:
Get a set (IQueryable<Address>, not executed at once) of addresses filtered by street name and street number
Get a set (IQueryable<Person>, not executed at once) of people filtered by the beginning of first name and last name
Join the two sets by AddressMap_ID. The resulting set of people and addresses contains only those pairs that fulfill the filter criteria for addresses and people. If one of the filter criteria for person or address is not supplied (the first and the third of your examples at the bottom of the question) the join happens on the unfiltered set of all people/addresses, i.e. the joined pairs contain always all people of the filtered address (or all addresses of the filtered people)
Group the joined pairs of people and addresses by Address.ID
Project the groups into your Results collection. The group key is the AddressID. StreetName and StreetNumber can be fetched from the first address in each group and the people are projected from the people in each group.
Execute the query
The following code doesn't cover the case specifically that none of the four filter criteria is supplied. It works in that case but would just load all addresses with all people of those addresses. Maybe you want to throw an exception in that case. Or return nothing (model.Results = null or so), then just jump out of the method.
public void GetResults(Search model)
{
using (var _context = new Entities())
{
// "All" addresses
IQueryable<Address> addresses = _context.Addresses;
// "All" people
IQueryable<Person> people = _context.People;
// Build a Queryable with filtered Addresses
if (!string.IsNullOrEmpty(model.streetname))
addresses = addresses.Where(a => a.StreetName
.StartsWith(model.streetname));
if (!string.IsNullOrEmpty(model.streetnumber))
addresses = addresses.Where(a => a.StreetNumber
.StartsWith(model.streetnumber));
// Build a Queryable with filtered People
if (!string.IsNullOrEmpty(model.fname))
people = people.Where(p => p.FirstName == model.fname);
if (!string.IsNullOrEmpty(model.lname))
people = people.Where(p => p.LastName == model.lname);
// Join the two Queryables with AddressMap_ID
// and build group by Address.ID containing pairs of address and person
// and project the groups into the Results collection
var resultQuery = from a in addresses
join p in people
on a.AddressMap_ID equals p.AddressMap_ID
group new { a, p } by a.ID into g
select new Results
{
AddressID = g.Key,
StreetName = g.Select(ap => ap.a.StreetName)
.FirstOrDefault(),
StreetNumber = g.Select(ap => ap.a.StreetNumber)
.FirstOrDefault(),
people = g.Select(ap => new PeopleResults
{
First = ap.p.FirstName,
Last = ap.p.LastName
})
};
// Execute query (the whole code performs one single query)
model.results = resultQuery.ToList();
}
}
I am unsure if I interpret the AddressMap table correctly as a kind of join table for a many-to-many relationship (Address can have many people, Person can have many addresses), but the code above yields the three results of the three queries in your example as expected if the tables are filled like so:
The AddressMap table isn't actually used in the query because Addresses and People table are joined directly via the AddressMap_ID columns.

It seems like an approach like this would probably work:
IQueryable<Person> ppl = _context.People;
ppl = addr.Where(filter=>filter.First.StartsWith(model.fname));
ppl = addr.Where(filter=>filter.Last.StartsWith(model.lname));
var pplIds = ppl.Select(p => p.PersonId);
model.results = from a in addr
where a.AddressMap.People.Any(p => pplIds.Contains(p.PersonId))
select new Results {
AddressID = a.ID,
StreetName = a.StreetName,
StreetNumber = a.StreetNumber,
people = from p in a.People
select new PeopleResults {
First = p.FirstName,
Last = p.LastName
}
};
Rather than basing the people property on the matching people, you want to base the entire address set on the matching people.

Related

Using Expression to return selective columns on entities having multiple objects

I have an application where I have a model composed of several other objects. For instance:
class Customer
{
public int CustomerId{get;set;}
public int? AddressId {get;set;} // this is set as allow null in database
public string Name {get;set}
public virtual Addresss Address {get;set;}
}
class Address
{
public int AddressId {get;set}
public string A1 {get;set}
}
The idea is to use context.customers.include("Address"). However the model I am currently working on is much more complex than the above.
I have used https://stackoverflow.com/a/51772067 as a reference, but unfortunately this does not work for an id having no value (nullable, as the database allows nulls)
How can I modified the expression to behave as a true left join (includes an empty entity if the id is null).
Thanks in advance for your assistance
As per the official doc you could do left join as below
List<Person> people = new List<Person> { magnus, terry, charlotte, arlene };
List<Pet> pets = new List<Pet> { barley, boots, whiskers, bluemoon, daisy };
var query = from person in people
join pet in pets on person equals pet.Owner into gj
from subpet in gj.DefaultIfEmpty()
select new { person.FirstName, PetName = subpet?.Name ?? String.Empty };
When you use DefaultIfEmpty() then it becomes a left join.
Upvote if it works.

.NET Core - join 2 tables with AsQueryable

I have two nested classes: Partner contains a Company (that has a field "Name")
I do a search by Id on the partner's Id
I want to do a search on the company's "Name" field
here is my poco:
public class Partner
{
[Required]
public int? Id { get; set; }
[Required]
public Company Company { get; set; }
using AsQueryable, I can then stack filters one by one
I try to have a query that joins the second table to do a search on that entity's name field
public DbSet<Partner> Partners { get; set; }
...
var data = _context.Partners.AsQueryable();
if (partnersSearch.SearchById != null)
{
data = data.Where(p => p.Id == partnersSearch.SearchById.GetValueOrDefault());
}
if (partnersSearch.SearchByName != null)
{
data = data.Include(a => a.Company.Select(b => b.Name = partnersSearch.SearchByName));
but for the join between the tables, the last line cannot compile
it complains that Company does not contain a definition of has no Select
what am I doing wrong ?
thanks for helping me on this
If you try a where after your include. Does that help?
data.Include(a => a.Company).Where(partner=>partner.Company.Name.equals(partnersSearch.SearchByName))

Linq - Create new object which contains a collection

I am trying to get data from Entity Framework using linq to populate a custom object. The problem is the object contains a list collection where I am having problems populating.
I have the following
public class Person
{
public string FirstName { get; set; }
public string ListName { get; set; }
public IList<Address> ContactAddresses v{ get; set; }
}
public class Address
{
public string FirstLine { get; set; }
public string SecondLine { get; set; }
public string Town { get; set; }
}
I am trying to populate the Person object but having trouble populating the list of addresses. The situation I have at the moment one address is stored in the Person table and work and other addresses are stored in another. Right now I am only trying to get the address stored in the Person table. Below is my attempt at this but syntax is not working.
Person details = context.Person.Where(p => p.Id == id)
.Select(p => new Person)
{
FirstName = p.FirstName,
LastName = p.LastName,
ContactAddresses = new List<Address>()
.add(new Address
(
FirstLine = m.FirstLineAddress,
SecondLine = m.SecondLine,
Town = m.Town
))
}.FirstOrDefault();
UPDATE:
I worked out how to do this. The following linq is my solution.
Person details = context.Person.Where(p => p.Id == id)
.Select(p => new Person)
{
FirstName = p.FirstName,
LastName = p.LastName,
ContactAddresses = (new List<Address>()
{
new Address()
{
FirstLine = p.FirstLine ,
SecondLine = p.SecondLine,
Town = p.Town
}
}).ToList(),
}.FirstOrDefault();
This is not really a problem of LINQ. It is more about object initialization. First rule (for me) if an object has a collection, it should never be null. It can be empty, but never null. (Another question would be, if a collection property should be settable or not, but that's another story.)
public class Person
{
public string FirstName { get; set; }
public string ListName { get; set; }
public IList<Address> ContactAddresses { get; set; } = new List<Address>();
}
And then you can initialize the object like this:
var person = new Person
{
FirstName = "Foo",
ListName = "Bar",
ContactAddresses = { new Address { FirstLine = "A", SecondLine = "B", Town = "C" } }
};
As you can see, the contact addresses has an initializer that doesn't use new. So the list of objects will be added to the existing list.
If you want to replace the list instead of changing it, you can also provide a new list:
var person = new Person
{
FirstName = "Foo",
ListName = "Bar",
ContactAddresses = new List<Address> { new Address { FirstLine = "A", SecondLine = "B", Town = "C" } }
};
More about object initialization can be found at Microsoft.
You wrote:
The situation I have at the moment one address is stored in the Person table and work and other addresses are stored in another.
So you have a class Person, which has a contact Address, and also a collection of zero or more alternative Addresses. This might be a one-to-many relation, or a many-to-many relation, for a Person class this does not really matter:
class Person
{
public int Id {get; set;}
public Address MainAddress {get; set;}
... // other properties
// every Person has zero or more Alternative Addresses:
public virtual ICollection<AlternativeAddress> AlternativeAddresses {get; set;}
}
class AlternativeAddress
{
public int Id {get; set;}
public Address Address {get; set;}
... // other properties
// if this Address belongs to exactly one Person (one-to-many)
public int PersonId {get; set;}
public virtual Person Person {get; set;}
// or if this address may be the address of several Persons (many-to-many)
public virtual ICollection<Person> Persons {get; set;}
}
In entity framework the columns of a table are represented by the non-virtual properties. The virtual properties represent the relations between the tables (one-to-many, many-to-many, ...)
Address is a class. Because I didn't declare MainAddress virtual, the properties of Address will be columns within Person. Similarly, the properties of the Address in AlternativeAddress will be columns within an AlternativeAddress.
If you didn't use the Class Address to make sure that your MainAddress has exactly the same structure as the AlternativeAddresses you'll have to do a Select.
This is all that entity framework needs to know to detect your tables and the relations between the tables, the primary and foreign keys. Only if you want different identifiers you need attributes or fluent API.
Requirement Given an input parameter personId, Give me (some properties of) the Person with all his Addresses.
var result = dbContext.Persons
.Where(person => person.Id == personId)
.Select(person => new
{
// select only the properties you plan to use:
Id = person.Id,
FirstName = person.FirstName,
...
// For the contact Addresses, concat the MainAddress and the Alternative Addresses
ContactAddresses = new Address[] {person.MainAddress}
.Concat(person.AlternativeAddresses.Select(address => address.Address)
// use a Select if you don't need all Address properties:
.Select(address => new
{
Street = address.Street,
City = address.City,
PostCode = address.PostCode,
...
});
If classes Person and AlternativeAddress don't have an Address Properties, you'll have to select the properties you want:
ContactAddresses =
// Main Address
new []
{
Street = person.Street,
City = person.City,
PostCode = person.PostCode,
...
}
.Concat(person.AlternativeAddresses.Select(address => new
{
Street = address.Street,
City = address.City,
PostCode = address.PostCode,
...
}),

MVC/Entity framework CORE searching through a linked model's data

Following the Contoso university tutorials, I've got a Person class which links to an Address class to give a one-to-many relationship
public class Person{
public string FullName { get; set; }
public virtual ICollection<Address> AddressIDs { get; set; }
}
public class Address {
public string Postcode { get; set; }
[ForeignKey("PersonID")]
[Display(Name = "Person ID")]
public int? PersonID { get; set; }
public virtual Person objPerson { get; set; }
}
So my view shows all of a person's addresses like this, which all works fine and for an individual person I get a list of all of their addresses.
#foreach (var item in Model.AddressIDs) {
#Html.DisplayFor(modelItem => item.Address1)
#Html.DisplayFor(modelItem => item.Address2)
#Html.DisplayFor(modelItem => item.Postcode)
In the controller I use
var persons = from ps in _context.FullPerson
select ps;
persons = persons.Where(ps => ps.objPerson.Forename.Contains("Smith"));
to bring back all the "Smith"s from the database. How can I extend this so I can search for only those records with a certain postcode?
persons = persons.Where(ps => ps.objPerson.Postcode.Contains("SW9"));.
This doesn't work because Postcode is in the connected Address model, not the Person model
persons = persons.Where(ps => ps.objPerson.AddressIDs.something.Contains("SW9"));
This doesn't work as the something are object references (I think) like Add, Aggregate<>, All<>, etc.
Am I supposed to use LINQ to join these two together (even though I think they're joined together already via EF) ?
Thanks
Edit
Using the answer below I got a list of the PersonIDs with the postcode I'm searching for
IQueryable<int> PersonIDsWithThisPostcode = _context.Addresses.Where(pst => pst.Postcode.Contains(p)).Select(b => b.PersonID);
Now I need to do a SQL IN command along these lines
persons = persons.Where(ps => ps.HumanID.Contains(PersonIDsWithThisPostcode));
this doesn't work because
"int does not contain a definition for contains"
Effectively, this is the SQL for the data I'm trying to retrieve
SELECT * FROM person
WHERE personid IN(
SELECT personid FROM address
WHERE postcode LIKE 'sw%'
)
try :
var addresses= from adr in _context.Address
select adr;
addresses= addresses.Where(adr => adr.PostCode.Contains("zipCode") &&
adr.objPerson.Forename.Contains("Smith"));
So, even though everything is linked via models and entity framework, I've still got to write LINQ to get the two things linked.
IQueryable<int> PersonIDsWithThisPostcode = _context.Addresses.Where(pst => pst.Postcode.Contains(p)).Select(b => b.PersonID);
That gets all of the people with the postcode I'm searching for
persons = persons.Where(ps => PersonIDsWithThisPostcode.Contains(ps.PersonID));
That get's only the people in that list.
Thanks Gusti, you led me down the right path.

Entity Framework - DRY Queries

I have a Entity Framework Domain model (using code first) with a context that includes Customers, and each Customer can have multiple Addresses with a 'calendar range' of dates on each address. I can write a query such as:
var query = from c in context.Customers
where c.CustomerId == 1
select c
Customer cust = query.Single();
The resulting customer is the one I selected via my Where clause. No problem. Now I also want to get their mailing address for a view, so I'd do:
var query = from c in context.Customers
where c.CustomerId == 1
select new
{
FirstName = c.FirstName,
Address = c.Addresses.Where(a => a.AddressStartDate > DateTime.Now &&
a.AddressEndDate < DateTime.Now)
}
var data = query.Single();
Address MailingAddress = data.Address;
Again, no problem, I get customer information and the current mailing address and the query executes in SQL.
Now I want to factor out the query that does the finding of the mailing address. I don't want to repeat it in every call that needs to get a mailing address. Ideally I'd like to add it to my domain object Customer so that the logic for finding mailing address is a part of my Customer object.
I want to place a method like this in my Customer object:
public partial class Customer
{
public Address MailingAddress
{
get
{
return (from a in this.Addresses.AsQueryable()
where a.AddressStartDate > DateTime.Now &&
a.AddressEndDate < DateTime.Now
select a).Single()
}
}
}
Now that I have the property in my Domain Model, I want to run a query:
var query = from c in context.Customers
where customerId == 1
select new
{
FirstName = c.FirstName,
Address = c.MailingAddress
}
Unfortunately this does not work with the error 'Only initializers, entity members, and entity navigation properties are supported.' I understand the error, and why I can't do it that way. I also know that my method would actually work if I retrieved the Customer first and then called the MailingAddress property after I had an instance of a Customer, but this doubles the database calls.
How can I stay DRY using Entity Framework? I need to centralize the code for the 'get mailing address' requirement while also making sure I execute the logic for 'get mailing address' in SQL and in one db operation?
The general way I've gone about solving this is to have a navigation property to the address so the class would look like:
public class Customer
{
public int Id { get; set; }
public int MailingAddressId { get; set; }
[ForeignKey("MailingAddressId")]
public virtual Address MailingAddress { get; set; }
public ICollection<Address> Addresses { get; set; }
}
Then you can go:
var customer = context.Customer.Include("MailingAddress").FirstOrDefault(c => c.Id = 1);