Last active
November 7, 2023 12:39
-
-
Save chrisfcarroll/bb86372734eb2baec8ec2d6cc84cd552 to your computer and use it in GitHub Desktop.
Minimal EFCore Demo for Domain-Application-Infrastructure DDD structure
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env bash | |
dotnet ef migrations add InitialCreate --context SqliteApplicationDb -- ./IntegrationTest.db | |
dotnet ef migrations script --context SqliteApplicationDb -- ./IntegrationTest.db > sqlitedbscript.sql |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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