Feature Request: Seed EF Core InMemory provider
Closed this issue · 9 comments
For unit/integration tests, it would be cool if the DbInitializer
could directly seed to the EF Core InMemory provider.
https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory
Hi, yes i'm fully agree with you.
But I think, that we can add Sqlite db support faster then EF core support.
Is it OK for you to use in-memory sqlite db?
For projects that use an SQL Server database, I'd prefer to use the EF Core InMemory provider. I have created some unit test base class that reads the JSON data files I've created with DbTool and saves it into the InMemory dbcontext. This works nice and easy. I past my code here, since I don't have it in any public repository yet:
Base class:
public class InMemoryDatabaseTests
{
protected string DbSeedDataPath => Path.Combine("App_Data", "DbSeedData");
protected DbContextOptions<ApplicationDbContext> CreateInMemoryDatabaseOptions(
[CallerMemberName] string memberName = null)
{
// https://stackoverflow.com/a/54220067/54159
var methodName = this.GetMethodName(memberName);
var guid = Guid.NewGuid().ToString(); // guid required to have consistent testing results in NCrunch
var databaseName = $"{methodName}-{guid}";
var dbContextOptionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName)
.EnableSensitiveDataLogging(true);
return dbContextOptionsBuilder.Options;
}
protected void SeedFromDbToolExportData(IAppDbContext context, SeedEntityData seedEntityData)
{
context.Set<Facility>().AddRange(seedEntityData.Facilities);
context.Set<FacilityState>().AddRange(seedEntityData.FacilityStates);
context.Set<FacilityCategory>().AddRange(seedEntityData.FacilityCategories);
context.Set<FacilityDisplayItem>().AddRange(seedEntityData.FacilityDisplayItems);
}
protected SeedEntityData ReadJsonSeedEntityData()
{
var result = new SeedEntityData();
var dbSeedDataPath = Path.Combine("App_Data", "DbSeedData");
result.Facilities = ReadDbUtilImportEntities<Facility>(Path.Combine(dbSeedDataPath, "Facilities.json"));
result.FacilityStates =
ReadDbUtilImportEntities<FacilityState>(Path.Combine(dbSeedDataPath, "FacilityStates.json"));
result.FacilityCategories =
ReadDbUtilImportEntities<FacilityCategory>(Path.Combine(dbSeedDataPath, "FacilityCategories.json"));
result.FacilityDisplayItems =
ReadDbUtilImportEntities<FacilityDisplayItem>(Path.Combine(dbSeedDataPath,
"FacilityDisplayItems.json"));
return result;
}
public List<TEntity> ReadDbUtilImportEntities<TEntity>(string filename) where TEntity : class
{
var fileContents = File.ReadAllText(filename);
// https: //www.newtonsoft.com/json/help/html/SerializingJSONFragments.htm
JObject facilitiesJson = JObject.Parse(fileContents);
var dataPart = facilitiesJson["data"].Children().ToList();
var entities = new List<TEntity>();
foreach (var jsonEntity in dataPart)
{
var entity = jsonEntity.ToObject<TEntity>();
entities.Add(entity);
}
return entities;
}
protected async Task<SeedEntityData> SeedWithAllJsonEntityData(DbContextOptions<ApplicationDbContext> options)
{
var seedEntityData = ReadJsonSeedEntityData();
using IAppDbContext context = new ApplicationDbContext(options);
SeedFromDbToolExportData(context, seedEntityData);
await context.SaveChangesAsync(CancellationToken.None);
return seedEntityData; // navigation properties are now valid!
}
}
// Entities
public class SeedEntityData
{
public List<FacilityCategory> FacilityCategories { get; set; }
public List<Facility> Facilities { get; set; }
public List<FacilityState> FacilityStates { get; set; }
public List<FacilityDisplayItem> FacilityDisplayItems { get; set; }
}
Some test:
public class GetFacilityDisplayItemsListHandlerTests : InMemoryDatabaseTests
{
[Fact]
public async Task Get_from_database_Facility_Display_Items_list()
{
// Arrange
var options = CreateInMemoryDatabaseOptions();
var seedEntityData = await SeedWithAllJsonEntityData(options);
List<FacilityDisplayItem> results;
using (IAppDbContext context = new ApplicationDbContext(options))
{
var facilityDisplayItemsRepository =
new FacilityDisplayItemsRepository(context, new NullLogger<FacilityDisplayItemsRepository>());
var sut =
new GetFacilityDisplayItemsListHandler(facilityDisplayItemsRepository);
// Act
results = await sut.Handle(new GetFacilityDisplayItemsListQuery(false), CancellationToken.None);
}
// Assert
results.Should().NotBeNullOrEmpty();
results.Should().BeEquivalentTo(seedEntityData.FacilityDisplayItems);
}
}
But of course, I have nothing against Sqlite db support, I have project where I use Sqlite Db as well 😉.
I forgot that we have unfinished efcore package. I think it will be easy to rewrite it for InMemory provider. So, in version 1.4 we will add support for either SqLite or EFCore InMemory dbs
We have published the updated packages on NuGet (version 1.4.0). You can find the description of the new features in "Testing scenario..." section in README
Greate, thanks for that!
I started testing this, but had a bit of a rough start getting it to work:
After cloning the Korzh.DbUtils project and analyzing the differences to my setup, I've found out that in order for the seeding to work (with Korzh.DbUtils.EntityFrameworkCore.InMemory
), I had to extend my OnModelCreating
method and add the .ToTable
mappings:
public class ApplicationDbContext : DbContext
{
// ctors here
public DbSet<Facility> Facilities { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.HasDefaultSchema("FacilityStates");
// next line only required for Korzh.DbUtils.EntityFrameworkCore.InMemory to work:
builder.Entity<Facility>()
.ToTable(nameof(Facilities));
Assembly assemblyWithConfigurations = typeof(Facility).Assembly;
builder.ApplyConfigurationsFromAssembly(assemblyWithConfigurations);
}
}
Without the .ToTable, the Seed method finds my .json file(s), but does not populate the tables then.
I've also tried the data annotations table attribute
[Table("Facilities")]
, but this didn't work either (although in my understanding it should have the same effect as .ToTable (https://www.learnentityframeworkcore.com/configuration/data-annotation-attributes/table-attribute)
Maybe the documentation should point that out. Or maybe the Seed method could detect this on it's own? After all, the table name is contained within the .json file.
Hi. The problem is that InMemory db uses different default names. If I'm not mistaken it uses class names instead of DbSet property names. That's why I've also added ToTable everywhere. Table attribute doesn't work with InMemory db. Possibly it is a reason to raise an issue in ef core repository. You can rename tour test .json files as a temporary solution. Or you can inherit your dbcontext for testing and call ToTable code there.
Actually, we use GetTableName extension method to get the mapped table name for some entity. So, I guess this function does not work correctly if there is no ToTable
defined for some entity.
So, we need to raise an issue about that as well (in EF Core repository) .
Hi,
we have created an issue dotnet/efcore#19751