Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
<Project>
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<AnalysisLevel>latest-All</AnalysisLevel>
<!--

Keep NU1903 visible without failing the build while the vulnerable transitive
SQLitePCLRaw.lib.e_sqlite3 2.1.11 dependency is addressed in issue #1066
and PR #1067 (GHSA-2m69-gcr7-jv3q).

Remove this exclusion once PR #1067 is merged or the transitive dependency
is otherwise updated to a non-vulnerable version.

-->
<WarningsNotAsErrors>$(WarningsNotAsErrors);NU1903</WarningsNotAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand All @@ -11,4 +24,4 @@
<PropertyGroup>
<NoWarn>1591</NoWarn> <!-- Remove this to turn on warnings for missing XML Comments -->
</PropertyGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,31 @@

namespace Clean.Architecture.Core.ContributorAggregate.Handlers;

public class ContributorDeletedHandler(ILogger<ContributorDeletedHandler> logger,
public partial class ContributorDeletedHandler(
ILogger<ContributorDeletedHandler> logger,
IEmailSender emailSender) : INotificationHandler<ContributorDeletedEvent>
{
public async ValueTask Handle(ContributorDeletedEvent domainEvent, CancellationToken cancellationToken)
public async ValueTask Handle(
ContributorDeletedEvent domainEvent,
CancellationToken cancellationToken)
{
logger.LogInformation("Handling Contributed Deleted event for {contributorId}", domainEvent.ContributorId);
Guard.Against.Null(domainEvent);

await emailSender.SendEmailAsync("to@test.com",
"from@test.com",
"Contributor Deleted",
$"Contributor with id {domainEvent.ContributorId} was deleted.");
LogHandlingContributorDeleted(logger, domainEvent.ContributorId);

await emailSender.SendEmailAsync(
"to@test.com",
"from@test.com",
"Contributor Deleted",
$"Contributor with id {domainEvent.ContributorId} was deleted.")
.ConfigureAwait(false);
}

[LoggerMessage(
EventId = 1,
Level = LogLevel.Information,
Message = "Handling Contributor Deleted event for {ContributorId}")]
private static partial void LogHandlingContributorDeleted(
ILogger<ContributorDeletedHandler> logger,
ContributorId contributorId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,33 @@

namespace Clean.Architecture.Core.ContributorAggregate.Handlers;

public class ContributorNameUpdatedEmailNotificationHandler(
public partial class ContributorNameUpdatedEmailNotificationHandler(
ILogger<ContributorNameUpdatedEmailNotificationHandler> logger,
IEmailSender emailSender) : INotificationHandler<ContributorNameUpdatedEvent>
{
public async ValueTask Handle(ContributorNameUpdatedEvent domainEvent, CancellationToken cancellationToken)
public async ValueTask Handle(
ContributorNameUpdatedEvent domainEvent,
CancellationToken cancellationToken)
{
logger.LogInformation("Handling Contributor Name Updated event for {contributorId}", domainEvent.Contributor.Id);
Guard.Against.Null(domainEvent);

await emailSender.SendEmailAsync("to@test.com",
"from@test.com",
$"Contributor {domainEvent.Contributor.Id} Name Updated",
$"Contributor with id {domainEvent.Contributor.Id} had their name updated to {domainEvent.Contributor.Name}.");
LogHandlingContributorNameUpdated(
logger,
domainEvent.Contributor.Id);

await emailSender.SendEmailAsync(
"to@test.com",
"from@test.com",
$"Contributor {domainEvent.Contributor.Id} Name Updated",
$"Contributor with id {domainEvent.Contributor.Id} had their name updated to {domainEvent.Contributor.Name}.")
.ConfigureAwait(false);
}

[LoggerMessage(
EventId = 2,
Level = LogLevel.Information,
Message = "Handling Contributor Name Updated event for {ContributorId}")]
private static partial void LogHandlingContributorNameUpdated(
ILogger<ContributorNameUpdatedEmailNotificationHandler> logger,
ContributorId contributorId);
}
2 changes: 1 addition & 1 deletion src/Clean.Architecture.Core/Interfaces/IEmailSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

public interface IEmailSender
{
Task SendEmailAsync(string to, string from, string subject, string body);
Task SendEmailAsync(string recipient, string from, string subject, string body);
}
46 changes: 33 additions & 13 deletions src/Clean.Architecture.Core/Services/DeleteContributorService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Clean.Architecture.Core.ContributorAggregate;
using Clean.Architecture.Core.ContributorAggregate;
using Clean.Architecture.Core.ContributorAggregate.Events;
using Clean.Architecture.Core.Interfaces;

Expand All @@ -8,23 +8,43 @@ namespace Clean.Architecture.Core.Services;
/// This is here mainly so there's an example of a domain service
/// and also to demonstrate how to fire domain events from a service.
/// </summary>
/// <param name="_repository"></param>
/// <param name="_mediator"></param>
/// <param name="_logger"></param>
public class DeleteContributorService(IRepository<Contributor> _repository,
IMediator _mediator,
ILogger<DeleteContributorService> _logger) : IDeleteContributorService
public partial class DeleteContributorService(
IRepository<Contributor> repository,
IMediator mediator,
ILogger<DeleteContributorService> logger) : IDeleteContributorService
{
public async ValueTask<Result> DeleteContributor(ContributorId contributorId)
public async ValueTask<Result> DeleteContributor(
ContributorId contributorId)
{
_logger.LogInformation("Deleting Contributor {contributorId}", contributorId);
Contributor? aggregateToDelete = await _repository.GetByIdAsync(contributorId);
if (aggregateToDelete == null) return Result.NotFound();
LogDeletingContributor(logger, contributorId);

Contributor? aggregateToDelete = await repository
.GetByIdAsync(contributorId)
.ConfigureAwait(false);

if (aggregateToDelete is null)
{
return Result.NotFound();
}

await repository
.DeleteAsync(aggregateToDelete)
.ConfigureAwait(false);

await _repository.DeleteAsync(aggregateToDelete);
var domainEvent = new ContributorDeletedEvent(contributorId);
await _mediator.Publish(domainEvent);

await mediator
.Publish(domainEvent)
.ConfigureAwait(false);

return Result.Success();
}

[LoggerMessage(
EventId = 3,
Level = LogLevel.Information,
Message = "Deleting Contributor {ContributorId}")]
private static partial void LogDeletingContributor(
ILogger<DeleteContributorService> logger,
ContributorId contributorId);
}
2 changes: 2 additions & 0 deletions src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
Guard.Against.Null(modelBuilder);

base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public class ContributorConfiguration : IEntityTypeConfiguration<Contributor>
{
public void Configure(EntityTypeBuilder<Contributor> builder)
{
Guard.Against.Null(builder);

builder.Property(entity => entity.Id)
.ValueGeneratedOnAdd()
.HasVogenConversion()
Expand All @@ -18,9 +20,9 @@ public void Configure(EntityTypeBuilder<Contributor> builder)

builder.OwnsOne(builder => builder.PhoneNumber);

builder.Property(x => x.Status)
builder.Property(entity => entity.Status)
.HasConversion(
x => x.Value,
x => ContributorStatus.FromValue(x));
status => status.Value,
value => ContributorStatus.FromValue(value));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@

public static class DataSchemaConstants
{
#pragma warning disable CA1707 // Preserve the established public constant name.
public const int DEFAULT_NAME_LENGTH = 100;
#pragma warning restore CA1707
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using Clean.Architecture.Core.ContributorAggregate;
using Clean.Architecture.Core.ContributorAggregate;
using Vogen;

namespace Clean.Architecture.Infrastructure.Data.Config;

// This type is intentionally used as a marker for the Vogen source generator.
#pragma warning disable CA1812, CA1852
[EfCoreConverter<ContributorId>]
[EfCoreConverter<ContributorName>]
internal partial class VogenEfCoreConverters;
#pragma warning restore CA1812, CA1852
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,42 @@
namespace Clean.Architecture.Infrastructure.Data;

// Intercepts SaveChanges to dispatch domain events after changes are successfully saved
public class EventDispatchInterceptor(IDomainEventDispatcher domainEventDispatcher) : SaveChangesInterceptor
public class EventDispatchInterceptor(
IDomainEventDispatcher domainEventDispatcher)
: SaveChangesInterceptor
{
private readonly IDomainEventDispatcher _domainEventDispatcher = domainEventDispatcher;
private readonly IDomainEventDispatcher _domainEventDispatcher =
domainEventDispatcher;

// Called after SaveChangesAsync has completed successfully
public override async ValueTask<int> SavedChangesAsync(SaveChangesCompletedEventData eventData, int result,
CancellationToken cancellationToken = new CancellationToken())
public override async ValueTask<int> SavedChangesAsync(
SaveChangesCompletedEventData eventData,
int result,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(eventData);

var context = eventData.Context;

if (context is not AppDbContext appDbContext)
{
return await base.SavedChangesAsync(eventData, result, cancellationToken).ConfigureAwait(false);
return await base
.SavedChangesAsync(eventData, result, cancellationToken)
.ConfigureAwait(false);
}

// Retrieve all tracked entities that have domain events
var entitiesWithEvents = appDbContext.ChangeTracker.Entries<HasDomainEventsBase>()
.Select(e => e.Entity)
.Where(e => e.DomainEvents.Any())
var entitiesWithEvents = appDbContext.ChangeTracker
.Entries<HasDomainEventsBase>()
.Select(entry => entry.Entity)
.Where(entity => entity.DomainEvents.Count > 0)
.ToArray();

// Dispatch and clear domain events
await _domainEventDispatcher.DispatchAndClearEvents(entitiesWithEvents);

return await base.SavedChangesAsync(eventData, result, cancellationToken);
await _domainEventDispatcher
.DispatchAndClearEvents(entitiesWithEvents)
.ConfigureAwait(false);

return await base
.SavedChangesAsync(eventData, result, cancellationToken)
.ConfigureAwait(false);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public partial class PhoneNumber : Migration
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
Guard.Against.Null(migrationBuilder);

migrationBuilder.CreateTable(
name: "Contributors",
columns: table => new
Expand All @@ -31,6 +33,8 @@ protected override void Up(MigrationBuilder migrationBuilder)
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
Guard.Against.Null(migrationBuilder);

migrationBuilder.DropTable(
name: "Contributors");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public partial class UpdateForNet10 : Migration
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
Guard.Against.Null(migrationBuilder);

// Only alter columns if using SQL Server
// SQLite uses dynamic typing so these changes aren't necessary
if (migrationBuilder.ActiveProvider == "Microsoft.EntityFrameworkCore.SqlServer")
Expand Down Expand Up @@ -73,6 +75,8 @@ protected override void Up(MigrationBuilder migrationBuilder)
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
Guard.Against.Null(migrationBuilder);

if (migrationBuilder.ActiveProvider == "Microsoft.EntityFrameworkCore.SqlServer")
{
migrationBuilder.AlterColumn<int>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

Expand All @@ -10,6 +10,8 @@ public partial class UseDbGeneratedIds : Migration
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
Guard.Against.Null(migrationBuilder);

if (migrationBuilder.ActiveProvider == "Microsoft.EntityFrameworkCore.SqlServer")
{
// SQL Server requires drop/recreate to add IDENTITY to an existing column.
Expand Down Expand Up @@ -48,6 +50,8 @@ protected override void Up(MigrationBuilder migrationBuilder)
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
Guard.Against.Null(migrationBuilder);

if (migrationBuilder.ActiveProvider == "Microsoft.EntityFrameworkCore.SqlServer")
{
migrationBuilder.Sql("EXEC sp_rename N'[Contributors]', N'[Contributors_old]'");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ public ListContributorsQueryService(AppDbContext db)
.Take(perPage)
.Select(c => new ContributorDto(c.Id, c.Name, c.PhoneNumber ?? PhoneNumber.Unknown))
.AsNoTracking()
.ToListAsync();
.ToListAsync().ConfigureAwait(false);

int totalCount = await _db.Contributors.CountAsync();
int totalCount = await _db.Contributors.CountAsync().ConfigureAwait(false);
int totalPages = (int)Math.Ceiling(totalCount / (double)perPage);
var result = new UseCases.PagedResult<ContributorDto>(items, page, perPage, totalCount, totalPages);

Expand Down
Loading
Loading