KorzhCom/Korzh.DbUtils

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

Here it is #21.
Possibly, we will publish the update tomorrow.

korzh commented

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.

korzh commented

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