#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 есть идея получше, вы можете поделиться ею здесь.