I'm currently learning EF Core and I came across some strange behaviour which I don't understand.
I have a Blazor component which inherits from a base class EditPageBase:
public abstract class EditPageBase<TModel, TIdentifier> : ActivePageBase{ protected EditForm? _form; protected bool _showOptionsMenu; [Parameter, SupplyParameterFromQuery(Name = "Id")] public TIdentifier? Identifier { get; set; } public TModel? Input { get; set; } protected HSTrendDbContext DbContext { get; set; } = default!; protected SemaphoreSlim DbLock { get; } = new(1, 1); protected override async Task OnInitializedAsync() { DbContext = await DbFactory.CreateDbContextAsync(); } protected override async Task OnParametersSetAsync() { await DbLock.WaitAsync(); Input = await LoadEditModeAsync(); DbLock.Release(); if (Input is not null) { await CheckActivePageAsync(); } } protected async Task<bool> SaveAsync() { if (_form is null || _form.EditContext is null || Input is null) { return false; } if (!_form.EditContext.Validate()) { return false; } try { await DbContext.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { throw; } Log.Information("Saved: {url}; Identifier: {identifier}; User: {user}", NavigationManager.Uri, Identifier?.ToString() ?? "<NEU>", CurrentUser?.DisplayName ?? "<UNBEKANNT>"); ToastService.ShowSuccess("Datensatz wurde erfolgreich gespeichert"); await OnParametersSetAsync(); return true; } protected abstract Task<TModel?> LoadEditModeAsync(); public override async ValueTask DisposeAsync() { await base.DisposeAsync(); DbLock.Dispose(); DbContext.Dispose(); }}Now I have a class Recipient which looks like this:
public class Recipient{ public int RecipientId { get; set; } public string Name { get; set; } = string.Empty; public RecipientCategory Category { get; set; } public bool SendEmail { get; set; } public bool UseEmail { get; set; } public string Email { get; set; } = string.Empty; public bool IsActive { get; set; } public IEnumerable<User> Users { get; set; } = new List<User>(); // DO NOT REFACTOR}The class User looks like this:
public class User{ public int UserId { get; set; } public string Username { get; set; } = string.Empty; public Guid? ActiveDirectoryGuid { get; set; } public string DisplayName { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; public string Salt { get; set; } = string.Empty; public string Origin { get; set; } = string.Empty; public string DmsName { get; set; } = string.Empty; public bool IsFieldSales { get; set; } public bool IsActive { get; set; } public bool IsActiveDirectoryAccount => Origin == "ad"; public string? AgentNumber { get; set; } public decimal ApprovalLimit { get; set; } public User? Supervisor { get; set; } public List<UserRole> Roles { get; set; } = []; public IEnumerable<Recipient> Recipients { get; set; } = new List<Recipient>(); // DO NOT REFACTOR public List<ResponsibleEntity> ResponsibleEntities { get; set; } = [];}The EditPageBase inherits from another base class which provides the following two methods:
protected virtual async Task OnSearchCostCentersAsync(OptionsSearchEventArgs<CostCenter> e){ try { var token = InitializeCancellationToken(); using var dbContext = await DbFactory.CreateDbContextAsync(token); CostCenterFilter filter = new CostCenterFilter() { SearchPhrase = e.Text, }; var results = await dbContext.GetCostCentersAsync(filter, token); e.Items = results.Items; } catch (OperationCanceledException) { }}protected virtual async Task OnSearchUsersAsync(OptionsSearchEventArgs<User> e){ try { var token = InitializeCancellationToken(); using var dbContext = await DbFactory.CreateDbContextAsync(token); UserFilter filter = new UserFilter { SearchPhrase = e.Text }; var results = await dbContext.GetUsersAsync(filter, token); e.Items = results.Items; } catch (OperationCanceledException) { }}Those are being used to search within the database for users and cost centers. When I use this methods within my Razor component then it tries to create a new row for every user which I assign to my Recipient object. However it doesn't do the same thing for the cost centers which I use in another class.
public class MainArea{ public int MainAreaId { get; set; } public string Name { get; set; } = string.Empty; public IEnumerable<CostCenter> CostCenters { get; set; } = new List<CostCenter>(); // DO NOT REFACTOR}I've created custom configurations for my classes which look like this:
public class MainAreaMapping : IEntityTypeConfiguration<MainArea>{ public void Configure(EntityTypeBuilder<MainArea> builder) { builder.ToTable("InvestmentMainAreas"); builder.Property(x => x.Name) .HasMaxLength(50); builder.HasMany(x => x.CostCenters) .WithMany(x => x.MainAreas) .UsingEntity<MainAreaCostCenter> ( j => j .HasOne(d => d.CostCenter) .WithMany() .HasForeignKey(d => d.CostCenterId) .HasConstraintName("FK_InvestmentMainAreaToCostCenter_CostCenterId"), j => j .HasOne(d => d.MainArea) .WithMany() .HasForeignKey(d => d.MainAreaId) .HasConstraintName("FK_InvestmentMainAreaToCostCenter_MainAreaId"), j => { j.HasKey(t => new { t.MainAreaId, t.CostCenterId }); j.ToTable("InvestmentMainAreaToCostCenter"); } ); }}public class RecipientMapping : IEntityTypeConfiguration<Recipient>{ public void Configure(EntityTypeBuilder<Recipient> builder) { builder.ToTable("Recipients"); builder.Property(x => x.Name) .HasMaxLength(50); builder.Property(x => x.Email) .HasMaxLength(255); builder.HasMany(x => x.Users) .WithMany(x => x.Recipients) .UsingEntity<RecipientUser> ( j => j .HasOne(d => d.User) .WithMany() .HasForeignKey(d => d.UserId) .HasConstraintName("FK_RecipientUsers_UserId"), j => j .HasOne(d => d.Recipient) .WithMany() .HasForeignKey(d => d.RecipientId) .HasConstraintName("FK_RecipientUsers_RecipientId"), j => { j.HasKey(t => new { t.UserId, t.RecipientId }); j.ToTable("RecipientUsers"); } ); }}My question now is: why does Entity Framework Core try to insert all users again into the database and why doesn't it try the same for the cost centers? I don't want to create the users twice. I just want to assign them just like I do with the cost centers.
I understand that the problem exists because I load the classes form different DbContext instances but I don't get why the classes behave differently although the look identical to me and they both use the same base class.
EDIT: when I run this code
foreach (var user in recipient.Users){ DbContext.Entry(user).State = EntityState.Unchanged;}then the entries won't be created but the assignment will be properly made in the database. But why do I need to do this? And why do I not need to do this for my CostCenters?