#c# #sqlite #entity-framework-core #xunit #temporal-tables
#c# #sqlite #entity-framework-core #xunit #временные таблицы
Вопрос:
Мы используем временную таблицу с системной версией в нашем базовом приложении Entity Framework. Это работает очень хорошо, но у нас возникают проблемы при создании теста.
Я следовал этому руководству, используя базы данных SQLite в памяти для тестирования приложения EF Core от Microsoft.
https://docs.microsoft.com/en-us/ef/core/testing/sqlite#using-sqlite-in-memory-databases
Проблема в том, что это Sqlite
вызовет исключение для SysStartTime
. Это ожидаемо, поскольку свойство помечено как prop.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAddOrUpdate;
in DbContext
и обычно обрабатывается Sql Server. Есть ли способ заставить это работать в SQLite?
Исключение SQLiteException: ошибка 19 SQLite: «НЕ удалось выполнить ограничение NOT NULL: User.SysStartTime».
Пользователь:
public class User : IEntity
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime SysStartTime { get; set; }
public DateTime SysEndTime { get; set; }
[Required]
public string ExternalId { get; set; }
}
Тест xUnit:
public class QuestionUpdateTest: IDisposable
{
private readonly DbConnection _connection;
private readonly ApplicationDbContext _context = null;
public ChoiceSequencingQuestionUpdateTest()
{
var dbContextOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(CreateInMemoryDatabase())
.Options;
_connection = RelationalOptionsExtension.Extract(dbContextOptions).Connection;
_context = new ApplicationDbContext(dbContextOptions);
_context.User.Add(new User()
{
ExternalId = "1"
});
_context.SaveChangesNoUser();
}
private static DbConnection CreateInMemoryDatabase()
{
var connection = new SqliteConnection("Filename=:memory:");
connection.Open();
return connection;
}
public void Dispose() => _connection.Dispose();
[Fact]
public void Test2()
{
}
}
ApplicationDbContext:
public int SaveChangesNoUser()
{
//Wont help since the property is marked as ValueGenerated
foreach (var changedEntity in ChangeTracker.Entries())
{
if (changedEntity.Entity is IEntity entity)
{
switch (changedEntity.State)
{
case EntityState.Added:
entity.SysStartTime = DateTime.Now;
break;
}
}
}
return base.SaveChanges();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var property in modelBuilder.Model.GetEntityTypes()
.SelectMany(t => t.GetProperties())
.Where(p => p.ClrType == typeof(string)))
{
if (property.GetMaxLength() == null)
property.SetMaxLength(256);
}
foreach (var property in modelBuilder.Model.GetEntityTypes()
.SelectMany(t => t.GetProperties())
.Where(p => p.ClrType == typeof(DateTime)))
{
property.SetColumnType("datetime2(0)");
}
foreach (var et in modelBuilder.Model.GetEntityTypes())
{
foreach (var prop in et.GetProperties())
{
if (prop.Name == "SysStartTime" || prop.Name == "SysEndTime")
{
prop.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAddOrUpdate;
}
}
}
base.OnModelCreating(modelBuilder);
}
Миграция:
public partial class Temporaltablesforallentities : Migration
{
List<string> tablesToUpdate = new List<string>
{
"User",
};
protected override void Up(MigrationBuilder migrationBuilder)
{
foreach (var table in tablesToUpdate)
{
string alterStatement = $@"ALTER TABLE [{table}]
ADD PERIOD FOR SYSTEM_TIME ([SysStartTime], [SysEndTime])";
migrationBuilder.Sql(alterStatement);
alterStatement = $@"ALTER TABLE [{table}]
SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = History.[{table}], DATA_CONSISTENCY_CHECK = ON));";
migrationBuilder.Sql(alterStatement);
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
foreach (var table in tablesToUpdate)
{
string alterStatement = $@"ALTER TABLE [{table}] SET (SYSTEM_VERSIONING = OFF);";
migrationBuilder.Sql(alterStatement);
alterStatement = $@"ALTER TABLE [{table}] DROP PERIOD FOR SYSTEM_TIME";
migrationBuilder.Sql(alterStatement);
alterStatement = $@"DROP TABLE History.[{table}]";
migrationBuilder.Sql(alterStatement);
}
}
}
Комментарии:
1. Поддерживает ли SQLite временные таблицы?
2. Если вы проголосовали против, пожалуйста, скажите, почему!
3. @MitchWheat Нет, но мне не нужно тестировать эту функциональность, мне просто нужно значение для
SysStartTime
иSysEndTime
для предотвращения исключения.
Ответ №1:
Решил это так в protected override void OnModelCreating(ModelBuilder modelBuilder)
:
foreach (var et in modelBuilder.Model.GetEntityTypes())
{
foreach (var prop in et.GetProperties())
{
if (prop.Name == "SysStartTime" || prop.Name == "SysEndTime")
{
if (Database.IsSqlServer())
{
prop.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAddOrUpdate;
}
else
{
prop.SetDefaultValue(DateTime.Now);
}
}
}
}
Ответ №2:
Попробуйте удалить SysStartTime и SysEndTime из ваших моделей. Вы можете добавить их, используя следующие фрагменты:
Создайте Constants.cs или аналогичный:
public const string AddSystemVersioningFormatString = @"
ALTER TABLE [dbo].[{0}]
ADD SysStartTime datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
CONSTRAINT DF_{0}_SysStartTime DEFAULT SYSUTCDATETIME(),
SysEndTime datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL
CONSTRAINT DF_{0}_SysEndTime DEFAULT CONVERT(datetime2, '9999-12-31 23:59:59.9999999'),
PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime)
ALTER TABLE [dbo].[{0}]
SET (SYSTEM_VERSIONING = ON (
HISTORY_TABLE = [dbo].[{0}History],
DATA_CONSISTENCY_CHECK = ON )
)";
public const string RemoveSystemVersioningFormatString = @"
ALTER TABLE [dbo].[{0}] SET (SYSTEM_VERSIONING = OFF)
ALTER TABLE [dbo].[{0}] DROP PERIOD FOR SYSTEM_TIME
ALTER TABLE [dbo].[{0}] DROP CONSTRAINT DF_{0}_SysStartTime
ALTER TABLE [dbo].[{0}] DROP CONSTRAINT DF_{0}_SysEndTime
ALTER TABLE [dbo].[{0}] DROP COLUMN SysStartTime, SysEndTime
DROP TABLE IF EXISTS [dbo].[{0}History]
";
Затем в вашей миграции:
migrationBuilder.Sql(string.Format(Constants.AddSystemVersioningFormatString, "User"));
Таким образом, ваши модели не будут знать о дополнительных столбцах, и вам не придется явно задавать что-либо в EF, поскольку SQL Server обработает все настройки за вас.
Ответ №3:
Я решил это немного по-другому, сначала я добавил этот метод this method в свой контекстный класс db (в отдельный файл):
public void CheckIfUsingInMemoryDatabase(ModelBuilder modelBuilder)
{
if (!Database.IsSqlServer())
{
modelBuilder.Entity<MyTemporalTable1>(b =>
{
b.Property<DateTime>("SysStartTime");
b.Property<DateTime>("SysEndTime");
});
modelBuilder.Entity<MyTemporalTable2>(b =>
{
b.Property<DateTime>("SysStartTime");
b.Property<DateTime>("SysEndTime");
});
}
}
а затем я просто вызываю его в начале OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
CheckIfUsingInMemoryDatabase(modelBuilder);
...
Мне было немного неудобно, поскольку я создаю каркас базы данных, а не использую код — во-первых, таким образом мне нужно добавлять только одну строку в сгенерированный контекст после каждого повторного создания каркаса.