LINQ/EF: conceptual/mechanics of doing multiple joins that return both an entity and a computed value - entity-framework

Im hoping someone can help me with a brain block I'm having about how to do multiple joins with EF that returns an entity, where one of the fields is calculated by a query. Ive contrived a (pretty much useless) example in the hopes somebody can help me understand how this should be done.
Id like to return a list of ISPs entities from a DBContext with the "TotalUsed" property filled in by the LINQ query. Ive successfully done the joins (trivial), and have played with grouping and sum, but cant seem to get it quite right.
The general idea to this contrived example is that NICs are connected to one and only one router, and multiple routers are connected to a single ISP each.
How to write a LINQ query that sums the bandwidth needed for each ISP, along with the other properties in that ISP?
Expected output would be a list of ISP that might look like
{{1, "foo", 52}, {2, "bar", 345}, {3, "foobar", 621}}, where the 3rd property is the summation of the BandwidthNeeded property on all NICs transitively related to ISP.
The classes:
public class ISP
{
public int ISPid {get; set;}
public int ISPName {get; set;}
public int TotalUsed {get; set;} // not mapped to DB -> should populate via LINQ
}
public class Router
{
public int RouterId {get; set;}
public string RouterName {get; set;}
public int ISPId {get; set;} // foreign key for ISP
}
public class Nic
{
public int NicId { get; set; }
public string NicLocation { get; set; }
public int BandwidthUsed { get; set; }
public int RouterId {get; set; } // foreign key for router
}

If you move the TotalUsed property into a separate type, you could do this (please excuse any typos):
public class ISP // mapped
{
public int ISPid {get; set;}
public int ISPName {get; set;}
}
public class ISPWithTotalUsed : ISP // not mapped
{
public int TotalUsed { get; set; }
}
var query = (from ISP in context.ISPs
select new ISPWithTotalUsed
{
ISPid = ISP.ISPid,
ISPName = ISP.ISPName,
TotalUsed = (from router in context.Routers
where router.ISPid == ISP.ISPid
from nic in context.Nics
where nic.RouterId == router.RouterId
select nic.BandwidthUsed).Sum()
});
If you add navigation properties, it could be made somewhat more readable by removing the links on ID.

var context = new IspContext();
var groups = context.Nics.Include("Router.Isp").GroupBy(n => n.Router.ISPId).ToList();
var result = groups.Select(g => new()
{
Key = g.Key,
Name = g.FirstOrDefault().Router.Isp.IspName,
Total = g.Sum(n => n.BandwithUsed)
});
Does this work for you? The name thing is not pretty and you might get it all into one query. It's hard to tell if it runs faster though.

Something like this should work:
var query = context.ISPS.Select(isp => new{ ISP : isp, Used : isp.Routes.Sum(r => r.Nics.Sum(n => n.BandwidthUsed))}).ToList();
var result = query.Select(item => {
item.ISP.TotalUsed = item.Used;
return item.ISP;
}).ToList();
I would hoverwer remove the TotalUsed propery from ISP and instead create a wrapper class that holds both an ISP and the TotalUsed. Then you could remove the second query. (EF will not let you contruct an entity inside an entity query).

Related

EF Best practices - Shadow Properties

I generally create a domain model and use data annotations/explicit foreign key relationships in that domain model to define how the database is created. To me, this makes certain rules (like max length or required) clear and in one place.
I was going through a code review and one part of the feedback was to not do either of those things. They recommended I do everything in the configuration for each model using Fluent API/shadow properties instead so that all database generation code is in one place and that the model stays clean and only concerns itself with the domain alone.
As an example, I updated my code to something similar to this:
public class Blog
{
private HashSet<Comment> comments;
public IReadOnlyCollection<Comment> Comments => comments;
public string Title {get; private set;}
public string Content {get; private set;}
public Core.Constants.Blog.Categories Category {get; private set;}
public Blog(string title, string content, Core.Constants.Blog.Categories category)
{
Id = Guid.NewGuid().ToString("D");
comments = new HashSet<Comment>();
Title = title;
Content = content;
Category = category;
}
}
public class Comment{
private string Id {get; private set;}
public string Identifier { get; private set; }
public string Description { get; private set; }
public Comment(string identifier, string description)
{
Id = Guid.NewGuid().ToString("D");
Identifier = identifier;
Description = description;
}
}
and added fluent configuration code with:
internal class BlogConfiguration : IEntityTypeConfiguration<Domain.Blog>
{
public void Configure(EntityTypeBuilder<Domain.Blog> builder)
{
builder.ToTable("Blog");
builder.HasKey("Id").HasName("PK_Blog");
builder.Property(res => res.Title)
.IsRequired();
builder.Property(res => res.Content)
.IsRequired();
builder.Metadata
.FindNavigation(nameof(Domain.Blog.Comments))
.SetPropertyAccessMode(PropertyAccessMode.Field);
builder.Property(blog => blog.Category)
.HasConversion(value =>
value.ToString(),
convertedValue => Enum.Parse<Core.Constants.Blog.Categories>(convertedValue));
}
}
internal class CommentConfig : IEntityTypeConfiguration<Domain.Comment>
{
public void Configure(EntityTypeBuilder<Domain.Comment> builder) {
builder.ToTable("Comment");
builder.HasKey("Id").HasName("PK_Comment");
builder.Property(asset => asset.Identifier)
.HasMaxLength(25)
.IsRequired();
builder.Property(asset => asset.Description)
.HasMaxLength(250);
builder.HasIndex(asset => asset.Identifier).IsUnique();
}
}
My questions are:
In my actual project, I need a navigation property filled from "Comment" back to "Blog". Is it bad to add an explicitly navigation property on Comment that points back to Blog that is ignored in the fluent configuration?
Is using Data Annotations on the domain model really that bad of a practice?

What is the best practice regarding setting Id's of contained entities in entity framework core

Assuming i have the following entities that i store in a database using entity framework core
public class Container{
public int ContainerId {get; set;}
public int ContainedId {get; set;}
public Contained Contained {get; set;}
}
public class Contained {
public int ContainedId {get; set;}
public int ValueA {get; set;}
public int ValueB {get; set;}
}
if at some point in time i decide to save a Container class, with a know Contained class should i manually set the Id?
var containerToSave = new Container{
Contained = contained;
ContainedId = contained.ContainedId
}
or this is a bad practice? If I set it with an id that doesnt match the one in the actual class what will it save?
As far as Entity Framework goes you can specify either a foreign key or add the related entity when saving the main one.
To answer your question... No, is not a bad practice and this isn't related to the EF either. Rather your Container matches the Contained is a business related problem and should be treated and validated there. EF role in your case is to save the data whatever method you choose.
Assuming you mark the Contained EF will give you 2 choices:
public virtual Contained Contained { get; set; }
Add the foreign key:
var containerToSave = new Container
{
ContainedId = contained.ContainedId
}
Add the entity itself:
var containerToSave = new Container
{
Contained = new Contained
{
ValueA = x;
ValueB = y;
}
}
In you case, if you already have the Contained added, you can go with the first one but not the less, either of above methods are working just fine.

EF6:How to include subproperty with Select so that single instance is created. Avoid "same primary key" error

I'm trying to fetch (in disconnected way) an entity with its all related entities and then trying to update the entity. But I'm getting the following error:
Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
public class Person
{
public int PersonId { get; set; }
public string Personname { get; set }
public ICollection Addresses { get; set; }
}
public class Address
{
public int AddressId { get; set; }
public int PersonId { get; set; }
public string Line1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public Person Person { get; set; }
public ICollection<Feature> Features { get; set; }
}
// Many to Many: Represented in database as AddressFeature (e.g Air Conditioning, Central Heating; User could select multiple features of a single address)
public class Feature
{
public int FeatureId { get; set; }
public string Featurename { get; set; }
public ICollection<Address> Addresses { get; set; } // Many-To-Many with Addresses
}
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
var person = dbContext.People.AsNoTracking().Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}
public void UpdateCandidate(Person newPerson)
{
Person existingPerson = GetPerson(person.Id); // Loading the existing candidate from database with ASNOTRACKING
dbContext.People.Attach(existingPerson); // This line is giving error
.....
.....
.....
}
Error:
Additional information: Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Kindly suggest.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Since you are using a short lived DbContext for retrieving the data, all you need is to remove AsNoTracking(), thus allowing EF to use the context cache and consolidate the Feature entities. EF tracking serves different purposes. One is to allow consolidating the entity instances with the same PK which you are interested in this case, and the second is to detect the modifications in case you modify the entities and call SaveChanges(), which apparently you are not interested when using the context simply to retrieve the data. When you disable the tracking for a query, EF cannot use the cache, thus generates separate object instances.
What you really not want is to let EF create proxies which hold reference to the context used to obtain them and will cause issues when trying to attach to another context. I don't see virtual navigation properties in your models, so most likely EF will not create proxies, but in order to be absolutely sure, I would turn ProxyCreationEnabled off:
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
dbContext.Configuration.ProxyCreationEnabled = false;
var person = dbContext.People.Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}

Prevent AutoMapper Projections From Forcing Related Data Loading

Is there a way to configure AutoMapper to adhere to the .Include style loading instructions for Entity Framework?
I've disabled lazy loading for my context, and I want to conditionally load related data for particular entities. Ideally, I'd like to do this by using an include syntax. Something like:
if(loadAddreses)
{
query = query.Include(e => e.Addresses);
}
if(loadEmails)
{
query = query.Include(e => e.Emails);
}
The problem is, AutoMapper is seeing that the model I'm projecting to includes Addresses and E-mails, and is generating SQL that loads all that data regardless of what I've asked EF to include. In other words:
var model = query.Project.To<MyModel>();
If MyModel has an Addresses collection, it will load addresses, regardless of my Include statements.
Short of changing my model so that I have one that doesn't have an Addresses or Emails property, is there a way to fix this? I suppose I could change my mapping, but mappings are usually static and don't change after they're initially created.
This was kind of tricky to tease out, but see how this works for you. Note that I'm using version 3.3.0-ci1027 of AutoMapper (at the time of writing this was a pre-release).
Assume my data model looks like this:
public class Address
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int AddressId { get; set; }
public string Text { get; set; }
}
public class Email
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int EmailId { get; set; }
public string Text { get; set; }
}
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public virtual ICollection<Address> Addresses { get; set; }
public virtual ICollection<Email> Emails { get; set; }
public User()
{
this.Addresses = new List<Address>();
this.Emails = new List<Email>();
}
}
My view models are not specified but they just contain the same properties as the entities.
My mapping from User to UserViewModel looks like this:
Mapper.CreateMap<User, UserViewModel>()
.ForMember(x => x.Emails, opt => opt.ExplicitExpansion())
.ForMember(x => x.Addresses, opt => opt.ExplicitExpansion());
And my projection looks like this:
var viewModels = context.Set<User>().Project()
.To<UserViewModel>(new { }, u => u.Emails).ToList();
With that mapping and projection, only the Emails collection is loaded. The important parts to this are the opt => opt.ExplicitExpansion() call in the mapping - which prevents a navigation property being followed unless explicitly expanded during projection, and the overloaded To method. This overload allows you to specify parameters (which I've left as an empty object), and the members you wish to expand (in this case just the Emails).
The one thing I'm not sure of at this stage is the precise mechanism to extract the details from the Include statements so you can in turn pass them into the To method, but hopefully this gives you something to work with.

Searching the Entity Framework domain model utilising Code First

Got a very difficult EntityFramework Code First question. I'll keep this as simple as possible.
Imagine we have n number of classes, lets start with 2 for now
public class Person
{
public string Name { get; set; }
}
public class Address
{
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
}
Now then, what I want to do is be able to search the domain model with a single string, i.e. something like DbContext.Search( "Foo" ). The call would search both the person and address tables for a string match and would return a list populated with both Person and Address entities.
Have to say I am not entirely clear how to go about it but I am considering using DataAnnotations to do something like this
public class Person
{
**[Searchable]**
public string Name { get; set; }
}
public class Address
{
**[Searchable]**
public string AddressLine1 { get; set; }
**[Searchable]**
public string AddressLine2 { get; set; }
}
Am I on the right track?
Should I use the Fluent API instead?
Reflection?
Any and all thoughts massively appreciated.
the Find method searches only in the Primary Key column. If we don't make any column explicitly primary key column then find method will throw error. Generally EF convention takes propertyName+id as the primary key in the class. But if you want to search with Name then Make add [Key] to the property. it will become primary key and u will be able to find properties.
dbContext.Addresses.find("Foo");
Create a new object type onto which you'll project 2 types of search results:
public class Result
{
public string MainField { get; set; }
// you may have other properties in here.
}
Then find entities of each type that match your criteria, projecting them onto this type:
var personResults = DbContext.Persons
.Where(p => p.Name == "Foo")
.Select(p => new Result{MainField = p.Name});
// don't forget to map to any other properties you have in Result as well
var addressResults = DbContext.Adresses
.Where(a =>
a.AddressLine1 == "Foo" ||
a.AddressLine2 == "Foo"
).
.Select(a => new Result{MainField = a.AddressLine1 + ", " + a.AddressLine2 });
// again, don't forget to map to any other properties in Result
Then merge the lists:
var allResults = personResults.Union(addressResults).ToList();
...at which point you can sort the list however you like.
"Result" and "MainField", are rather generic; just using them because I am not thoroughly aware of your domain model.