Style: C# 12 Primary constructor

This commit is contained in:
Kamigen
2024-03-30 20:59:17 -04:00
parent 786c65410d
commit 8f58311283
50 changed files with 267 additions and 449 deletions

View File

@@ -5,19 +5,12 @@ using Hutopy.Application.Common.Security;
namespace Hutopy.Application.Common.Behaviours;
public class AuthorizationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
public class AuthorizationBehaviour<TRequest, TResponse>(
IUser user,
IIdentityService identityService)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IUser _user;
private readonly IIdentityService _identityService;
public AuthorizationBehaviour(
IUser user,
IIdentityService identityService)
{
_user = user;
_identityService = identityService;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
var authorizeAttributes = request.GetType().GetCustomAttributes<AuthorizeAttribute>();
@@ -25,7 +18,7 @@ public class AuthorizationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRe
if (authorizeAttributes.Any())
{
// Must be authenticated user
if (_user.Id == null)
if (user.Id == null)
{
throw new UnauthorizedAccessException();
}
@@ -41,7 +34,7 @@ public class AuthorizationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRe
{
foreach (var role in roles)
{
var isInRole = await _identityService.IsInRoleAsync(_user.Id, role.Trim());
var isInRole = await identityService.IsInRoleAsync(user.Id, role.Trim());
if (isInRole)
{
authorized = true;
@@ -63,7 +56,7 @@ public class AuthorizationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRe
{
foreach (var policy in authorizeAttributesWithPolicies.Select(a => a.Policy))
{
var authorized = await _identityService.AuthorizeAsync(_user.Id, policy);
var authorized = await identityService.AuthorizeAsync(user.Id, policy);
if (!authorized)
{

View File

@@ -4,28 +4,24 @@ using Microsoft.Extensions.Logging;
namespace Hutopy.Application.Common.Behaviours;
public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest> where TRequest : notnull
public class LoggingBehaviour<TRequest>(
ILogger<TRequest> logger,
IUser user,
IIdentityService identityService)
: IRequestPreProcessor<TRequest>
where TRequest : notnull
{
private readonly ILogger _logger;
private readonly IUser _user;
private readonly IIdentityService _identityService;
public LoggingBehaviour(ILogger<TRequest> logger, IUser user, IIdentityService identityService)
{
_logger = logger;
_user = user;
_identityService = identityService;
}
private readonly ILogger _logger = logger;
public async Task Process(TRequest request, CancellationToken cancellationToken)
{
var requestName = typeof(TRequest).Name;
var userId = _user.Id ?? string.Empty;
var userId = user.Id ?? string.Empty;
string? userName = string.Empty;
if (!string.IsNullOrEmpty(userId))
{
userName = await _identityService.GetUserNameAsync(userId);
userName = await identityService.GetUserNameAsync(userId);
}
_logger.LogInformation("Hutopy Request: {Name} {@UserId} {@UserName} {@Request}",

View File

@@ -4,24 +4,14 @@ using Microsoft.Extensions.Logging;
namespace Hutopy.Application.Common.Behaviours;
public class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
public class PerformanceBehaviour<TRequest, TResponse>(
ILogger<TRequest> logger,
IUser user,
IIdentityService identityService)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly Stopwatch _timer;
private readonly ILogger<TRequest> _logger;
private readonly IUser _user;
private readonly IIdentityService _identityService;
public PerformanceBehaviour(
ILogger<TRequest> logger,
IUser user,
IIdentityService identityService)
{
_timer = new Stopwatch();
_logger = logger;
_user = user;
_identityService = identityService;
}
private readonly Stopwatch _timer = new();
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
@@ -33,20 +23,16 @@ public class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequ
var elapsedMilliseconds = _timer.ElapsedMilliseconds;
if (elapsedMilliseconds > 500)
{
var requestName = typeof(TRequest).Name;
var userId = _user.Id ?? string.Empty;
var userName = string.Empty;
if (elapsedMilliseconds <= 500) return response;
if (!string.IsNullOrEmpty(userId))
{
userName = await _identityService.GetUserNameAsync(userId);
}
var requestName = typeof(TRequest).Name;
var userId = user.Id ?? string.Empty;
var userName = string.Empty;
_logger.LogWarning("Hutopy Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@UserName} {@Request}",
requestName, elapsedMilliseconds, userId, userName, request);
}
if (!string.IsNullOrEmpty(userId)) userName = await identityService.GetUserNameAsync(userId);
logger.LogWarning("Hutopy Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@UserName} {@Request}",
requestName, elapsedMilliseconds, userId, userName, request);
return response;
}

View File

@@ -2,15 +2,11 @@
namespace Hutopy.Application.Common.Behaviours;
public class UnhandledExceptionBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
public class UnhandledExceptionBehaviour<TRequest, TResponse>(
ILogger<TRequest> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly ILogger<TRequest> _logger;
public UnhandledExceptionBehaviour(ILogger<TRequest> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
try
@@ -21,7 +17,7 @@ public class UnhandledExceptionBehaviour<TRequest, TResponse> : IPipelineBehavio
{
var requestName = typeof(TRequest).Name;
_logger.LogError(ex, "Hutopy Request: Unhandled Exception for Request {Name} {@Request}", requestName, request);
logger.LogError(ex, "Hutopy Request: Unhandled Exception for Request {Name} {@Request}", requestName, request);
throw;
}

View File

@@ -2,34 +2,27 @@
namespace Hutopy.Application.Common.Behaviours;
public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
public class ValidationBehaviour<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
if (!validators.Any()) return await next();
var validationResults = await Task.WhenAll(
_validators.Select(v =>
v.ValidateAsync(context, cancellationToken)));
var context = new ValidationContext<TRequest>(request);
var failures = validationResults
.Where(r => r.Errors.Any())
.SelectMany(r => r.Errors)
.ToList();
var validationResults = await Task.WhenAll(
validators.Select(v =>
v.ValidateAsync(context, cancellationToken)));
if (failures.Any())
throw new ValidationException(failures);
}
var failures = validationResults
.Where(r => r.Errors.Any())
.SelectMany(r => r.Errors)
.ToList();
if (failures.Count != 0) throw new ValidationException(failures);
return await next();
}
}

View File

@@ -2,14 +2,9 @@
namespace Hutopy.Application.Common.Exceptions;
public class ValidationException : Exception
public class ValidationException()
: Exception("One or more validation failures have occurred.")
{
public ValidationException()
: base("One or more validation failures have occurred.")
{
Errors = new Dictionary<string, string[]>();
}
public ValidationException(IEnumerable<ValidationFailure> failures)
: this()
{
@@ -18,5 +13,5 @@ public class ValidationException : Exception
.ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
}
public IDictionary<string, string[]> Errors { get; }
public IDictionary<string, string[]> Errors { get; } = new Dictionary<string, string[]>();
}

View File

@@ -1,19 +1,15 @@
namespace Hutopy.Application.Common.Models;
public class PaginatedList<T>
public class PaginatedList<T>(
IReadOnlyCollection<T> items,
int count,
int pageNumber,
int pageSize)
{
public IReadOnlyCollection<T> Items { get; }
public int PageNumber { get; }
public int TotalPages { get; }
public int TotalCount { get; }
public PaginatedList(IReadOnlyCollection<T> items, int count, int pageNumber, int pageSize)
{
PageNumber = pageNumber;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
TotalCount = count;
Items = items;
}
public IReadOnlyCollection<T> Items { get; } = items;
public int PageNumber { get; } = pageNumber;
public int TotalPages { get; } = (int)Math.Ceiling(count / (double)pageSize);
public int TotalCount { get; } = count;
public bool HasPreviousPage => PageNumber > 1;

View File

@@ -1,16 +1,12 @@
namespace Hutopy.Application.Common.Models;
public class Result
public class Result(
bool succeeded,
IEnumerable<string> errors)
{
internal Result(bool succeeded, IEnumerable<string> errors)
{
Succeeded = succeeded;
Errors = errors.ToArray();
}
public bool Succeeded { get; init; } = succeeded;
public bool Succeeded { get; init; }
public string[] Errors { get; init; }
public string[] Errors { get; init; } = errors.ToArray();
public static Result Success()
{

View File

@@ -3,7 +3,7 @@
/// <summary>
/// Specifies the class this attribute is applied to requires authorization.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class AuthorizeAttribute : Attribute
{
/// <summary>

View File

@@ -1,6 +1,7 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Extensions.DependencyInjection;
namespace Hutopy.Application;
public static class DependencyInjection
{

View File

@@ -3,7 +3,7 @@ using Hutopy.Domain.Entities;
namespace Hutopy.Application.FutureCreators.Commands;
public record CreateFutureCreatorCommand : IRequest<int>
public abstract record CreateFutureCreatorCommand : IRequest<int>
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
@@ -13,16 +13,10 @@ public record CreateFutureCreatorCommand : IRequest<int>
public required string ReasonToJoin { get; init; }
}
public class CreateFuturCreatorCommandHandler : IRequestHandler<CreateFutureCreatorCommand, int>
public class CreateFutureCreatorCommandHandler(
IApplicationDbContext context)
: IRequestHandler<CreateFutureCreatorCommand, int>
{
private readonly IApplicationDbContext _context;
public CreateFuturCreatorCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<int> Handle(CreateFutureCreatorCommand request, CancellationToken cancellationToken)
{
var entity = new FutureCreator
@@ -35,9 +29,9 @@ public class CreateFuturCreatorCommandHandler : IRequestHandler<CreateFutureCrea
ReasonToJoin = request.ReasonToJoin,
};
_context.FutureCreators.Add(entity);
context.FutureCreators.Add(entity);
await _context.SaveChangesAsync(cancellationToken);
await context.SaveChangesAsync(cancellationToken);
return entity.Id;
}

View File

@@ -2,7 +2,7 @@
namespace Hutopy.Application.Stripe.Commands;
public record CreateSessionCheckoutCommand : IRequest<string>
public abstract record CreateSessionCheckoutCommand : IRequest<string>
{
public required int Price { get; init; }
@@ -10,21 +10,16 @@ public record CreateSessionCheckoutCommand : IRequest<string>
}
public class CreateSessionCheckoutCommandHandler : IRequestHandler<CreateSessionCheckoutCommand, string>
public class CreateSessionCheckoutCommandHandler(
IApplicationDbContext context,
IStripeService stripeService)
: IRequestHandler<CreateSessionCheckoutCommand, string>
{
private readonly IApplicationDbContext _context;
private readonly IStripeService _stripeService;
public CreateSessionCheckoutCommandHandler(IApplicationDbContext context, IStripeService stripeService)
{
_context = context;
_stripeService = stripeService;
}
private readonly IApplicationDbContext _context = context;
public async Task<string> Handle(CreateSessionCheckoutCommand request, CancellationToken cancellationToken)
{
var stripeSecret = await _stripeService.CreateCheckoutSession(request.Price, request.Currency);
var stripeSecret = await stripeService.CreateCheckoutSession(request.Price, request.Currency);
return stripeSecret;
}

View File

@@ -11,15 +11,10 @@ public record CreateTodoItemCommand : IRequest<int>
public string? Title { get; init; }
}
public class CreateTodoItemCommandHandler : IRequestHandler<CreateTodoItemCommand, int>
public class CreateTodoItemCommandHandler(
IApplicationDbContext context)
: IRequestHandler<CreateTodoItemCommand, int>
{
private readonly IApplicationDbContext _context;
public CreateTodoItemCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<int> Handle(CreateTodoItemCommand request, CancellationToken cancellationToken)
{
var entity = new TodoItem
@@ -31,9 +26,9 @@ public class CreateTodoItemCommandHandler : IRequestHandler<CreateTodoItemComman
entity.AddDomainEvent(new TodoItemCreatedEvent(entity));
_context.TodoItems.Add(entity);
context.TodoItems.Add(entity);
await _context.SaveChangesAsync(cancellationToken);
await context.SaveChangesAsync(cancellationToken);
return entity.Id;
}

View File

@@ -9,27 +9,22 @@ namespace Hutopy.Application.TodoItems.Commands.DeleteTodoItem;
[Authorize(Policy = Policies.CanDelete)]
public record DeleteTodoItemCommand(int Id) : IRequest;
public class DeleteTodoItemCommandHandler : IRequestHandler<DeleteTodoItemCommand>
public class DeleteTodoItemCommandHandler(
IApplicationDbContext context)
: IRequestHandler<DeleteTodoItemCommand>
{
private readonly IApplicationDbContext _context;
public DeleteTodoItemCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task Handle(DeleteTodoItemCommand request, CancellationToken cancellationToken)
{
var entity = await _context.TodoItems
var entity = await context.TodoItems
.FindAsync(new object[] { request.Id }, cancellationToken);
Guard.Against.NotFound(request.Id, entity);
_context.TodoItems.Remove(entity);
context.TodoItems.Remove(entity);
entity.AddDomainEvent(new TodoItemDeletedEvent(entity));
await _context.SaveChangesAsync(cancellationToken);
await context.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -11,18 +11,13 @@ public record UpdateTodoItemCommand : IRequest
public bool Done { get; init; }
}
public class UpdateTodoItemCommandHandler : IRequestHandler<UpdateTodoItemCommand>
public class UpdateTodoItemCommandHandler(
IApplicationDbContext context)
: IRequestHandler<UpdateTodoItemCommand>
{
private readonly IApplicationDbContext _context;
public UpdateTodoItemCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task Handle(UpdateTodoItemCommand request, CancellationToken cancellationToken)
{
var entity = await _context.TodoItems
var entity = await context.TodoItems
.FindAsync(new object[] { request.Id }, cancellationToken);
Guard.Against.NotFound(request.Id, entity);
@@ -30,6 +25,6 @@ public class UpdateTodoItemCommandHandler : IRequestHandler<UpdateTodoItemComman
entity.Title = request.Title;
entity.Done = request.Done;
await _context.SaveChangesAsync(cancellationToken);
await context.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -14,18 +14,13 @@ public record UpdateTodoItemDetailCommand : IRequest
public string? Note { get; init; }
}
public class UpdateTodoItemDetailCommandHandler : IRequestHandler<UpdateTodoItemDetailCommand>
public class UpdateTodoItemDetailCommandHandler(
IApplicationDbContext context)
: IRequestHandler<UpdateTodoItemDetailCommand>
{
private readonly IApplicationDbContext _context;
public UpdateTodoItemDetailCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task Handle(UpdateTodoItemDetailCommand request, CancellationToken cancellationToken)
{
var entity = await _context.TodoItems
var entity = await context.TodoItems
.FindAsync(new object[] { request.Id }, cancellationToken);
Guard.Against.NotFound(request.Id, entity);
@@ -34,6 +29,6 @@ public class UpdateTodoItemDetailCommandHandler : IRequestHandler<UpdateTodoItem
entity.Priority = request.Priority;
entity.Note = request.Note;
await _context.SaveChangesAsync(cancellationToken);
await context.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -3,18 +3,13 @@ using Microsoft.Extensions.Logging;
namespace Hutopy.Application.TodoItems.EventHandlers;
public class TodoItemCompletedEventHandler : INotificationHandler<TodoItemCompletedEvent>
public class TodoItemCompletedEventHandler(
ILogger<TodoItemCompletedEventHandler> logger)
: INotificationHandler<TodoItemCompletedEvent>
{
private readonly ILogger<TodoItemCompletedEventHandler> _logger;
public TodoItemCompletedEventHandler(ILogger<TodoItemCompletedEventHandler> logger)
{
_logger = logger;
}
public Task Handle(TodoItemCompletedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Hutopy Domain Event: {DomainEvent}", notification.GetType().Name);
logger.LogInformation("Hutopy Domain Event: {DomainEvent}", notification.GetType().Name);
return Task.CompletedTask;
}

View File

@@ -3,18 +3,13 @@ using Microsoft.Extensions.Logging;
namespace Hutopy.Application.TodoItems.EventHandlers;
public class TodoItemCreatedEventHandler : INotificationHandler<TodoItemCreatedEvent>
public class TodoItemCreatedEventHandler(
ILogger<TodoItemCreatedEventHandler> logger)
: INotificationHandler<TodoItemCreatedEvent>
{
private readonly ILogger<TodoItemCreatedEventHandler> _logger;
public TodoItemCreatedEventHandler(ILogger<TodoItemCreatedEventHandler> logger)
{
_logger = logger;
}
public Task Handle(TodoItemCreatedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Hutopy Domain Event: {DomainEvent}", notification.GetType().Name);
logger.LogInformation("Hutopy Domain Event: {DomainEvent}", notification.GetType().Name);
return Task.CompletedTask;
}

View File

@@ -4,31 +4,25 @@ using Hutopy.Application.Common.Models;
namespace Hutopy.Application.TodoItems.Queries.GetTodoItemsWithPagination;
public record GetTodoItemsWithPaginationQuery : IRequest<PaginatedList<TodoItemBriefDto>>
public abstract record GetTodoItemsWithPaginationQuery : IRequest<PaginatedList<TodoItemBriefDto>>
{
public int ListId { get; init; }
public int PageNumber { get; init; } = 1;
public int PageSize { get; init; } = 10;
}
public class GetTodoItemsWithPaginationQueryHandler : IRequestHandler<GetTodoItemsWithPaginationQuery, PaginatedList<TodoItemBriefDto>>
public class GetTodoItemsWithPaginationQueryHandler(
IApplicationDbContext context,
IMapper mapper)
: IRequestHandler<GetTodoItemsWithPaginationQuery, PaginatedList<TodoItemBriefDto>>
{
private readonly IApplicationDbContext _context;
private readonly IMapper _mapper;
public GetTodoItemsWithPaginationQueryHandler(IApplicationDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<PaginatedList<TodoItemBriefDto>> Handle(GetTodoItemsWithPaginationQuery request, CancellationToken cancellationToken)
{
Console.WriteLine(request);
return await _context.TodoItems
return await context.TodoItems
.Where(x => x.ListId == request.ListId)
.OrderBy(x => x.Title)
.ProjectTo<TodoItemBriefDto>(_mapper.ConfigurationProvider)
.ProjectTo<TodoItemBriefDto>(mapper.ConfigurationProvider)
.PaginatedListAsync(request.PageNumber, request.PageSize);
}
}

View File

@@ -8,24 +8,17 @@ public record CreateTodoListCommand : IRequest<int>
public string? Title { get; init; }
}
public class CreateTodoListCommandHandler : IRequestHandler<CreateTodoListCommand, int>
public class CreateTodoListCommandHandler(
IApplicationDbContext context)
: IRequestHandler<CreateTodoListCommand, int>
{
private readonly IApplicationDbContext _context;
public CreateTodoListCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<int> Handle(CreateTodoListCommand request, CancellationToken cancellationToken)
{
var entity = new TodoList();
var entity = new TodoList { Title = request.Title };
entity.Title = request.Title;
context.TodoLists.Add(entity);
_context.TodoLists.Add(entity);
await _context.SaveChangesAsync(cancellationToken);
await context.SaveChangesAsync(cancellationToken);
return entity.Id;
}

View File

@@ -4,25 +4,20 @@ namespace Hutopy.Application.TodoLists.Commands.DeleteTodoList;
public record DeleteTodoListCommand(int Id) : IRequest;
public class DeleteTodoListCommandHandler : IRequestHandler<DeleteTodoListCommand>
public class DeleteTodoListCommandHandler(
IApplicationDbContext context)
: IRequestHandler<DeleteTodoListCommand>
{
private readonly IApplicationDbContext _context;
public DeleteTodoListCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task Handle(DeleteTodoListCommand request, CancellationToken cancellationToken)
{
var entity = await _context.TodoLists
var entity = await context.TodoLists
.Where(l => l.Id == request.Id)
.SingleOrDefaultAsync(cancellationToken);
Guard.Against.NotFound(request.Id, entity);
_context.TodoLists.Remove(entity);
context.TodoLists.Remove(entity);
await _context.SaveChangesAsync(cancellationToken);
await context.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -8,19 +8,14 @@ namespace Hutopy.Application.TodoLists.Commands.PurgeTodoLists;
[Authorize(Policy = Policies.CanPurge)]
public record PurgeTodoListsCommand : IRequest;
public class PurgeTodoListsCommandHandler : IRequestHandler<PurgeTodoListsCommand>
public class PurgeTodoListsCommandHandler(
IApplicationDbContext context)
: IRequestHandler<PurgeTodoListsCommand>
{
private readonly IApplicationDbContext _context;
public PurgeTodoListsCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task Handle(PurgeTodoListsCommand request, CancellationToken cancellationToken)
{
_context.TodoLists.RemoveRange(_context.TodoLists);
context.TodoLists.RemoveRange(context.TodoLists);
await _context.SaveChangesAsync(cancellationToken);
await context.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -9,25 +9,20 @@ public record UpdateTodoListCommand : IRequest
public string? Title { get; init; }
}
public class UpdateTodoListCommandHandler : IRequestHandler<UpdateTodoListCommand>
public class UpdateTodoListCommandHandler(
IApplicationDbContext context)
: IRequestHandler<UpdateTodoListCommand>
{
private readonly IApplicationDbContext _context;
public UpdateTodoListCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task Handle(UpdateTodoListCommand request, CancellationToken cancellationToken)
{
var entity = await _context.TodoLists
var entity = await context.TodoLists
.FindAsync(new object[] { request.Id }, cancellationToken);
Guard.Against.NotFound(request.Id, entity);
entity.Title = request.Title;
await _context.SaveChangesAsync(cancellationToken);
await context.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -18,7 +18,7 @@ public class UpdateTodoListCommandValidator : AbstractValidator<UpdateTodoListCo
.WithErrorCode("Unique");
}
public async Task<bool> BeUniqueTitle(UpdateTodoListCommand model, string title, CancellationToken cancellationToken)
private async Task<bool> BeUniqueTitle(UpdateTodoListCommand model, string title, CancellationToken cancellationToken)
{
return await _context.TodoLists
.Where(l => l.Id != model.Id)

View File

@@ -8,17 +8,11 @@ namespace Hutopy.Application.TodoLists.Queries.GetTodos;
[Authorize]
public record GetTodosQuery : IRequest<TodosVm>;
public class GetTodosQueryHandler : IRequestHandler<GetTodosQuery, TodosVm>
public class GetTodosQueryHandler(
IApplicationDbContext context,
IMapper mapper)
: IRequestHandler<GetTodosQuery, TodosVm>
{
private readonly IApplicationDbContext _context;
private readonly IMapper _mapper;
public GetTodosQueryHandler(IApplicationDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<TodosVm> Handle(GetTodosQuery request, CancellationToken cancellationToken)
{
return new TodosVm
@@ -28,9 +22,9 @@ public class GetTodosQueryHandler : IRequestHandler<GetTodosQuery, TodosVm>
.Select(p => new LookupDto { Id = (int)p, Title = p.ToString() })
.ToList(),
Lists = await _context.TodoLists
Lists = await context.TodoLists
.AsNoTracking()
.ProjectTo<TodoListDto>(_mapper.ConfigurationProvider)
.ProjectTo<TodoListDto>(mapper.ConfigurationProvider)
.OrderBy(t => t.Title)
.ToListAsync(cancellationToken)
};

View File

@@ -4,18 +4,13 @@ namespace Hutopy.Application.TodoLists.Queries.GetTodos;
public class TodoListDto
{
public TodoListDto()
{
Items = Array.Empty<TodoItemDto>();
}
public int Id { get; init; }
public string? Title { get; init; }
public string? Colour { get; init; }
public IReadOnlyCollection<TodoItemDto> Items { get; init; }
public IReadOnlyCollection<TodoItemDto> Items { get; init; } = Array.Empty<TodoItemDto>();
private class Mapping : Profile
{

View File

@@ -8,10 +8,10 @@ public abstract class BaseEntity
// Using non-generic integer types for simplicity
public int Id { get; set; }
private readonly List<BaseEvent> _domainEvents = new();
private readonly List<BaseEvent> _domainEvents = [];
[NotMapped]
public IReadOnlyCollection<BaseEvent> DomainEvents => _domainEvents.AsReadOnly();
public IEnumerable<BaseEvent> DomainEvents => _domainEvents.AsReadOnly();
public void AddDomainEvent(BaseEvent domainEvent)
{

View File

@@ -3,7 +3,7 @@
// Learn more: https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/implement-value-objects
public abstract class ValueObject
{
protected static bool EqualOperator(ValueObject left, ValueObject right)
private static bool EqualOperator(ValueObject left, ValueObject right)
{
if (left is null ^ right is null)
{

View File

@@ -1,11 +1,8 @@
namespace Hutopy.Domain.Events;
public class TodoItemCompletedEvent : BaseEvent
public class TodoItemCompletedEvent(
TodoItem item)
: BaseEvent
{
public TodoItemCompletedEvent(TodoItem item)
{
Item = item;
}
public TodoItem Item { get; }
public TodoItem Item { get; } = item;
}

View File

@@ -1,11 +1,8 @@
namespace Hutopy.Domain.Events;
public class TodoItemCreatedEvent : BaseEvent
public class TodoItemCreatedEvent(
TodoItem item)
: BaseEvent
{
public TodoItemCreatedEvent(TodoItem item)
{
Item = item;
}
public TodoItem Item { get; }
public TodoItem Item { get; } = item;
}

View File

@@ -1,11 +1,8 @@
namespace Hutopy.Domain.Events;
public class TodoItemDeletedEvent : BaseEvent
public class TodoItemDeletedEvent(
TodoItem item)
: BaseEvent
{
public TodoItemDeletedEvent(TodoItem item)
{
Item = item;
}
public TodoItem Item { get; }
public TodoItem Item { get; } = item;
}

View File

@@ -1,9 +1,4 @@
namespace Hutopy.Domain.Exceptions;
public class UnsupportedColourException : Exception
{
public UnsupportedColourException(string code)
: base($"Colour \"{code}\" is unsupported.")
{
}
}
public class UnsupportedColourException(
string code) : Exception($"Colour \"{code}\" is unsupported.");

View File

@@ -47,7 +47,7 @@ public class Colour(string code) : ValueObject
return Code;
}
protected static IEnumerable<Colour> SupportedColours
private static IEnumerable<Colour> SupportedColours
{
get
{

View File

@@ -7,10 +7,10 @@ using Microsoft.EntityFrameworkCore;
namespace Hutopy.Infrastructure.Data;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IApplicationDbContext
public class ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: IdentityDbContext<ApplicationUser>(options), IApplicationDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
public DbSet<TodoList> TodoLists => Set<TodoList>();
public DbSet<TodoItem> TodoItems => Set<TodoItem>();

View File

@@ -1,5 +1,4 @@
using System.Runtime.InteropServices;
using Hutopy.Domain.Constants;
using Hutopy.Domain.Constants;
using Hutopy.Domain.Entities;
using Hutopy.Infrastructure.Identity;
using Microsoft.AspNetCore.Builder;
@@ -10,44 +9,35 @@ using Microsoft.Extensions.Logging;
namespace Hutopy.Infrastructure.Data;
public static class InitialiserExtensions
public static class InitializerExtensions
{
public static async Task InitialiseDatabaseAsync(this WebApplication app)
{
using var scope = app.Services.CreateScope();
var initialiser = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitialiser>();
var initializer = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitializer>();
await initialiser.InitialiseAsync();
await initializer.InitialiseAsync();
await initialiser.SeedAsync();
await initializer.SeedAsync();
}
}
public class ApplicationDbContextInitialiser
public class ApplicationDbContextInitializer(
ILogger<ApplicationDbContextInitializer> logger,
ApplicationDbContext context,
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager)
{
private readonly ILogger<ApplicationDbContextInitialiser> _logger;
private readonly ApplicationDbContext _context;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public ApplicationDbContextInitialiser(ILogger<ApplicationDbContextInitialiser> logger, ApplicationDbContext context, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
{
_logger = logger;
_context = context;
_userManager = userManager;
_roleManager = roleManager;
}
public async Task InitialiseAsync()
{
try
{
await _context.Database.MigrateAsync();
await context.Database.MigrateAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while initialising the database.");
logger.LogError(ex, "An error occurred while initialising the database.");
throw;
}
}
@@ -60,38 +50,38 @@ public class ApplicationDbContextInitialiser
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while seeding the database.");
logger.LogError(ex, "An error occurred while seeding the database.");
throw;
}
}
public async Task TrySeedAsync()
private async Task TrySeedAsync()
{
// Default roles
var administratorRole = new IdentityRole(Roles.Administrator);
if (_roleManager.Roles.All(r => r.Name != administratorRole.Name))
if (roleManager.Roles.All(r => r.Name != administratorRole.Name))
{
await _roleManager.CreateAsync(administratorRole);
await roleManager.CreateAsync(administratorRole);
}
// Default users
var administrator = new ApplicationUser { UserName = "administrator@localhost", Email = "administrator@localhost" };
if (_userManager.Users.All(u => u.UserName != administrator.UserName))
if (userManager.Users.All(u => u.UserName != administrator.UserName))
{
await _userManager.CreateAsync(administrator, "Administrator1!");
await userManager.CreateAsync(administrator, "Administrator1!");
if (!string.IsNullOrWhiteSpace(administratorRole.Name))
{
await _userManager.AddToRolesAsync(administrator, new [] { administratorRole.Name });
await userManager.AddToRolesAsync(administrator, new [] { administratorRole.Name });
}
}
// Default data
// Seed, if necessary
if (!_context.TodoLists.Any())
if (!context.TodoLists.Any())
{
_context.TodoLists.Add(new TodoList
context.TodoLists.Add(new TodoList
{
Title = "Todo List",
Items =
@@ -103,7 +93,7 @@ public class ApplicationDbContextInitialiser
}
});
await _context.SaveChangesAsync();
await context.SaveChangesAsync();
}
}
}

View File

@@ -6,19 +6,10 @@ using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Hutopy.Infrastructure.Data.Interceptors;
public class AuditableEntityInterceptor : SaveChangesInterceptor
public class AuditableEntityInterceptor(
IUser user,
TimeProvider dateTime) : SaveChangesInterceptor
{
private readonly IUser _user;
private readonly TimeProvider _dateTime;
public AuditableEntityInterceptor(
IUser user,
TimeProvider dateTime)
{
_user = user;
_dateTime = dateTime;
}
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
UpdateEntities(eventData.Context);
@@ -41,13 +32,13 @@ public class AuditableEntityInterceptor : SaveChangesInterceptor
{
if (entry.State is EntityState.Added or EntityState.Modified || entry.HasChangedOwnedEntities())
{
var utcNow = _dateTime.GetUtcNow();
var utcNow = dateTime.GetUtcNow();
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedBy = _user.Id;
entry.Entity.CreatedBy = user.Id;
entry.Entity.Created = utcNow;
}
entry.Entity.LastModifiedBy = _user.Id;
entry.Entity.LastModifiedBy = user.Id;
entry.Entity.LastModified = utcNow;
}
}

View File

@@ -5,15 +5,10 @@ using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Hutopy.Infrastructure.Data.Interceptors;
public class DispatchDomainEventsInterceptor : SaveChangesInterceptor
public class DispatchDomainEventsInterceptor(
IPublisher mediator)
: SaveChangesInterceptor
{
private readonly IMediator _mediator;
public DispatchDomainEventsInterceptor(IMediator mediator)
{
_mediator = mediator;
}
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
DispatchDomainEvents(eventData.Context).GetAwaiter().GetResult();
@@ -45,6 +40,6 @@ public class DispatchDomainEventsInterceptor : SaveChangesInterceptor
entities.ToList().ForEach(e => e.ClearDomainEvents());
foreach (var domainEvent in domainEvents)
await _mediator.Publish(domainEvent);
await mediator.Publish(domainEvent);
}
}

View File

@@ -8,8 +8,9 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Extensions.DependencyInjection;
namespace Hutopy.Infrastructure;
public static class DependencyInjection
{
@@ -34,7 +35,7 @@ public static class DependencyInjection
services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
services.AddScoped<ApplicationDbContextInitialiser>();
services.AddScoped<ApplicationDbContextInitializer>();
services.AddAuthentication()
.AddBearerToken(IdentityConstants.BearerScheme);

View File

@@ -5,25 +5,15 @@ using Microsoft.AspNetCore.Identity;
namespace Hutopy.Infrastructure.Identity;
public class IdentityService : IIdentityService
public class IdentityService(
UserManager<ApplicationUser> userManager,
IUserClaimsPrincipalFactory<ApplicationUser> userClaimsPrincipalFactory,
IAuthorizationService authorizationService)
: IIdentityService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IUserClaimsPrincipalFactory<ApplicationUser> _userClaimsPrincipalFactory;
private readonly IAuthorizationService _authorizationService;
public IdentityService(
UserManager<ApplicationUser> userManager,
IUserClaimsPrincipalFactory<ApplicationUser> userClaimsPrincipalFactory,
IAuthorizationService authorizationService)
{
_userManager = userManager;
_userClaimsPrincipalFactory = userClaimsPrincipalFactory;
_authorizationService = authorizationService;
}
public async Task<string?> GetUserNameAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
var user = await userManager.FindByIdAsync(userId);
return user?.UserName;
}
@@ -36,44 +26,44 @@ public class IdentityService : IIdentityService
Email = userName,
};
var result = await _userManager.CreateAsync(user, password);
var result = await userManager.CreateAsync(user, password);
return (result.ToApplicationResult(), user.Id);
}
public async Task<bool> IsInRoleAsync(string userId, string role)
{
var user = await _userManager.FindByIdAsync(userId);
var user = await userManager.FindByIdAsync(userId);
return user != null && await _userManager.IsInRoleAsync(user, role);
return user != null && await userManager.IsInRoleAsync(user, role);
}
public async Task<bool> AuthorizeAsync(string userId, string policyName)
{
var user = await _userManager.FindByIdAsync(userId);
var user = await userManager.FindByIdAsync(userId);
if (user == null)
{
return false;
}
var principal = await _userClaimsPrincipalFactory.CreateAsync(user);
var principal = await userClaimsPrincipalFactory.CreateAsync(user);
var result = await _authorizationService.AuthorizeAsync(principal, policyName);
var result = await authorizationService.AuthorizeAsync(principal, policyName);
return result.Succeeded;
}
public async Task<Result> DeleteUserAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
var user = await userManager.FindByIdAsync(userId);
return user != null ? await DeleteUserAsync(user) : Result.Success();
}
public async Task<Result> DeleteUserAsync(ApplicationUser user)
{
var result = await _userManager.DeleteAsync(user);
var result = await userManager.DeleteAsync(user);
return result.ToApplicationResult();
}

View File

@@ -16,22 +16,20 @@ public class StripeService : IStripeService
{
var options = new SessionCreateOptions
{
LineItems = new List<SessionLineItemOptions>
{
LineItems =
[
new SessionLineItemOptions
{
PriceData = new SessionLineItemPriceDataOptions
{
UnitAmount = price,
Currency = currency,
ProductData = new SessionLineItemPriceDataProductDataOptions
PriceData = new SessionLineItemPriceDataOptions
{
Name = "Tip",
UnitAmount = price,
Currency = currency,
ProductData = new SessionLineItemPriceDataProductDataOptions { Name = "Tip", },
},
},
Quantity = 1,
},
},
Quantity = 1,
}
],
Mode = "payment",
UiMode = "embedded",
ReturnUrl = $"http://localhost:5173/creatorfolio",

View File

@@ -3,11 +3,10 @@ using Hutopy.Application.Common.Interfaces;
using Hutopy.Infrastructure.Data;
using Hutopy.Web.Services;
using Microsoft.AspNetCore.Mvc;
using NSwag;
using NSwag.Generation.Processors.Security;
namespace Microsoft.Extensions.DependencyInjection;
namespace Hutopy.Web;
public static class DependencyInjection
{

View File

@@ -9,8 +9,8 @@ public class JoinUs : EndpointGroupBase
app.MapGroup(this)
.MapPost(CreateFutureCreator);
}
public Task<int> CreateFutureCreator(ISender sender, CreateFutureCreatorCommand command)
private static Task<int> CreateFutureCreator(ISender sender, CreateFutureCreatorCommand command)
{
return sender.Send(command);
}

View File

@@ -10,7 +10,7 @@ public class Stripe : EndpointGroupBase
.MapPost(CreateSessionCheckout);
}
public Task<string> CreateSessionCheckout(ISender sender, CreateSessionCheckoutCommand command)
private static Task<string> CreateSessionCheckout(ISender sender, CreateSessionCheckoutCommand command)
{
return sender.Send(command);
}

View File

@@ -20,31 +20,31 @@ public class TodoItems : EndpointGroupBase
.MapDelete(DeleteTodoItem, "{id}");
}
public Task<PaginatedList<TodoItemBriefDto>> GetTodoItemsWithPagination(ISender sender, [AsParameters] GetTodoItemsWithPaginationQuery query)
private static Task<PaginatedList<TodoItemBriefDto>> GetTodoItemsWithPagination(ISender sender, [AsParameters] GetTodoItemsWithPaginationQuery query)
{
return sender.Send(query);
}
public Task<int> CreateTodoItem(ISender sender, CreateTodoItemCommand command)
private static Task<int> CreateTodoItem(ISender sender, CreateTodoItemCommand command)
{
return sender.Send(command);
}
public async Task<IResult> UpdateTodoItem(ISender sender, int id, UpdateTodoItemCommand command)
private static async Task<IResult> UpdateTodoItem(ISender sender, int id, UpdateTodoItemCommand command)
{
if (id != command.Id) return Results.BadRequest();
await sender.Send(command);
return Results.NoContent();
}
public async Task<IResult> UpdateTodoItemDetail(ISender sender, int id, UpdateTodoItemDetailCommand command)
private static async Task<IResult> UpdateTodoItemDetail(ISender sender, int id, UpdateTodoItemDetailCommand command)
{
if (id != command.Id) return Results.BadRequest();
await sender.Send(command);
return Results.NoContent();
}
public async Task<IResult> DeleteTodoItem(ISender sender, int id)
private static async Task<IResult> DeleteTodoItem(ISender sender, int id)
{
await sender.Send(new DeleteTodoItemCommand(id));
return Results.NoContent();

View File

@@ -17,24 +17,24 @@ public class TodoLists : EndpointGroupBase
.MapDelete(DeleteTodoList, "{id}");
}
public Task<TodosVm> GetTodoLists(ISender sender)
private static Task<TodosVm> GetTodoLists(ISender sender)
{
return sender.Send(new GetTodosQuery());
return sender.Send(new GetTodosQuery());
}
public Task<int> CreateTodoList(ISender sender, CreateTodoListCommand command)
private static Task<int> CreateTodoList(ISender sender, CreateTodoListCommand command)
{
return sender.Send(command);
}
public async Task<IResult> UpdateTodoList(ISender sender, int id, UpdateTodoListCommand command)
private static async Task<IResult> UpdateTodoList(ISender sender, int id, UpdateTodoListCommand command)
{
if (id != command.Id) return Results.BadRequest();
await sender.Send(command);
return Results.NoContent();
}
public async Task<IResult> DeleteTodoList(ISender sender, int id)
private static async Task<IResult> DeleteTodoList(ISender sender, int id)
{
await sender.Send(new DeleteTodoListCommand(id));
return Results.NoContent();

View File

@@ -11,7 +11,7 @@ public class WeatherForecasts : EndpointGroupBase
.MapGet(GetWeatherForecasts);
}
public async Task<IEnumerable<WeatherForecast>> GetWeatherForecasts(ISender sender)
private static async Task<IEnumerable<WeatherForecast>> GetWeatherForecasts(ISender sender)
{
return await sender.Send(new GetWeatherForecastsQuery());
}

View File

@@ -11,8 +11,8 @@ public class CustomExceptionHandler : IExceptionHandler
public CustomExceptionHandler()
{
// Register known exception types and handlers.
_exceptionHandlers = new()
{
_exceptionHandlers = new Dictionary<Type, Func<HttpContext, Exception, Task>>
{
{ typeof(ValidationException), HandleValidationException },
{ typeof(NotFoundException), HandleNotFoundException },
{ typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException },
@@ -24,16 +24,14 @@ public class CustomExceptionHandler : IExceptionHandler
{
var exceptionType = exception.GetType();
if (_exceptionHandlers.ContainsKey(exceptionType))
{
await _exceptionHandlers[exceptionType].Invoke(httpContext, exception);
return true;
}
if (!_exceptionHandlers.TryGetValue(exceptionType, out Func<HttpContext, Exception, Task>? value)) return false;
await value.Invoke(httpContext, exception);
return true;
return false;
}
private async Task HandleValidationException(HttpContext httpContext, Exception ex)
private static async Task HandleValidationException(HttpContext httpContext, Exception ex)
{
var exception = (ValidationException)ex;
@@ -46,7 +44,7 @@ public class CustomExceptionHandler : IExceptionHandler
});
}
private async Task HandleNotFoundException(HttpContext httpContext, Exception ex)
private static async Task HandleNotFoundException(HttpContext httpContext, Exception ex)
{
var exception = (NotFoundException)ex;
@@ -61,7 +59,7 @@ public class CustomExceptionHandler : IExceptionHandler
});
}
private async Task HandleUnauthorizedAccessException(HttpContext httpContext, Exception ex)
private static async Task HandleUnauthorizedAccessException(HttpContext httpContext, Exception ex)
{
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
@@ -73,7 +71,7 @@ public class CustomExceptionHandler : IExceptionHandler
});
}
private async Task HandleForbiddenAccessException(HttpContext httpContext, Exception ex)
private static async Task HandleForbiddenAccessException(HttpContext httpContext, Exception ex)
{
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;

View File

@@ -4,7 +4,7 @@ namespace Hutopy.Web.Infrastructure;
public static class MethodInfoExtensions
{
public static bool IsAnonymous(this MethodInfo method)
private static bool IsAnonymous(this MethodInfo method)
{
var invalidChars = new[] { '<', '>' };
return method.Name.Any(invalidChars.Contains);
@@ -15,4 +15,4 @@ public static class MethodInfoExtensions
if (input.Method.IsAnonymous())
throw new ArgumentException("The endpoint name must be specified when using anonymous handlers.");
}
}
}

View File

@@ -1,4 +1,7 @@
using Hutopy.Application;
using Hutopy.Infrastructure;
using Hutopy.Infrastructure.Data;
using Hutopy.Web;
var builder = WebApplication.CreateBuilder(args);
@@ -61,4 +64,4 @@ app.MapEndpoints();
app.Run();
public partial class Program { }
public abstract partial class Program { }

View File

@@ -4,14 +4,9 @@ using Hutopy.Application.Common.Interfaces;
namespace Hutopy.Web.Services;
public class CurrentUser : IUser
public class CurrentUser(
IHttpContextAccessor httpContextAccessor)
: IUser
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CurrentUser(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string? Id => _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
public string? Id => httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
}