Skip to content

Latest commit

 

History

History
322 lines (242 loc) · 9.55 KB

ef-usage.md

File metadata and controls

322 lines (242 loc) · 9.55 KB

EntityFramework Core Usage

Interactions with SqlLocalDB via Entity Framework Core.

EfLocalDb package NuGet Status

https://nuget.org/packages/EfLocalDb/

Schema and data

The snippets use a DbContext of the following form:

using Microsoft.EntityFrameworkCore;

public class TheDbContext(DbContextOptions options) :
    DbContext(options)
{
    public DbSet<TheEntity> TestEntities { get; set; } = null!;

    protected override void OnModelCreating(ModelBuilder model) => model.Entity<TheEntity>();
}

snippet source | anchor

public class TheEntity
{
    public int Id { get; set; }
    public string? Property { get; set; }
}

snippet source | anchor

Initialize SqlInstance

SqlInstance needs to be initialized once.

To ensure this happens only once there are several approaches that can be used:

Static constructor

In the static constructor of a test.

If all tests that need to use the SqlInstance existing in the same test class, then the SqlInstance can be initialized in the static constructor of that test class.

public class Tests
{
    static SqlInstance<TheDbContext> sqlInstance;

    static Tests() =>
        sqlInstance = new(
            builder => new(builder.Options));

    public async Task Test()
    {
        var entity = new TheEntity
        {
            Property = "prop"
        };
        var data = new List<object>
        {
            entity
        };
        await using var database = await sqlInstance.Build(data);
        Assert.Single(database.Context.TestEntities);
    }
}

snippet source | anchor

Static constructor in test base

If multiple tests need to use the SqlInstance, then the SqlInstance should be initialized in the static constructor of test base class.

public abstract class TestBase
{
    static SqlInstance<TheDbContext> sqlInstance;

    static TestBase() =>
        sqlInstance = new(
            constructInstance: builder => new(builder.Options));

    public Task<SqlDatabase<TheDbContext>> LocalDb(
        [CallerFilePath] string testFile = "",
        string? databaseSuffix = null,
        [CallerMemberName] string memberName = "") =>
        sqlInstance.Build(testFile, databaseSuffix, memberName);
}

public class Tests :
    TestBase
{
    [Fact]
    public async Task Test()
    {
        await using var database = await LocalDb();
        var entity = new TheEntity
        {
            Property = "prop"
        };
        await database.AddData(entity);

        Assert.Single(database.Context.TestEntities);
    }
}

snippet source | anchor

SqlServerDbContextOptionsBuilder

Some SqlServer options are exposed by passing a Action<SqlServerDbContextOptionsBuilder> to the SqlServerDbContextOptionsExtensions.UseSqlServer. In this project the UseSqlServer is handled internally, so the SqlServerDbContextOptionsBuilder functionality is achieved by passing a action to the SqlInstance.

var sqlInstance = new SqlInstance<MyDbContext>(
    constructInstance: builder => new(builder.Options),
    sqlOptionsBuilder: sqlBuilder => sqlBuilder.EnableRetryOnFailure(5));

snippet source | anchor

Seeding data in the template

Data can be seeded into the template database for use across all tests:

public class BuildTemplate
{
    static SqlInstance<BuildTemplateDbContext> sqlInstance;

    static BuildTemplate() =>
        sqlInstance = new(
            constructInstance: builder => new(builder.Options),
            buildTemplate: async context =>
            {
                await context.Database.EnsureCreatedAsync();
                var entity = new TheEntity
                {
                    Property = "prop"
                };
                context.Add(entity);
                await context.SaveChangesAsync();
            });

    [Fact]
    public async Task BuildTemplateTest()
    {
        await using var database = await sqlInstance.Build();

        Assert.Single(database.Context.TestEntities);
    }
}

snippet source | anchor

Usage in a Test

Usage inside a test consists of two parts:

Build a SqlDatabase

await using var database = await sqlInstance.Build();

snippet source | anchor

See: Database Name Resolution

Using DbContexts

await using (var data = database.NewDbContext())
{

snippet source | anchor

Full Test

The above are combined in a full test:

using EfLocalDb;

public class EfSnippetTests
{
    static SqlInstance<MyDbContext> sqlInstance;

    static EfSnippetTests() =>
        sqlInstance = new(
            builder => new(builder.Options));


    [Fact]
    public async Task TheTest()
    {

        await using var database = await sqlInstance.Build();



        await using (var data = database.NewDbContext())
        {


            var entity = new TheEntity
            {
                Property = "prop"
            };
            data.Add(entity);
            await data.SaveChangesAsync();
        }

        await using (var data = database.NewDbContext())
        {
            Assert.Single(data.TestEntities);
        }

    }

    [Fact]
    public async Task TheTestWithDbName()
    {

        await using var database = await sqlInstance.Build("TheTestWithDbName");


        var entity = new TheEntity
        {
            Property = "prop"
        };
        await database.AddData(entity);

        Assert.Single(database.Context.TestEntities);
    }
}

snippet source | anchor

EntityFramework DefaultOptionsBuilder

When building a DbContextOptionsBuilder the default configuration is as follows:

static class DefaultOptionsBuilder
{
    static LogCommandInterceptor interceptor = new();

    public static DbContextOptionsBuilder<TDbContext> Build<TDbContext>()
        where TDbContext : DbContext
    {
        var builder = new DbContextOptionsBuilder<TDbContext>();
        if (LocalDbLogging.SqlLoggingEnabled)
        {
            builder.AddInterceptors(interceptor);
        }

        builder.EnableSensitiveDataLogging();
        builder.EnableDetailedErrors();
        return builder;
    }

    public static void ApplyQueryTracking<T>(this DbContextOptionsBuilder<T> builder, QueryTrackingBehavior? tracking)
        where T : DbContext
    {
        if (tracking.HasValue)
        {
            builder.UseQueryTrackingBehavior(tracking.Value);
        }
    }
}

snippet source | anchor