Entity Framework - DRY Queries - entity-framework

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);

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.

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.

Prevent EF from trying to update/insert child objects [duplicate]

This question already has answers here:
How do I stop Entity Framework from trying to save/insert child objects?
(13 answers)
Closed 3 years ago.
I have an Address object that has a City property. When creating a brand new Address to be inserted via EF6, I fill all required basic Address properties (address line 1, postal code, etc), but I don't need a fully hydrated City instance so long as it has the ID like so:
address.City = new City { Id = 1 };
When I attempt to insert my Address, it also attempts to do validation on the City's properties, but I don't want to do any CRUD on City since all I need is its ID.
I found the below question that introduced me to detaching entries from the DbContext so that EF does not attempt to do CRUD on said objects:
How do I stop Entity Framework from trying to save/insert child objects?
What seems to be happening when I detach the City instance is that it also nulls it out, so my Address has a null City property. This is a problem because City is also required, so a DbEntityValidationException is thrown saying "The City field is required".
I am new to EF, so perhaps the way I am going about all of this is just wrong to begin with.
Edit By request, here's all my code:
Building my Address entity in my client before passing it to WebApi endpoint:
var user = new AppUser { Id = 1 };
var address = new Address
{
City = new City { Id = 277 },
Line1 = "123 whatever ln",
PostalCode = "01233",
CreatedBy = user,
ModifiedBy = user,
CreatedOn = DateTime.Today,
ModifiedOn = DateTime.Today
};
In my ASP.NET app, I create an array of instances I want to detach from the context:
Detached = new object[] {
value.Principle.ModifiedBy,
value.Principle.CreatedBy,
value.Principle.City
};
Just before the save, I detach all instances in the array:
foreach (var d in DetachedObjects)
{
dbContext.Entry(d).State = EntityState.Detached;
}
dbContext.SaveChanges();
What I thought was happening with detaching properties was that it was simply telling EF not to do any CRUD on them, but I didn't want it to null them out because I want the parent/principle entity to have the ID for its FK.
Here are my Address and City classes:
[DebuggerDisplay("{Line1}")]
public class Address : CisEntity
{
[MaxLength(200)]
[Required]
public string Line1 { get; set; }
[MaxLength(200)]
public string Line2 { get; set; }
[Required]
public City City { get; set; }
[MaxLength(25)]
public string PostalCode { get; set; }
}
[DebuggerDisplay("{Name}, {Province.Name}, {Province.Country.Name}")]
public class City : CisEntity, IEntityName
{
[Required]
public Province Province { get; set; }
[MaxLength(100)]
public string Name { get; set; }
}
If you don't want the City to be required when performing CRUD, remove the required attribute. If you truly need a City for your Address in your program, but not in the database, then do as you are and null out the City piece before performing CRUD on Address. Otherwise it will be inserted. You should go check out what your tables look like in your database. EF will be tracking these in separate tables, and the City column on Address will be a foreign key. If you decorate your City property on Address with the required attribute, it means that the column is non-nullable. This means that in the database this column must contain a foreign key to a record in the City table, hence a City must exist.
I believe I understand your question and I'm answering 3 years later. LOL.
For the main entity,
context.Addresses.Add(address);
for the related or child entities,
context.Entry(address.City).State = EntityState.Modified;
Do this for every other related entity.

Creating dynamic queries with Entity Framework across multiple tables?

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.

IQueryable Entity Framework POCO Mappings

I am using ASP.NET MVC2 with EF4. I need to create POCOs for two of my classes PersonP and AddressP, which correspond to their EF4 'complex' classes (which include things like navigation properties and OnPropertyChanged()). Mapping just PersonP by itself works fine, but PersonP contains AddressP (foreign key) - how do I map this using an IQueryable expression?
Here is what I've tried:
class AddressP
{
int Id { get; set; }
string Street { get; set; }
}
class PersonP
{
int Id { get; set; }
string FirstName { get; set; }
AddressP Address { get; set; }
}
IQueryable<PersonP> persons = _repo.QueryAll()
.Include("Address")
.Select(p => new PersonP
{
Id = p.Id,
FirstName = p.FirstName,
//Address = p.Address <-- I'd like to do this, but p.Address is Address, not AddressP
//Address = (p.Address == null) ? null :
//new AddressP <-- does not work; can't use CLR object in LINQ runtime expression
//{
// Id = p.Address.Id,
// Street = p.Address.Street
//}
});
Without the .Include("Address") I would not retrieve anything from the Address table is this correct?
How do I map Address to AddressP inside PersonP, using the Select() statement above?
Thank you.
That is correct, if you've disable Lazy Loading or your object context has been disposed already and cannot be used to get Lazy Loading working.
Yes, it won't work since first you need to execute your query and then start mapping it, otherwise your mapping logic would be taken to be run in the database hence the exception. Something like this will work:
// First we execute the query:
IQueryable<PersonP> persons = _repo.QueryAll().Include("Address").ToList();
// Now we have a IEnumerable and we can safely do the mappings:
persons.Select(p => new PersonP
{
Id = p.Id,
FirstName = p.FirstName,
Address = (p.Address == null) ? null : new AddressP()
{
Id = p.Address.Id,
Street = p.Address.Street
}
}).ToList();
While this solution will do the trick but if the intention is to have POCO classes you should definitely consider to take advantage of EF4.0 POCO support and use POCO classes directly with EF instead of mapping them afterward. A good place to start would be this walkthrough:
Walkthrough: POCO Template for the Entity Framework