Skip to content

Commit

Permalink
Add DddDbContext
Browse files Browse the repository at this point in the history
  • Loading branch information
rungwiroon committed Jul 4, 2024
1 parent d40e0ac commit cdd6a03
Show file tree
Hide file tree
Showing 12 changed files with 581 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/Codehard.Common/Codehard.Common.DomainModel/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ protected Entity(Action<object, string> lazyLoader)
/// <summary>
/// Gets the unique identifier for the entity.
/// </summary>
public abstract TKey Id { get; protected init; }
public abstract TKey Id { get; init; }

/// <summary>
/// Gets a read-only collection of notifications associated with the entity.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using LanguageExt;

using static LanguageExt.Prelude;
Expand Down Expand Up @@ -163,6 +158,28 @@ public static Aff<Option<TSource>> SingleOrNoneAff<TSource>(
await source.SingleOrNoneAsync(predicate, ct));
}

/// <summary>
/// Asynchronously returns the only element of a sequence satisfying a specified condition within an Aff monad.
/// This method will returns fail if no such element exists or if more than one element satisfies the condition.
/// </summary>
/// <typeparam name="TSource">The type of the elements in the sequence.</typeparam>
/// <param name="source">The IQueryable&lt;TSource&gt; to get the single element from.</param>
/// <param name="predicate">A lambda expression representing the condition to satisfy.</param>
/// <param name="ct">The CancellationToken to observe while waiting for the task to complete.</param>
/// <returns>
/// An Aff&lt;TSource&gt; that represents the asynchronous operation.
/// The Aff monad wraps the result, which is the only element of the sequence satisfying the specified condition.
/// </returns>
public static Aff<TSource> SingleOrFailAff<TSource>(
this IQueryable<TSource> source,
Expression<Func<TSource, bool>> predicate,
CancellationToken ct = default)
{
return
Aff(async () =>
await source.SingleAsync(predicate, ct));
}

/// <summary>
/// Asynchronously returns the first element of a sequence as an Option&lt;T&gt; within a Task,
/// or a None value if the sequence is empty.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using Codehard.Infrastructure.EntityFramework.Tests.Entities.DomainDrivenDesign;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

namespace Codehard.Infrastructure.EntityFramework.Tests;

public class DddEntityTests
{
[Fact]
public async Task WhenSaveAggregateRoot_UsingDddDbContext_ThenSaveAllEntities()
{
// Arrange
var options = new DbContextOptionsBuilder<TestDddDbContext>()
.UseSqlite(CreateInMemoryDatabase())
.Options;

await using var context = new TestDddDbContext(options);

await context.Database.EnsureCreatedAsync();

var a = new DddA { Id = Guid.NewGuid() };
var b1 = new DddB { Id = Guid.NewGuid() };
var c1 = new DddC { Id = Guid.NewGuid() };

a.AddB(b1);
b1.AddC(c1);
context.As.Add(a);

await context.SaveChangesAsync();

context.ChangeTracker.Clear();

// Act
var savedA = await context.As
.Include(dddA => dddA.Bs)
.ThenInclude(dddB => dddB.Cs)
.SingleAsync(dddA => dddA.Id == a.Id);

// Assert
Assert.NotNull(savedA);
Assert.NotEmpty(savedA.Bs);
Assert.NotEmpty(savedA.Bs.First().Cs);
}

[Fact]
public async Task WhenAddNonAggregateRootDirectly_UsingDddDbContextSet_ThenExceptionIsThrown()
{
// Arrange
var options = new DbContextOptionsBuilder<TestDddDbContext>()
.UseSqlite(CreateInMemoryDatabase())
.Options;

await using var context = new TestDddDbContext(options);

await context.Database.EnsureCreatedAsync();

var b1 = new DddB { Id = Guid.NewGuid() };
var c1 = new DddC { Id = Guid.NewGuid() };

b1.AddC(c1);

InvalidOperationException? exception = null;

// Act
try
{
context.Set<DddB>().Add(b1);
}
catch (InvalidOperationException e)
{
exception = e;
}

// Assert
Assert.NotNull(exception);
}

[Fact]
public async Task WhenAddNonAggregateRootDirectly_UsingDddDbContext_ThenExceptionIsThrown()
{
// Arrange
var options = new DbContextOptionsBuilder<TestDddDbContext>()
.UseSqlite(CreateInMemoryDatabase())
.Options;

await using var context = new TestDddDbContext(options);

await context.Database.EnsureCreatedAsync();

var b1 = new DddB { Id = Guid.NewGuid() };
var c1 = new DddC { Id = Guid.NewGuid() };

b1.AddC(c1);

InvalidOperationException? exception = null;

// Act
try
{
context.Add(b1);
}
catch (InvalidOperationException e)
{
exception = e;
}

// Assert
Assert.NotNull(exception);
}

[Fact]
public async Task WhenAddNonAggregateRootDirectly_UsingDddDbContextGenericAsync_ThenExceptionIsThrown()
{
// Arrange
var options = new DbContextOptionsBuilder<TestDddDbContext>()
.UseSqlite(CreateInMemoryDatabase())
.Options;

await using var context = new TestDddDbContext(options);

await context.Database.EnsureCreatedAsync();

var b1 = new DddB { Id = Guid.NewGuid() };
var c1 = new DddC { Id = Guid.NewGuid() };

b1.AddC(c1);

InvalidOperationException? exception = null;

// Act
try
{
await context.AddAsync(b1);
}
catch (InvalidOperationException e)
{
exception = e;
}

// Assert
Assert.NotNull(exception);
}

[Fact]
public async Task WhenAddNonAggregateRootDirectly_UsingDddDbContextAsync_ThenExceptionIsThrown()
{
// Arrange
var options = new DbContextOptionsBuilder<TestDddDbContext>()
.UseSqlite(CreateInMemoryDatabase())
.Options;

await using var context = new TestDddDbContext(options);

await context.Database.EnsureCreatedAsync();

var b1 = new DddB { Id = Guid.NewGuid() };
var c1 = new DddC { Id = Guid.NewGuid() };

b1.AddC(c1);

InvalidOperationException? exception = null;

// Act
try
{
await context.AddAsync((object)b1);
}
catch (InvalidOperationException e)
{
exception = e;
}

// Assert
Assert.NotNull(exception);
}

private static SqliteConnection CreateInMemoryDatabase()
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ public async Task WhenSaveChangesAsync_UsingDomainEventDbContext_ShouldPublishAn

var loggerMock = new Mock<ILogger<TestDbContext>>();
var logger = loggerMock.Object;

await using var context = new TestDbContext(
options,
builder => builder.ApplyConfigurationsFromAssemblyFor<TestDbContext>(assembly),
logger);

await context.Database.EnsureCreatedAsync();

// Act
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Codehard.Common.DomainModel;

namespace Codehard.Infrastructure.EntityFramework.Tests.Entities.DomainDrivenDesign;

public class DddA : Entity<Guid>, IAggregateRoot<Guid>
{
public override Guid Id { get; init; }

private readonly List<DddB> bs = new();

public IReadOnlyCollection<DddB> Bs => bs.AsReadOnly();

public void AddB(DddB b)
{
bs.Add(b);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Codehard.Common.DomainModel;

namespace Codehard.Infrastructure.EntityFramework.Tests.Entities.DomainDrivenDesign;

public class DddB : Entity<Guid>
{
public override Guid Id { get; init; }

public DddA A { get; private set; } = null!;

private readonly List<DddC> cs = new();

public IReadOnlyCollection<DddC> Cs => cs.AsReadOnly();

public void AddC(DddC c)
{
cs.Add(c);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Codehard.Common.DomainModel;

namespace Codehard.Infrastructure.EntityFramework.Tests.Entities.DomainDrivenDesign;

public class DddC : Entity<Guid>
{
public override Guid Id { get; init; }

public DddB B { get; private set; } = null!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public struct EntityAKey

public class EntityA : Entity<EntityAKey>
{
public override EntityAKey Id { get; protected init; }
public override EntityAKey Id { get; init; }

public string Value { get; set; } = string.Empty;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Codehard.Infrastructure.EntityFramework.DbContexts;
using Codehard.Infrastructure.EntityFramework.Tests.Entities.DomainDrivenDesign;
using Microsoft.EntityFrameworkCore;

namespace Codehard.Infrastructure.EntityFramework.Tests;

public class TestDddDbContext : DddDbContext
{
public TestDddDbContext(DbContextOptions options) : base(options)
{
}

public DbSet<DddA> As { get; set; }
}
Loading

0 comments on commit cdd6a03

Please sign in to comment.