Невозможно отследить объект, поскольку свойство альтернативного ключа равно нулю

#c# #asp.net-core #entity-framework-core

#c# #asp.net-core #entity-framework-core

Вопрос:

Я уже целую вечность пытаюсь заставить это работать, но безуспешно.

Архитектура: приложение WPF добавляет, обновляет, получает и удаляет объекты в веб-приложении Azure (ASP.NET Базовый ReST API с JWT) База данных находится только в веб-приложении и создана с помощью Entity Framework Core.

Проблема

Когда я впервые добавляю объект «инцидент», он работает отлично. Даже обновления с «первым раундом» работают без проблем. Но если я закрою приложение WPF и попытаюсь обновить, оно не работает и выдает исключение, ничего не работает, что бы я ни пытался изменить в коде.

Невозможно отследить объект типа ‘Инцидент’, поскольку свойство альтернативного ключа ‘UniqueID’ равно нулю. Если альтернативный ключ не используется в отношениях, рассмотрите возможность использования вместо него уникального индекса. Уникальные индексы могут содержать нули, а альтернативные ключи — нет.

UniqueID НЕ является идентификатором и будет использоваться как внешний ключ для отчетов, которые могут отображаться, а могут и не отображаться. Но наверняка и подтверждено, что UniqueID НИКОГДА не равен нулю. Я понятия не имею, почему он продолжает мне это говорить.

Есть какие-нибудь идеи?

Инцидент

 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);
    }
}
 

Сообщить

 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);
    }
}
 

Метод «Update»

 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;
    }
 

Комментарии:

1. После еще одного дня рыбалки я подозреваю, что это может быть какое-то поведение, основанное на соглашении об именах. Даже пометка «.ValueGeneratedNever()» не помогает. Возможно, ядро EF по-разному обрабатывает объекты (свойства), имена которых заканчиваются на «Id». Похоже, что EF Core просто игнорирует предоставленное строковое значение и пытается сначала установить значение null.

2. Еще одно наблюдение: после устранения этой паршивой ошибки появилось еще одно странное поведение. После нескольких раундов «обновления», «контекст. Инциденты. Обновление (инцидент)» начинает добавлять пустые объекты вместо обновления. Возможно, это и есть причина первоначальной ошибки «null». Итак, почему ядро EF неожиданно добавляет «нулевые» записи вместо обновления того, что должно было обновляться?

3. На самом деле не так много терпения осталось с каркасом объекта, и я близок к тому, чтобы реорганизовать все обратно к традиционному подходу доступа к базе данных…

4. Автоматическое поведение EF действительно разочаровывает. Это намного больше проблем, чем того стоит.

Ответ №1:

В конце концов я пришел к следующему решению: «контекст.Инциденты.Обновление (инцидент);» по-прежнему НЕ будет работать и, вероятно, никогда не будет. Поэтому я изменил его на

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

Больше никаких записей-призраков и нулевых ошибок, но свойства навигации теперь исчезли или не распознаны.

Как это решить? Переключение из автоматического режима в более ручной режим. Я внедрил свойства внешнего ключа с возможностью обнуления в модель данных инцидента.

 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); } }
 

Изменения в конфигурации типа объекта (Инцидент)

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

Изменения в конфигурации типа объекта (Клиент)

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

Изменения в конфигурации типа объекта (Сторонник)

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

Наконец, с помощью этих реализованных вручную и назначенных внешних ключей с возможностью обнуления свойства навигации имеют исключенные значения при чтении из базы данных.

Но у меня все еще есть плохое предчувствие, что это всего лишь обходной путь и что-то не так, как должно быть. Если у anonye есть идея получше, вы можете поделиться ею здесь.