最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c# - Entity Framework Core tries to create existing object - Stack Overflow

programmeradmin1浏览0评论

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?

EDIT2: I've created a small sample repository which shows my issue. I don't understand how I should "attach" the object to the saving DbContext correctly. See test case UpdateExistingRole

发布评论

评论列表(0)

  1. 暂无评论