I am a little puzzled as to why this code doesn't work. I have a basic one-to-many relationship where I load a parent and include its children. I later I am trying to navigate from the child back to the parent, but the parent is null and I can't figure out why.
Question:
Why can't I query a graph of parent/child objects and navigate backward through them from child to parent? The parent is always null.
Here are the entities.
public class Budget
{
[Key]
public int Id { get; set; }
public ICollection<Expense> Expenses { get; set; }
public ICollection<Income> Incomes { get; set; }
}
public class Expense
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[StringLength(200)]
public string ExpenseName { get; set; }
[Required]
public decimal Cost { get; set; }
[StringLength(800)]
public string Notes { get; set; }
public DateTime? DueDate { get; set; }
public Budget Budget { get; set; }
}
public class Income
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public decimal Amount { get; set; } = 0;
[Required]
[StringLength(200)]
public string Source { get; set; }
[Required]
public DateTime? PayDate { get; set; } = DateTime.Now;
public Budget Budget { get; set; }
}
Here is the repository query.
public async Task<Budget> GetBudget(int id)
{
try
{
return await context.Budget
.Include(e => e.Expenses)
.Include(i => i.Incomes)
.SingleAsync(b => b.Id == id);
}
catch(Exception x)
{
x.ToString();
}
return null;
}
I want to be able to navigate back through the relation from expense to budget to get the Budget.Income collection.
Expected result:
foreach(Expense expense in Budget.Expenses)
{
if (expense.Budget is not null)
{
ICollection<Income> paychecks = expense.Budget.Incomes; // Why is Budget always null?
}
}
I expected that even if I didn't use the ThenInclude(e => e.Budget) that I should still be able to navigate from the child back to the parent {var budget = expense.Budget}. I'm surprised that this isn't working.
I didn't include the Income entity here, but my goal is to traverse expense.Budget.Incomes to get the collection of incomes in code where I only have access to the expense instance.
After removing ThenInclude(e => e.Budget) I no longer get an error, but the expense.Budget property is still null.
UPDATE
I believe that I found the root cause of my problem. When I added the property Budget to the Expense class I started getting an error when deserializing the objects coming from the API. The HttpClient was throwing an error due to a cyclical reference.
Because I'm fetching the root Budget entity and it's related Expenses, and the Expense has a reference to the Budget I got the cyclical reference error.
I added this code the the startup Blazor server startup class to fix it. I think this is my problem.
services.AddControllersWithViews().AddJsonOptions(x =>
x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);
If I change to ReferenceHandler.Preserve I get a different error.
'The JSON value could not be converted to System.Collections.Generic.ICollection`1[BlazorApp.Data.Models.Expense]. Path: $.expenses | LineNumber: 0 | BytePositionInLine: 34.'
What I don't know and would like to solve is how to make this work so I can get the Budget -> Expenses and have the Expense.Budget property point to it's parent Budget instance. My real issue is probably more related to json serialization and deserialization.
Thank you all for the help. Your input helped me know what wasn't the problem.
This fixed my problem.
WebAPI : JSON ReferenceHandler.Preserve
My original question doesn't relate to the actual answer. I did not realize what was really going on. The real problem was a serialization problem. I'm using a hosted Blazor app and have Client and Server projects.
The problem has a number of layers to it. I added the Budget property to the Expense class so that I could traverse back to the budget. After adding the Budget property I started getting an error about cyclical references. This error appears in the Client project during the HttpClient call to the Server controller. I fixed this error by adding the following code to the Server Startup class.
services.AddControllersWithViews().AddJsonOptions(options => {
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
options.JsonSerializerOptions.PropertyNamingPolicy = null; // prevent camel case
});
Days later when I start doing more work I tried to reference the expense.Budget property. Finding that it was always null and started trying to fix it in the entities which is the wrong place.
It turns out that I also needed to pass the JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve to the HttpClient code during the call to the controller.
public async Task<Budget> GetBudget(int id)
{
Budget budget = null;
try
{
budget = await httpClient.GetFromJsonAsync<Budget>("api/Budget/" + id, new JsonSerializerOptions() { ReferenceHandler = ReferenceHandler.Preserve });
}
catch(Exception x)
{
x.ToString();
}
return budget;
}
This foreach statement now works as expected.
foreach(Expense expense in Budget.Expenses)
{
if (expense.Budget is not null)
{
ICollection<Income> paychecks = expense.Budget.Incomes; // How do I populate budget?
}
}
You have a bug in your query. Income doesn't depend on expenses, it depends only on budget. The same is about expenses.
So your code can be translated to this
var budget= await context.Incomes
.Include(e => e.Expenses)
.Include(i => i.Incomes)
.SingleAsync(b => b.Id == id);
foreach (Expense expense in budget.Expenses)
{
var paychecks = budget.Incomes;
}
This foreach doesn't make any sense in this case since it just repeats
"var paychecks = budget.Incomes;" many times. It is not even saved anywhere.
The same can be done in one line
var paychecks= await context.Incomes
.Where(b => b.BudgetId == id);
.ToListAsync();
or if budget is downloaded already
var paychecks = budget.Incomes;
You can not use .ThenInclude(e => e.Budget) since your are querying context.Budget already and as you can see the Budget class doesn't have any extra Budget property.
And Expenses are list. List don't have any Budget property too, only items of the list have. If you try .Include(e => e.Expenses.Budget) it will give a syntax error. But since Expense or Income class has a Budget property this query will be valid
return await context.Incomes
.Include(i => i.Budget)
....
I think you need only this query that merges budget, income and expenses together
var budget= await context.Budgets
.Include(e => e.Expenses)
.Include(i => i.Incomes)
.FirstOrDefaultAsync(b => b.Id == id);
Also remove public ICollection<Expense> Expenses { get; set; } from Income or make it [NotMapped] if you need it for some reasons.
If you use EF Core 5+, EF Core automatically created foreign keys as the shadow properties. But I personally prefer to have them explicitly.
I recommend to add them to your classes (or you still can keep the current version)
public class Budget
{
[Key]
public int Id { get; set; }
.............
[InverseProperty("Budget")]
public virtual ICollection<Expense> Expenses { get; set; }
[InverseProperty("Budget")]
public virtual ICollection<Income> Incomes { get; set; }
}
public class Expense
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
...........
public int BudgetId { get; set; }
[ForeignKey(nameof(BudgetId ))]
[InverseProperty("Expenses")]
public virtual Budget Budget { get; set; }
}
public class Income
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
............
public int BudgetId { get; }
[ForeignKey(nameof(BudgetId ))]
[InverseProperty("Expenses")]
public virtual Budget Budget { get; set; }
}
Related
I have a .net core api application which includes EF to retrieve data. I have set up a data context and I can map tables from the db fine. When I try and set up a relationship though I am always getting a null back for the nested object.
I have an 'Opportunity' class which contains an ICollection of 'Notes'
public class Opportunity
{
public int Id { get; set; }
public string Name { get; set; }
...
public decimal FinalDealProfit { get; set; }
public ICollection<CRMNote> CRMNotes { get; set; }
}
and a Note class that references the opportunity:
public class CRMNote
{
public int Id { get; set; }
public int OpportunityId { get; set; }
public string Note { get; set; }
public string User { get; set; }
public DateTime DateTime { get; set; }
public string FilePath { get; set; }
public Opportunity Opportunity { get; set; }
}
In my context class have the following set up:
modelBuilder.Entity<Opportunity>(entity =>
{
entity.ToTable("CRM_Opportunity");
entity.HasMany<CRMNote>(n => n.CRMNotes)
.WithOne(t => t.Opportunity)
.HasForeignKey(k => k.OpportunityId);
});
and I have also been mapping the Note class:
modelBuilder.Entity<CRMNote>(entity =>
{
entity.ToTable("CRM_Note");
//entity.HasOne<Opportunity>(t => t.Opportunity)
// .WithMany(p => p.CRMNotes)
// .HasForeignKey(k => k.OpportunityId);
});
as you can see I have been playing around with how to connect the entities together.
Whenever I retrieve the opportunity though the notes array is always null. I have tried putting an empty constructor on the Opportunity class:
public Opportunity()
{
CRMNotes = new List<CRMNote>();
}
but this just means I get an empty array rather than a null.
I can't see what I have missed. I have checked the docs for it:
https://www.entityframeworktutorial.net/efcore/one-to-many-conventions-entity-framework-core.aspx
but clearly I have missed something. Any help greatly appreciated as this should be an easy task but something is clearly eluding me.
There are three common O/RM patterns used to load related data
Eager loading,
Explicit loading
and
Lazy loading
For example, in eager loading you can use:
var opportunities=context.opportunities.Include(opportunity=>opportunity.CRMNotes).ToList()
Here is a simplified version of my model:
public class User {
public int UserID { get; set; }
public string FirstName { get; set; }
public virtual ICollection<Recipe> Recipes { get; set; }
}
public class Recipe {
public int RecipeID { get; set; }
public string RecipeName { get; set; }
public int UserID { get; set; }
public virtual User User { get; set; }
}
I have a controller that I'd like to return a User as well as some summary information about their recipes. The scaffolded controller code looks like this:
var user = await _context.Users.SingleOrDefaultAsync(m => m.UserID == id);
It works fine. Now I try to add the Recipes, and it breaks:
var user = await _context.Users.Include(u => u.Recipes).SingleOrDefaultAsync(m => m.UserID == id);
My web browser starts to render the JSON, and it flickers and I get a message in the browser saying the connection has been reset.
My Theory - I believe that the parent (User) renders, which exposes the child (Recipe) which contains a reference to the parent (User), which contains a collection of the child (Recipe) and so on which is causing an infinite loop. Here's why I think this is happening:
The Visual Studio debugger allows me to navigate the properties in that way infinitely.
If I comment out the Recipe.User property, it works fine.
What I've tried
I tried to just include the data from Recipe that I need using Entity Framework projection (I'm attempting to not include Recipe.User). I tried to only include Recipe.RecipeName... but when I try to use projection to create an anonymous type like this:
var user = await _context.Users.Include(u => u.Recipes.Select(r => new { r.RecipeName })).SingleOrDefaultAsync(m => m.UserID == id);
I receive this error:
InvalidOperationException: The property expression 'u => {from Recipe r in u.Recipes select new <>f__AnonymousType1`1(RecipeName = [r].RecipeName)}' is not valid. The expression should represent a property access: 't => t.MyProperty'.
What is the solution? Can I project with different syntax? Am I going about this all wrong?
Consider using POCOs for serialization rather than doubly-linked entity classes:
public class UserPOCO {
public int UserID { get; set; }
public string FirstName { get; set; }
public ICollection<RecipePOCO> Recipes { get; set; }
}
public class RecipePOCO {
public int RecipeID { get; set; }
public string RecipeName { get; set; }
public int UserID { get; set; }
}
Copy the entity contents to the corresponding POCO and then return those POCO objects as the JSON result. The removal of the User property via usage of the RecipePOCO class will remove the circular reference.
I can propose you 3 options.
U sing [JsonIgnore] on property, but it will work on every use of Recipe class, so when you would like to just return Recipe class you won't have User in it.
public class Recipe {
public int RecipeID { get; set; }
public string RecipeName { get; set; }
public int UserID { get; set; }
[JsonIgnore]
public virtual User User { get; set; }
}
You can this solution to stop reference loop in all jsons https://stackoverflow.com/a/42522643/3355459
Last option is to create class (ViewModel) that will only have properties that you want send to the browser, and map your result to it. It is propably best from security reason.
I have been working on a shop site project, using asp.net core spa templates provided with the latest VS2017, and have come across an issue that I haven't had before, possibly because until now my apps were quite simple!
I know what the problem is and where, I just can't fix it. I have a product model which has a collection of "Attributes" and a collection of "Variations" (different colour size, etc) and those variations also have attributes, so if the same Attribute shows up in the Variation (VAttributes), as is already in the main "Attributes" I get the error
InvalidOperationException: The instance of entity type
'ProductAttribute' cannot be tracked because another instance with the
key value 'Id:2' is already being tracked. When attaching existing
entities, ensure that only one entity instance with a given key value
is attached.
The best answer I found was here : https://stackoverflow.com/a/19695833/6749293
Unfortunately, even with the above check I got the error, I even tried making a list of attached attributes, and if the vattribute matched one of the items in the list, I didn't attach it. In fact I found that even if I don't attach (_context.attach()) any of the vAttributes, it still throws the error!.
Here's the code in question:
public async Task<Product> Create(Product product)
{
try
{
foreach (var variation in product.Variations)
{
foreach (var vAttr in variation.VAttributes)
{
bool isDetached = _context.Entry(vAttr).State == EntityState.Detached;
if (isDetached)
_context.Attach(vAttr);
}
}
foreach (var attribute in product.Attributes)
{
bool isDetached = _context.Entry(attribute).State == EntityState.Detached;
if (isDetached)
_context.Attach(attribute);
}
foreach (var category in product.Categories)
{
_context.Attach(category);
_context.Attach(category).Collection(x => x.Children);
}
_context.Products.Add(product);
await Save();
return product;
}
catch (Exception)
{
throw;
}
}
The models for the 3 objects are as follows:
public class Product
{
[Key, DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
public string StockRef { get; set; }
public DateTime? LastModified { get; set; }
//image needed
public ICollection<ProductCategory> Categories { get; set; }
public ICollection<ProductAttribute> Attributes { get; set; }
public ICollection<ProductVariation> Variations { get; set; }
public Product()
{
Attributes = new List<ProductAttribute>();
Variations = new List<ProductVariation>();
Categories = new List<ProductCategory>();
}
}
Variation:
public class ProductVariation
{
[Key, DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime? LastModified { get; set; }
public virtual ICollection<ProductAttribute> VAttributes { get; set; }
//needs images
public decimal VPrice { get; set; }
public string VStockRef { get; set; }
}
Finally the Attribute:
public class ProductAttribute
{
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
[ForeignKey("AttributeCategory")]
public int AttributeCategoryId { get; set; }
public virtual AttributeCategory AttributeCategory { get; set; }
}
Most help I found when searching was more related to having repo's injected as singletons, or HttpPut methods where the code had check for existence omitting the .AsNoTracking() or it was a mistake that they had the second instance in some way, where I am aware of the second instance, I just don't know how to prevent it from being tracked!
EDIT: I found that adding a foreign key on the ProductVariation model to the Product that was being created failed as it was only a temp key!? anyway removed it from the variation model, so have updated my code. Also thought I'd add one of my earler failed attempts, that led to all of the foreach loops.
_context.AttachRange(product.Attributes);
_context.AttachRange(product.Categories);
_context.AttachRange(product.Variations);
_context.Add(product);
I believe you can allow EF to handle the tracking.
public virtual bool Create(T item)
{
try
{
_context.Add(item);
_context.SaveChanges();
return true;
}
catch (Exception e)
{
return false;
}
}
This allows for you to save the entire object structure without worring about attaching items.
var newProduct = new Product();
newProduct.Categories.Add(cat);
newProduct.Attributes.Add(att);
newProduct.Variations.Add(vari);
Create(newProduct);
I'm trying to setup a method that returns all presentations that are not overlapping with presentations you have signed up for. However, when trying to implement this I came across an error I can't seem to fix, I looked around and haven't been able to find any similar issues. Am I missing something obvious?
This is the Error:
InvalidOperationException: variable 't0' of type
'Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor+TransparentIdentifier`2[NameSpace.Models.Presentation,Microsoft.EntityFrameworkCore.Storage.ValueBuffer]'
referenced from scope '', but it is not defined
These are my models.
public class ApplicationUser : IdentityUser
{
public List<Presentation> MyPresentations { get; set; }
public List<PresentationUser> RegisteredPresentations { get; set; }
}
public class Presentation
{
public int PresentationId { get; set; }
public string HostId { get; set; }
public ApplicationUser Host { get; set; }
public List<PresentationUser> Attendees { get; set; }
public int TimeId { get; set; }
public PresentationTime Time { get; set; }
}
public class PresentationUser
{
public int PresentationId { get; set; }
public Presentation Presentation { get; set; }
public string ApplicationUserId { get; set; }
public ApplicationUser ApplicationUser { get; set; }
}
public class PresentationTime
{
public int PresentationTimeId { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
}
This is the method I can't get to work
private async Task<IQueryable<Presentation>> GetAvailablePresentations()
{
User user = await context.Users
.Include(u => u.RegisteredPresentations)
.ThenInclude(eu => eu.Presentation.Host)
.FirstOrDefaultAsync(u => u.Id == userManager.GetUserId(User));
var Presentations = context.Presentations
.Include(e => e.Host)
.Include(e => e.Time)
.Include(e => e.Attendees)
.ThenInclude(e => e.ApplicationUser)
// Filter Out Conditions
.Where(e => e.Attendees.All(u => u.ApplicationUserId != user.Id)) // Cannot see Presentations they are attending.
.Where(e => e.HostId != user.Id); // Cannot see their own Presentation
var debug = user.RegisteredPresentations.Select(ex => ex.Presentation).ToList();
// This section makes it so that users can't sign up for more that one Presentation per timeslot.
// Error Occurs Here
Presentations = Presentations.Where(e => debug.All(ex =>
ex.Time.EndTime < e.Time.StartTime || e.Time.EndTime < ex.Time.StartTime));
// This also does not work
// Presentations = Presentations.Where(e => debug.All(ex => ex.Time.StartTime != e.Time.StartTime));
return Presentations;
}
If anyone can help me fix this it would be huge help.
Note: I stripped a lot of other logic to help isolate this issue, so I may have a couple unnecessary .Include() in this code.
Presentations is not a list, it's still a IQueryable - a not-yet-executed query to DB. Applying Where you instruct EF to apply additional WHERE in SQL.
But debug is a list of objects in memory (.ToList()). How you think EF will transfer them back to DB?
If you want all filtering be applied in DB - you should change debug to list of something "simple" (list of ids?) - then EF will be able to pass this list back to DB.
Alternatively, you should read all suitable Presentations into memory (call .ToList()) and apply last filtering in memory. You may calculate min(StartTime) and max(EndTime) from debug and apply this two simple values to Presentations query (you will receive less unnecessary items) then read to memory and apply "strong" filtering in memory.
I am new to EF, Code First and DDD and still in the learning process, so let's say I have simple domain like this
public class Customer
{
public string Name { get; set; }
public string Address { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public DateTime OrderDate { get; set; }
public List<LineItem> LineItems { get; set; }
}
public class LineItem
{
public Product Product { get; set; }
public int Quantity { get; set; }
}
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
After domain is defined the next step is to create DbContext derived class and my question is how should it look like? What is a context class definition driven by? Is it by the use cases of the final application?
For example, looking at the domain above we can see that given the Customer instance we can access any child object. So is it then enough to make the context class contains only Customers property look like this:
class MyContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
}
With this I can browse customers from my app and then select one to see details and orders history etc... Looks good for now.
Now let's say I want following feature in my application:
- list last 10 orders in store (no matter who was the customer)
- list all orders with specific product in it
I guess that this data can be pulled from the current context right? Eg. last 10 orders:
using (var context = new MyContext())
{
var lastTenOrders = context.Customers.Include("Orders")
.Select(customer => customer.Orders)
.SelectMany(orderList => orderList)
.OrderByDescending(order => order.OrderDate)
.Take(10)
.ToList();
}
And getting all orders that contain product with Id = 5:
using (var context = new MyContext())
{
int productId = 5;
var lastTenOrders = context.Customers.Include("Orders")
.Select(customer => customer.Orders)
.SelectMany(orderList => orderList)
.Where(order => order.LineItems.Where(i => i.Product.Id == productId).Any())
.ToList();
}
(note I didn't test these queries so not sure if they work but they describe my general idea)
So I guess this would work but I am wondering is this the right path. Queries can get pretty complex here and it would probably be easier if I add say Orders and Products to the DbContext:
public class MyContext : DbContext
{
public DbSet Customers { get; set; }
public DbSet Orders { get; set; }
public DbSet Products { get; set; }
}
On the other hand I am not sure if I should add orders since they can already be retrieved from the Customer etc...
To sum it up, what are best practices when defining DbContext (and domain model for that matter) and should that be driven by the features (use cases) of the application? Feel free to use and change code in my example above in your explanation.
Consider protected setters for your properties.
Otherwise you could end up with 'data entities' rather then a proper domain model.
public class Customer
{
public string Address { get; protected set; }
public MoveTo(string newAddress)
{
if (newAddress == null) throw new ArgumentNullException("newAddress");
// and other address sanity checks..
Address = newAddress;
DomainEvent.Publish(new CustomerMoved(CustomerNumber, newAddress));
}
}