Skip to content

Instantly share code, notes, and snippets.

@chrisfcarroll
Last active November 7, 2023 12:39
Show Gist options
  • Save chrisfcarroll/bb86372734eb2baec8ec2d6cc84cd552 to your computer and use it in GitHub Desktop.
Save chrisfcarroll/bb86372734eb2baec8ec2d6cc84cd552 to your computer and use it in GitHub Desktop.
Minimal EFCore Demo for Domain-Application-Infrastructure DDD structure
using System.Diagnostics;
using EFCoreAndDomainModels.Application;
using EFCoreAndDomainModels.Domain;
using EFCoreAndDomainModels.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using TestBase;
using Xunit.Abstractions;
namespace EFCoreAndDomainModels
{
namespace Domain
{
public record ApiResource
{
public bool Enabled { get; set; } = true;
public string Name { get; set; }
public string ClientId { get; set; }
public IEnumerable<HandledScope> HandledScopes { get; set; } = Array.Empty<HandledScope>();
}
public record HandledScope(string ScopeName)
{
public static implicit operator HandledScope(string str) => new HandledScope(str);
public static implicit operator string(HandledScope scope) => scope.ScopeName;
}
}
namespace Application
{
public class QueryApiResources
{
readonly ApplicationDb database;
public IList<ApiResource> GetForClient(string clientId)
=> database.ApiResources.Where(r => r.ClientId == clientId).ToList();
public QueryApiResources(ApplicationDb database) => this.database = database;
}
public interface ApplicationDb
{
public DbSet<ApiResource> ApiResources { get; set; }
}
}
namespace Infrastructure
{
public class SqliteApplicationDb : DbContext, ApplicationDb
{
public SqliteApplicationDb(DbContextOptionsBuilder<SqliteApplicationDb> optionsBuilderOptions)
: base(optionsBuilderOptions.Options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ApiResource>().HasKey(r => r.Name);
modelBuilder.Entity<HandledScope>().HasKey(r => r.ScopeName);
}
public DbSet<ApiResource> ApiResources { get; set; }
}
public class SqliteApplicationDbFactory : IDesignTimeDbContextFactory<SqliteApplicationDb>
{
public SqliteApplicationDb CreateDbContext(params string[] args)
{
Debug.Assert(args?.Length>0 && args[0]?.Length>0, "Required argument: filepath for the sqlite db.");
var path = args[0];
try { Path.GetFullPath(path);} catch(Exception){throw new ArgumentOutOfRangeException("Not a valid path for a file: "+args[0].Substring(0,999));}
//
var optionsBuilder = new DbContextOptionsBuilder<SqliteApplicationDb>();
optionsBuilder.UseSqlite($"Data Source={path}");
return new SqliteApplicationDb(optionsBuilder);
}
}
public record Config
{
public string DbPath { get; set; }
public static Config ForTest = new Config
{
DbPath = "./IntegrationTest.db"
};
}
}
namespace Tests
{
public class IntegrationTestQueryApiResources
{
readonly ITestOutputHelper _out;
readonly ApiResource FakeApiResource = new ApiResource
{
Enabled = true,
ClientId = "testid",
HandledScopes = new HandledScope[] { "testscope1" },
Name = "testresource"
};
Config TestConfig = Config.ForTest;
SqliteApplicationDb testDb;
[Fact]
public void QueryApiResources_CanGetByClientId()
{
//Arrange
GivenEmptyTestDb();
GivenApiResourceInDb(FakeApiResource);
//Act & Assert
var uut = new QueryApiResources(testDb);
var result= uut.GetForClient(FakeApiResource.ClientId)
.ShouldBeOfLength(1)
.First()
.ShouldEqualByValue(FakeApiResource);
//Debug
_out.WriteLine(result.ToString());
}
void GivenApiResourceInDb(ApiResource resource)
{
testDb.ApiResources.Add(resource);
testDb.SaveChanges();
}
void GivenEmptyTestDb()
{
if (File.Exists(TestConfig.DbPath)) { File.Delete(TestConfig.DbPath); }
testDb = new SqliteApplicationDbFactory().CreateDbContext(TestConfig.DbPath);
testDb.Database.GetMigrations()
.ShouldNotBeEmpty(@"
You must FIRST create at least one database migration from the commandline with for example:
dotnet ef migrations add InitialCreate --context SqliteApplicationDb -- ./IntegrationTest.db
Inspect the Db creation sql script with
dotnet ef migrations script --context SqliteApplicationDb -- ./IntegrationTest.db > sqlitedbscript.sql
See https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli#create-the-database"
);
testDb.Database.Migrate();
}
public IntegrationTestQueryApiResources(ITestOutputHelper @out) => _out = @out;
}
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" />
<PackageReference Include="TestBase" Version="4.1.4.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
#! /usr/bin/env bash
dotnet ef migrations add InitialCreate --context SqliteApplicationDb -- ./IntegrationTest.db
dotnet ef migrations script --context SqliteApplicationDb -- ./IntegrationTest.db > sqlitedbscript.sql
using System;
using System.IO;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace BlazingPizza.Specs;
public class SqliteDbDesignTimeFactory<T> : IDesignTimeDbContextFactory<T> where T : DbContext
{
public T CreateDbContext(params string[] args)
{
return args.Length switch
{
0 => CreateDbContext(),
_ => CreateDbContext(args[0]),
};
}
static T CreateDbContext(string path = DefaultTestDbPath)
{
try { Path.GetFullPath(path); }
catch (Exception)
{
throw new ArgumentOutOfRangeException(
"Not a valid path for a file: " + path.Substring(0, 999));
}
//
var optionsBuilder = new DbContextOptionsBuilder<T>();
optionsBuilder.UseSqlite($"Data Source={path}");
return Activator.CreateInstance(typeof(T), optionsBuilder.Options) as T ??
throw new TargetInvocationException(
$"Activator.CreateInstance( {typeof(T).FullName}, {optionsBuilder.Options})",
null);
}
public const string DefaultTestDbPath = "./IntegrationTest.db";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment