Unable to track an entity because alternate key property is null

I am already trying forever to get this working, but no success.

Architecture: WPF Application adds, updates, receives and deletes entities on an Azure WebApp (ASP.NET Core ReST API with JWT) The Database is only at the WebApp and made with Entity Framework Core.

Problem

When I first add an ‘incident’ entity it does work perfectly. Even updates with the “first round” work seamless. But if I close the WPF App and try to update, it doesnt work and throws exeption, nothing works whatever I try to modify in code.

Unable to track an entity of type ‘Incident’ because alternate key property ‘UniqueId’ is null. If the alternate key is not used in a relationship, then consider using a unique index instead. Unique indexes may contain nulls, while alternate keys may not.

UniqueId is NOT the ID and will be used a foreign key for reports who may or may not show up. But for sure and confirmed, UniqueId is NEVER null. I have no idea why it does keep telling me that.

Any Ideas?

Incident

internal class IncidentConfiguration : IEntityTypeConfiguration<Incident>
{
    internal static IncidentConfiguration Create() => new();
    public void Configure(EntityTypeBuilder<Incident> builder)
    {
        builder
            .Property(incident => incident.Id)
            .ValueGeneratedOnAdd();
        builder
            .Property(incident => incident.RowVersion)
            .IsConcurrencyToken()
            .ValueGeneratedOnAddOrUpdate();
        builder
            .Property(incident => incident.UniqueId)
            .HasField("_uniqueId")
            .IsRequired();
        builder
            .Property(incident => incident.Completion)
            .HasField("_completion");
        builder
            .Property(incident => incident.Status)
            .HasField("_status");
        builder
            .Property(incident => incident.Estimated)
            .HasField("_estimated")
            .HasConversion(new TimeSpanToTicksConverter());
        builder
            .Property(incident => incident.Actual)
            .HasField("_actual")
            .HasConversion(new TimeSpanToTicksConverter());
        builder
            .Property(incident => incident.Closed)
            .HasField("_closed");
        builder
            .Property(incident => incident.Comments)
            .HasField("_comments");
        builder
            .Property(incident => incident.Opened)
            .HasField("_opened");
        builder
            .Property(incident => incident.Updated)
            .HasField("_updated");
        builder
            .Property(incident => incident.BriefDescripion)
            .HasField("_briefDescripion");
        builder
            .Property(incident => incident.Project)
            .HasField("_project");
        builder
            .Ignore(incident => incident.IsUpdated);
    }
}

Report

internal class ReportConfiguration : IEntityTypeConfiguration<Report>
{
    internal static ReportConfiguration Create() => new();
    public void Configure(EntityTypeBuilder<Report> builder)
    {
        builder
            .Property(report => report.Id)
            .ValueGeneratedOnAdd();
        builder
            .Property(report => report.RowVersion)
            .IsConcurrencyToken()
            .ValueGeneratedOnAddOrUpdate();
        builder
            .HasOne(report => report.Incident)
            .WithMany(incident => incident.Reports)
            .HasForeignKey(report => report.UniqueId)
            .HasPrincipalKey(incident => incident.UniqueId)
            .OnDelete(DeleteBehavior.Cascade);
        builder
            .Ignore(report => report.IsUpdated);
    }
}

The “Update” Method

public async Task<bool> UpdateAsync(Common.Models.Incident incident)
    {
        _manualResetEvent.WaitOne();
        try
        {
            using var context = new IncidentManagerContext(_connectionString);                
            context.Incidents.Update(incident);
            bool saveFailed;
            do
            {
                saveFailed = false;
                try
                {
                    await context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    saveFailed = true;
                    var entry = ex.Entries.Single();
                    entry.OriginalValues.SetValues(entry.GetDatabaseValues());
                }

            } while (saveFailed);
        }
        catch (Exception) { return false; }
        finally { _manualResetEvent.Set(); }
        return true;
    }

Answer

Finally I came up with the following solution: “context.Incidents.Update(incident);” is still NOT going to work, and probably won’t ever. So I changed it to

context.Entry(await context.Incidents.FirstOrDefaultAsync(x => x.Id == incident.Id)).CurrentValues.SetValues(incident);

No more ghost-entries neither null errors, but the navigation properties are now gone or not recognized.

How to solve this? Switching from auto-mode to more manual mode. I implemented nullable foreign key properties to the incident data model.

private int? _supporterId, _customerId;

/// <summary>
/// The supporter's foreign key
/// </summary>
[JsonPropertyName("supporterId")]
public int? SupporterId { get { return _supporterId; } set { SetProperty(ref _supporterId, value); } }

/// <summary>
/// The customer's foreign key
/// </summary>
[JsonPropertyName("customerId")]
public int? CustomerId { get { return _customerId; } set { SetProperty(ref _customerId, value); } }

Changes in entity type configuration (Incident)

builder
    .Property(incident => incident.SupporterId)
    .HasField("_supporterId");
builder
    .Property(incident => incident.CustomerId)
    .HasField("_customerId");

Changes in entity type configuration (Customer)

builder
    .HasMany(customer => customer.Incidents)
    .WithOne(incident => incident.Customer)
    .HasForeignKey(incident => incident.CustomerId)
    .OnDelete(DeleteBehavior.NoAction);

Changes in entity type configuration (Supporter)

builder
    .HasMany(supporter => supporter.Incidents)
    .WithOne(incident => incident.Supporter)
    .HasForeignKey(incident => incident.SupporterId)
    .OnDelete(DeleteBehavior.NoAction);

Finally with these manually implemented and assigned nullable foreign keys the navigation properties have there excpected values upon reading from database.

But I still have the bad feeling that this is just a workaround and something not as it supposed to be. If anonye has a better idea, you are welcome to share it here.

Leave a Reply

Your email address will not be published. Required fields are marked *