Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cross-Cutting Concerns and Apply Them to the Notes Module #37

Merged
merged 18 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from 11 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
10 changes: 2 additions & 8 deletions Backend/src/Modules/Notes/Notes.Api/Endpoints/CreateNote.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
using BuildingBlocks.Domain.ValueObjects.Ids;
using Carter;
using Mapster;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Notes.Application.Dtos;
using Notes.Application.Notes.Commands.CreateNote;
using Notes.Application.Entities.Notes.Commands.CreateNote;
using Notes.Application.Entities.Notes.Dtos;

namespace Notes.Api.Endpoints;

Expand Down
26 changes: 26 additions & 0 deletions Backend/src/Modules/Notes/Notes.Api/Endpoints/GetNotesById.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Text.Json.Serialization;
using Notes.Application.Entities.Notes.Queries.GetNoteById;

namespace Notes.Api.Endpoints;

public class GetNotesById : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/Notes/{noteId}", async (string noteId, ISender sender) =>
{
var result = await sender.Send(new GetNoteByIdQuery(noteId));
var response = result.Adapt<GetNoteByIdResponse>();

return Results.Ok(response);
})
.WithName("GetNoteById")
.Produces<GetNoteByIdResponse>()
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status404NotFound)
.WithSummary("Get Note by Id")
.WithDescription("Get Note by Id");
}
}

public record GetNoteByIdResponse([property: JsonPropertyName("note")] NoteDto NoteDto);
25 changes: 25 additions & 0 deletions Backend/src/Modules/Notes/Notes.Api/Endpoints/ListNotes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using BuildingBlocks.Application.Pagination;
using Notes.Application.Entities.Notes.Queries.ListNotes;

namespace Notes.Api.Endpoints;

public class ListNotes : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/Notes", async ([AsParameters] PaginationRequest query, ISender sender) =>
{
var result = await sender.Send(new ListNotesQuery(query));
var response = result.Adapt<ListNotesResponse>();

return Results.Ok(response);
})
.WithName("ListNotes")
.Produces<ListNotesResponse>()
.ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("List Notes")
.WithDescription("List Notes");
}
}

public record ListNotesResponse(PaginatedResult<NoteDto> Notes);
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static IServiceCollection AddNotesModule
services.AddApiServices();
services.AddApplicationServices();
services.AddInfrastructureServices(configuration);

return services;
}

Expand All @@ -27,7 +27,7 @@ private static IServiceCollection AddApiServices(this IServiceCollection service
public static IEndpointRouteBuilder UseNotesModule(this IEndpointRouteBuilder endpoints)
{
endpoints.MapGet("/Notes/Test", () => "Notes.Api Test -> Ok!");

return endpoints;
}
}
7 changes: 7 additions & 0 deletions Backend/src/Modules/Notes/Notes.Api/GlobalUsing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
global using Carter;
global using Mapster;
global using MediatR;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Http;
global using Microsoft.AspNetCore.Routing;
global using Notes.Application.Entities.Notes.Dtos;
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using BuildingBlocks.Domain.ValueObjects.Ids;
using FluentValidation;
using Notes.Application.Dtos;
using Notes.Application.Entities.Notes.Dtos;

namespace Notes.Application.Notes.Commands.CreateNote;
namespace Notes.Application.Entities.Notes.Commands.CreateNote;

public record CreateNoteCommand(NoteDto Note) : ICommand<CreateNoteResult>;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
using BuildingBlocks.Domain.ValueObjects.Ids;
using Notes.Application.Data;

namespace Notes.Application.Notes.Commands.CreateNote;
namespace Notes.Application.Entities.Notes.Commands.CreateNote;

public class CreateNoteHandler(INotesDbContext dbContext)
: ICommandHandler<CreateNoteCommand, CreateNoteResult>
{
public async Task<CreateNoteResult> Handle(CreateNoteCommand command, CancellationToken cancellationToken)
{
var note = Note.Create(
var note = command.ToNote();

dbContext.Notes.Add(note);
await dbContext.SaveChangesAsync(cancellationToken);

return new CreateNoteResult(note.Id);
}
}

internal static class CreateNoteCommandExtensions
{
public static Note ToNote(this CreateNoteCommand command)
{
return Note.Create(
NoteId.Of(Guid.NewGuid()),
command.Note.Title,
command.Note.Content,
command.Note.Timestamp,
command.Note.Importance
);

dbContext.Notes.Add(note);
await dbContext.SaveChangesAsync(cancellationToken);

return new CreateNoteResult(note.Id);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Notes.Application.Dtos;
namespace Notes.Application.Entities.Notes.Dtos;

public record NoteDto(
string Id,
string Title,
string Content,
DateTime Timestamp,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Notes.Application.Entities.Notes.Exceptions;

public class NoteNotFoundException(string id) : NotFoundException("Note", id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Notes.Application.Entities.Notes.Dtos;

namespace Notes.Application.Entities.Notes.Extensions;

public static class NoteExtensions
{
public static NoteDto ToNoteDto(this Note note)
{
return new NoteDto(
note.Id.ToString(),
note.Title,
note.Content,
note.Timestamp,
note.Importance);
}

public static IEnumerable<NoteDto> ToNodeDtoList(this IEnumerable<Note> notes)
{
return notes.Select(ToNoteDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Notes.Application.Entities.Notes.Exceptions;
using Notes.Application.Entities.Notes.Extensions;

namespace Notes.Application.Entities.Notes.Queries.GetNoteById;

internal class GetNoteByIdHandler(INotesDbContext dbContext) : IQueryHandler<GetNoteByIdQuery, GetNoteByIdResult>
{
public async Task<GetNoteByIdResult> Handle(GetNoteByIdQuery request, CancellationToken cancellationToken)
{
var note = await dbContext.Notes
.AsNoTracking()
.SingleOrDefaultAsync(n => n.Id == NoteId.Of(Guid.Parse(request.Id)), cancellationToken);

if (note is null)
throw new NoteNotFoundException(request.Id);

return new GetNoteByIdResult(note.ToNoteDto());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Notes.Application.Entities.Notes.Dtos;

namespace Notes.Application.Entities.Notes.Queries.GetNoteById;

public record GetNoteByIdQuery(string Id) : IQuery<GetNoteByIdResult>;

public record GetNoteByIdResult(NoteDto NoteDto);
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using BuildingBlocks.Application.Pagination;
using Notes.Application.Entities.Notes.Dtos;
using Notes.Application.Entities.Notes.Extensions;

namespace Notes.Application.Entities.Notes.Queries.ListNotes;

public class ListNotesHandler(INotesDbContext dbContext) : IQueryHandler<ListNotesQuery, ListNotesResult>
{
public async Task<ListNotesResult> Handle(ListNotesQuery query, CancellationToken cancellationToken)
{
var pageIndex = query.PaginationRequest.PageIndex;
var pageSize = query.PaginationRequest.PageSize;

var totalCount = await dbContext.Notes.LongCountAsync(cancellationToken);

var notes = await dbContext.Notes
.AsNoTracking()
.OrderBy(n => n.Timestamp)
.Skip(pageSize * pageIndex)
.Take(pageSize)
.ToListAsync(cancellationToken: cancellationToken);

return new ListNotesResult(
new PaginatedResult<NoteDto>(
pageIndex,
pageSize,
totalCount,
notes.ToNodeDtoList()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using BuildingBlocks.Application.Pagination;
using Notes.Application.Entities.Notes.Dtos;

namespace Notes.Application.Entities.Notes.Queries.ListNotes;

public record ListNotesQuery(PaginationRequest PaginationRequest) : IQuery<ListNotesResult>;

public record ListNotesResult(PaginatedResult<NoteDto> Notes);
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using BuildingBlocks.Application.Behaviors;
using Microsoft.Extensions.DependencyInjection;

namespace Notes.Application.Extensions;
Expand All @@ -10,6 +11,8 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
services.AddMediatR(config =>
{
config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
config.AddOpenBehavior(typeof(ValidationBehavior<,>));
config.AddOpenBehavior(typeof(LoggingBehavior<,>));
});

return services;
Expand Down
4 changes: 4 additions & 0 deletions Backend/src/Modules/Notes/Notes.Application/GlobalUsing.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
global using FluentValidation;
global using Microsoft.EntityFrameworkCore;
global using BuildingBlocks.Application.Cqrs;
global using BuildingBlocks.Application.Exceptions;
global using BuildingBlocks.Domain.ValueObjects.Ids;
global using Notes.Application.Data;
global using Notes.Domain.Models;
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace Notes.Infrastructure.Data.Interceptors;

public class AuditableEntityInterceptor : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
UpdateEntities(eventData.Context);
return base.SavingChanges(eventData, result);
}

public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData,
InterceptionResult<int> result, CancellationToken cancellationToken = default)
{
UpdateEntities(eventData.Context);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}

private static void UpdateEntities(DbContext? context)
{
if (context == null) return;

context.ChangeTracker.DetectChanges();

foreach (var entry in context.ChangeTracker.Entries<IEntity>())
{
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedBy = "username";
entry.Entity.CreatedAt = DateTime.UtcNow;
}

var isAdded = entry.State == EntityState.Added;
var isModified = entry.State == EntityState.Modified;

if (!isAdded && !isModified && !entry.HasChangedOwnedEntities())
continue;

entry.Entity.LastModifiedBy = "username";
entry.Entity.LastModifiedAt = DateTime.UtcNow;
}
}
}

public static class Extensions
{
public static bool HasChangedOwnedEntities(this EntityEntry entry)
{
return entry.References.Any(r =>
r.TargetEntry != null &&
r.TargetEntry.Metadata.IsOwned() &&
r.TargetEntry.State is EntityState.Added or EntityState.Modified);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using MediatR;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace Notes.Infrastructure.Data.Interceptors;

public class DispatchDomainEventsInterceptor(IMediator mediator) : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
DispatchDomainEvents(eventData.Context).GetAwaiter().GetResult();
return base.SavingChanges(eventData, result);
}

public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData,
InterceptionResult<int> result, CancellationToken cancellationToken = default)
{
await DispatchDomainEvents(eventData.Context);
return await base.SavingChangesAsync(eventData, result, cancellationToken);
}

private async Task DispatchDomainEvents(DbContext? context)
{
if (context == null) return;

var aggregates = context.ChangeTracker
.Entries<IAggregate>()
.Where(a => a.Entity.DomainEvents.Any())
.Select(a => a.Entity);

var aggregatesList = aggregates.ToList();

var domainEvents = aggregatesList
.SelectMany(a => a.DomainEvents)
.ToList();

aggregatesList.ForEach(a => a.ClearDomainEvents());

foreach (var domainEvent in domainEvents)
await mediator.Publish(domainEvent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Configuration;
using Notes.Application.Data;
using Notes.Infrastructure.Data;
using Notes.Infrastructure.Data.Interceptors;

namespace Notes.Infrastructure;

Expand All @@ -12,6 +13,9 @@ public static IServiceCollection AddInfrastructureServices
{
var connectionString = configuration.GetConnectionString("DefaultConnection");

services.AddScoped<ISaveChangesInterceptor, AuditableEntityInterceptor>();
services.AddScoped<ISaveChangesInterceptor, DispatchDomainEventsInterceptor>();

// Add Note-specific DbContext
services.AddDbContext<NotesDbContext>((serviceProvider, options) =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
global using Microsoft.EntityFrameworkCore;
global using Microsoft.Extensions.DependencyInjection;
global using BuildingBlocks.Domain.Abstractions;
global using BuildingBlocks.Domain.ValueObjects.Ids;
global using Notes.Domain.Models;
Loading