I generated my DbContext and entity types using the Scaffold-DbContext command in powershell.
I can successfully retrieve records from the database however, the relationships always seem to be null.
Asset.cs
class Asset
{
[ForeignKey("CategoryId")]
public virtual AssetCategory Category { get; set; } = null!;
}
The relationship configuration appears to be OK in the DbContext file that was automatically generated.
entity.HasOne(d => d.Category)
.WithMany(p => p.Assets)
.HasForeignKey(d => d.CategoryId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Asset_AssetCategory");
What I do note is that the Constraints folder within the database for the Asset table is empty?
Example usage
// basic properties of asset are correctly hydrated here, just not relationships
var asset = await _dbContext.Assets.FindAsync(id);
// category is always null
var category = asset.Category;
This happens with all the relationships defined on the entity model and I don't understand why?
Thanks to the comments from DavidG pointing out I needed to use the Include method to load the relationship(s) as they are not automatically lazy loaded.
var asset = _dbContext.Assets
.Where(a => a.id == id)
.Include(a => a.Category)
.FirstOrDefaultAsync();
Related
The join table of a many-to-many relationship in my Xamarin.Forms application seems to not be cleared correctly when deleting one of the two entities.
I have these classes:
public class Input
{
// One-to-many
public ObservableCollection<InputResult> InputResults { get; set; }
//...
// Here are many more entities which shouldn't be relevant for this example
//...
}
public class InputResult
{
// One-to-many
public string ParentInputId { get; set;}
// Many-to-many
public ObservableCollection<MyDropdown> MyDropdowns { get; set; }
}
public class MyDropdown
{
// Many-to-many
public ObservableCollection<InputResult> InputResults { get; set; }
}
I configured the relationships in my DbContext class like this:
modelBuilder.Entity<Input>()
.HasMany(b => b.InputResults)
.WithOne()
.HasForeignKey(b => b.ParentInputId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<InputResult>()
.HasMany(b => b.MyDropdowns)
.WithMany(b => b.InputResults);
Let's say I have previously loaded an Input with its InputResults. Then I delete the InputResults in a helper class like that:
Context.RemoveRange(Input.InputResults);
The InputResults get deleted correctly. When I look into the SQLite database directly I still see all the entries in the join table of InputResults and MyDropdown. Why are there still entries? Yesterday one of our users got a unique constraint error after deleting some data and trying to insert the same data again.
I appreciate any help.
Edit:
To expand my comment on CSharp's answer:
I can't use OnDelete(DeleteBehavior.Cascade) when configuring the DbContext. It seems as EF Core did this correctly by itself though. The part of the join table in the DatabaseContextModelSnapshot.cs looks like this:
modelBuilder.Entity("InputResultMyDropdown", b =>
{
b.HasOne("Inputs.MyDropdown", null)
.WithMany()
.HasForeignKey("MyDropdownId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Inputs.InputResult", null)
.WithMany()
.HasForeignKey("InputResultId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
There should be a cascade delete behavior in DbContext:
modelBuilder.Entity<InputResult>()
.HasMany(b => b.MyDropdowns)
.WithMany(b => b.InputResults)
.OnDelete(DeleteBehavior.Cascade);
public class Book
{
public int Id { get; set; }
public string Name{ get; set; }
public string ISBN { get; set; }
public ICollection<Category> Categories { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name{ get; set; }
public ICollection<Book> Books { get; set; }
}
I a using Entity Framework core 6 with .NET 6.
I am trying to update the Categories of a specific Book.
For example, If one book has categories like .NET, C# then I want to update categories into .NET, EF Core, SqlServer, I think you get it now.
Do I need to add a Join entity for only the Update operation? As you can see I have not created any Join entity like BookCategories though I managed to Insert categories while creating Book for the first time.
But when trying to update the book with new categories I am getting two issues.
The old category is not deleted.
And getting Duplicate Error Key while trying to update with existing category, in this case, .NET.
Please kindly show the proper way of updating related entities in Entity Framework Core 6 in .NET6.
Many-to-Many relationships need a bit of configuration depending on what you want out of the relationship. If you just want the linking table to manage the link and nothing else:
[BookCategories]
BookId (PK, FK)
CategoryId (PK, FK)
Then you can set up the relationship to either use an entity definition or a shadow entity. In both cases this is typically preferable since your Book can have a collection of Categories, and the Category can have a collection of books. With Code-First and Migrations I believe EF can and will set up this linking table automatically. Otherwise you can use OnModelCreating or an EntityTypeConfiguration to configure what Table and Columns to use for the relationship.
This can be done either with an Entity declared for BookCategory, or without one:
With entity:
modelBuilder.Entity<Book>()
.HasMany(x => x.Categories)
.WithMany(x => Books);
.UsingEntity<BookCategory>(
l => l.HasOne<Book>().WithMany().HasForeignKey(x => x.BookId),
r => r.HasOne<Category>().WithMany().HasForeignKey(x => x.CategoryId),
j =>
{
j.HasKey("BookId", "CategoryId");
j.ToTable("BookCategories");
});
Without entity: (See Scaffolding many-to-many relationships - https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-6.0/whatsnew)
modelBuilder.Entity<Book>()
.HasMany(x => x.Categories)
.WithMany(x => Books);
.UsingEntity<Dictionary<string, object>>(
"BookCategories",
l => l.HasOne<Book>().WithMany().HasForeignKey("BookId"),
r => r.HasOne<Category>().WithMany().HasForeignKey("CategoryId"),
j =>
{
j.HasKey("BookId", "CategoryId");
j.ToTable("BookCategories");
});
Alternatively, if the joining table needs to contain additional relevant details, for example if you are using a soft-delete system and want to mark deleted relationships as inactive rather than deleting those rows, then you have to adopt an indirect relationship using a BookCategory entity where Book has a collection of BookCategories, as does Category. (See Join entity type configuration - https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key)
Once you have your relationships set up, it is important to treat these relationships as associations, not copies of data. This means you should ensure that your collections are initialized on construction, and never reset. You can add items to the collection or remove items from the collection, but you should never have code that resets the collection. (I.e. no code that does stuff like book.Categories = new List<Category>() or book.Categories = myUpdatedCategories etc.) While EF is tracking entities, it is relying on proxies to help with change tracking to know when data needs to be added, removed, or updated. This also means if you want to "change" a book's category, this is a remove and add, not an update.
For instance to change a book's category from "Java" to ".Net", you don't want to do something like:
var book = context.Books.Include(x => x.Categories).Single(x => x.BookId == bookId);
var category = book.Categories.SingleOrDefault(x => x.CategoryName == "Java");
if (category != null)
category.CategoryName = ".Net"; // or category.CategoryId = dotNetCategoryId;
This would attempt to modify the Category record to change it's Name (likely not intended) or attempt to change it's PK. (illegal)
Instead, you want to change the association:
var dotNetCategory = context.Categories.Single(x => x.CategoryId == dotNetCategoryId);
var book = context.Books.Include(x => x.Categories).Single(x => x.BookId == bookId);
var category = book.Categories.SingleOrDefault(x => x.CategoryName == "Java");
if (category != null)
{
book.Categories.Remove(category);
book.Categories.Add(dotNetCategory);
}
Behind the scenes, EF will delete the BookCategory linking the book to Java category, and insert a BookCategory with the new .Net association. If you have a joining entity then you will just need to remove, add, or update the BookCategory entity specifically based on the relationship changes you want to make.
After migration from EF Core 3 to EF Core 6 this query:
private async Task<Variation[]> GetPizzasInOrder(Uuid[] productsInOrder, CancellationToken ct)
{
return await _clientCheckupsGatewayContext.MetaProducts
.SelectMany(mp => mp.Variations)
.Where(v => productsInOrder.Contains(v.Id))
.Include(v => v.MetaProduct)
.ToArrayAsync(ct);
}
started to throw error:
System.InvalidOperationException: A tracking query is attempting to project an owned entity without a corresponding owner in its result, but owned entities cannot be tracked without their owner. Either include the owner entity in the result or make the query non-tracking using 'AsNoTracking'.
Changing to 'AsNoTracking()' gives another error:
private async Task<Variation[]> GetPizzasInOrder(Uuid[] productsInOrder, CancellationToken ct)
{
return await _clientCheckupsGatewayContext.MetaProducts
.AsNoTracking()
.SelectMany(mp => mp.Variations)
.Where(v => productsInOrder.Contains(v.Id))
.Include(v => v.MetaProduct)
.ToArrayAsync(ct);
}
System.InvalidOperationException: The Include path
'MetaProduct->Variations' results in a cycle. Cycles are not allowed
in no-tracking queries; either use a tracking query or remove the
cycle.
public class MetaProduct
{
public Uuid Id { get; }
public IReadOnlyList<Variation> Variations => _variations.ToArray();
private List<Variation> _variations = null!;
}
public class Variation
{
public Uuid Id { get; }
public MetaProduct? MetaProduct { get; }
}
Relationship configuration:
private static void MapMetaProducts(ModelBuilder modelBuilder)
{
var tagsConverter = new ValueConverter<string[], string>(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<string[]>(v)
);
var builder = modelBuilder.Entity<MetaProduct>().ToTable("metaproducts");
builder.HasKey(p => p.Id);
builder.Property(p => p.Id).HasColumnName("Id");
builder.OwnsMany(mp => mp.Variations,
vBuilder =>
{
vBuilder.ToTable("metaproducts_variations");
vBuilder.WithOwner(v => v.MetaProduct!);
vBuilder.Property(v => v.Id).HasColumnName("Id");
vBuilder.HasKey("Id");
});
}
How to fix it?
It seems to me that all you're trying to do is return an array of Variations where the Variation Id is in the list.
Also, your variation should not have a navigation property back to MetaProduct when it is owned. It should really only be retrieved in the context of its owner.
If you really want to navigate from Variation to MetaProduct then you should reconsider whether Variation really is an 'owned' entity or just a related entity.
The problem is that you are entering the query from MetaProducts but then trying to Include it again. If you can do away with the navigation from Variation to MetaProduct then the following will work:
return await _clientCheckupsGatewayContext.MetaProducts
.AsNoTracking()
.SelectMany(mp => mp.Variations)
.Where(v => productsInOrder.Contains(v.Id))
.ToArrayAsync(ct);
If you really want to navigate the other way, then you'd need to promote Variation to a related entity (HasMany instead of OwnsMany), and then expose Variations as a DbSet on your context.
I have tables with foreign keys from main tables. I want that when I'm deleting an entry also want to be able to first remove all related entities from other tables.
I tried this
public void Delete<T>(T entity) where T : EntityBase
{
var relationManager = ((IObjectContextAdapter)m_context).ObjectContext.ObjectStateManager.GetRelationshipManager(entity);
var related = relationManager.GetAllRelatedEnds();
foreach (var relate in related)
{
// what to do here - ??
}
}
EntryBase is a base DBSet entity for all tables in DB contains UId as GUID type - the relationship is by this GUID
you can do it automatically using small configuration using FluentAPI in onModelCreating Method by add OnDelete(DeleteBehavior.Cascade) example as below
modelBuilder.Entity<YourEntity>(entity =>
{
entity.HasOne(d => d.Entity1)
.WithMany(p => p.YourEntity)
.HasForeignKey(d => d.Id)
.OnDelete(DeleteBehavior.Cascade) //This is the key to solve your problem
.HasConstraintName("FK_YourEntity_Entity1");
}
We have 3 tables in our db that each have an entity in our edmx. To illustrate my problem, imagine 3 tables:
Table: Make
Fields:
makeID
make
Table: Model
FIelds:
modelID
makeID foreign key
model
Table: Car
carID
modelID foreign key
car
Our Make, Model, and Car entity have all of the navigation properties in the entity model. Lazy loading is disabled. We want to be able to pull all cars that are Jeep Grand Cherokees to output to our page.
Right now we have something like this in one of our functions (C# 4.0)
IEnumerable<Make> makeList = (((ObjectSet<Lot>)_modelRepository.GetQuery())
.Include(mk => mk.Models.Where(md => md.model == "Grand Cherokee"))
.Where(mk => mk.make == "Jeep").ToList());
_makeRepository.GetQuery() returns an IQueryable ... we implement the repository pattern
This query should work fine (haven't tested it, created for ths example) but how can we .Include the car table so that our function returns Make entity objects such that the Model is populated and the Cars are populated (problem getting the Cars because they do not have a direct navigation property to Make)
We're using POCO objects.
The goal is to have 1 function returning a Make entity to be able to do this:
foreach(Make myMake in makeList)
{
Response.Write(myMake.make);
foreach(Model myModel in myMake.Models)
{
Response.Write(myModel.model);
foreach(Car mycar in myModel.Cars)
{
Response.Write(mycar.car);
}
}
}
Something like this doesn't seem possible but its what we're going for:
IEnumerable<Make> makeList = (((ObjectSet<Lot>)_modelRepository.GetQuery())
.Include(mk => mk.Models.Where(md => md.model == "Grand Cherokee"))
.Include(c => mk.Models.Cars)
.Where(mk => mk.make == "Jeep").ToList());
I've also tried creating a new entity in my edmx that contains all of this information so that I can just query that one object but I keep getting errors saying keys must be mapped... I did map them in the Mapping tab (Visual Studio 2010)... so I'm back to trying to get the query working.
I am not 100% sure but I believe you are going to need to create some kind of DTO like this:
public MakeModelCarDto
{
public IEnumerable<Make> Makes {get; set;}
public IEnumerable<Model> Models {get; set;}
public IEnumerable<Car> Cars {get; set;}
}
Then you are going to have to JOIN the tables like this:
_makeRepository.GetQuery()
.Join(_modelRepository.GetQuery(), mk => mk.makeid, mo => mo.makeid, (mk, mo) => new { mk, mo })
.Join(_carRepository.GetQuery(), #t => #t.mo.modelid, c => c.modelid, (#t, c) => new { #t, c })
.Where(#t => #t.#t.mk.make == "Jeep" && #t.#t.mo.model == "Grand Cherokee")
.Select(#t => new MakeModelCarDto
{
Makes = #t.#t.mk,
Model = #t.#t.mo,
Cars = #t.c
}).SingleOrDefault();