First commit. Include junk from template to remove
This commit is contained in:
19
src/Application/Application.csproj
Normal file
19
src/Application/Application.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Hutopy.Application</RootNamespace>
|
||||
<AssemblyName>Hutopy.Application</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Ardalis.GuardClauses" />
|
||||
<PackageReference Include="AutoMapper" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
79
src/Application/Common/Behaviours/AuthorizationBehaviour.cs
Normal file
79
src/Application/Common/Behaviours/AuthorizationBehaviour.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.Reflection;
|
||||
using Hutopy.Application.Common.Exceptions;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Security;
|
||||
|
||||
namespace Hutopy.Application.Common.Behaviours;
|
||||
|
||||
public class AuthorizationBehaviour<TRequest, TResponse> : 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>();
|
||||
|
||||
if (authorizeAttributes.Any())
|
||||
{
|
||||
// Must be authenticated user
|
||||
if (_user.Id == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
// Role-based authorization
|
||||
var authorizeAttributesWithRoles = authorizeAttributes.Where(a => !string.IsNullOrWhiteSpace(a.Roles));
|
||||
|
||||
if (authorizeAttributesWithRoles.Any())
|
||||
{
|
||||
var authorized = false;
|
||||
|
||||
foreach (var roles in authorizeAttributesWithRoles.Select(a => a.Roles.Split(',')))
|
||||
{
|
||||
foreach (var role in roles)
|
||||
{
|
||||
var isInRole = await _identityService.IsInRoleAsync(_user.Id, role.Trim());
|
||||
if (isInRole)
|
||||
{
|
||||
authorized = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Must be a member of at least one role in roles
|
||||
if (!authorized)
|
||||
{
|
||||
throw new ForbiddenAccessException();
|
||||
}
|
||||
}
|
||||
|
||||
// Policy-based authorization
|
||||
var authorizeAttributesWithPolicies = authorizeAttributes.Where(a => !string.IsNullOrWhiteSpace(a.Policy));
|
||||
if (authorizeAttributesWithPolicies.Any())
|
||||
{
|
||||
foreach (var policy in authorizeAttributesWithPolicies.Select(a => a.Policy))
|
||||
{
|
||||
var authorized = await _identityService.AuthorizeAsync(_user.Id, policy);
|
||||
|
||||
if (!authorized)
|
||||
{
|
||||
throw new ForbiddenAccessException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User is authorized / authorization not required
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
34
src/Application/Common/Behaviours/LoggingBehaviour.cs
Normal file
34
src/Application/Common/Behaviours/LoggingBehaviour.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using MediatR.Pipeline;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Application.Common.Behaviours;
|
||||
|
||||
public class LoggingBehaviour<TRequest> : 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;
|
||||
}
|
||||
|
||||
public async Task Process(TRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
var userId = _user.Id ?? string.Empty;
|
||||
string? userName = string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
userName = await _identityService.GetUserNameAsync(userId);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Hutopy Request: {Name} {@UserId} {@UserName} {@Request}",
|
||||
requestName, userId, userName, request);
|
||||
}
|
||||
}
|
||||
53
src/Application/Common/Behaviours/PerformanceBehaviour.cs
Normal file
53
src/Application/Common/Behaviours/PerformanceBehaviour.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Diagnostics;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Application.Common.Behaviours;
|
||||
|
||||
public class PerformanceBehaviour<TRequest, TResponse> : 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;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
{
|
||||
_timer.Start();
|
||||
|
||||
var response = await next();
|
||||
|
||||
_timer.Stop();
|
||||
|
||||
var elapsedMilliseconds = _timer.ElapsedMilliseconds;
|
||||
|
||||
if (elapsedMilliseconds > 500)
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
var userId = _user.Id ?? string.Empty;
|
||||
var userName = string.Empty;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Application.Common.Behaviours;
|
||||
|
||||
public class UnhandledExceptionBehaviour<TRequest, TResponse> : 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
|
||||
{
|
||||
return await next();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
|
||||
_logger.LogError(ex, "Hutopy Request: Unhandled Exception for Request {Name} {@Request}", requestName, request);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Application/Common/Behaviours/ValidationBehaviour.cs
Normal file
35
src/Application/Common/Behaviours/ValidationBehaviour.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using ValidationException = Hutopy.Application.Common.Exceptions.ValidationException;
|
||||
|
||||
namespace Hutopy.Application.Common.Behaviours;
|
||||
|
||||
public class ValidationBehaviour<TRequest, TResponse> : 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);
|
||||
|
||||
var validationResults = await Task.WhenAll(
|
||||
_validators.Select(v =>
|
||||
v.ValidateAsync(context, cancellationToken)));
|
||||
|
||||
var failures = validationResults
|
||||
.Where(r => r.Errors.Any())
|
||||
.SelectMany(r => r.Errors)
|
||||
.ToList();
|
||||
|
||||
if (failures.Any())
|
||||
throw new ValidationException(failures);
|
||||
}
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Application.Common.Exceptions;
|
||||
|
||||
public class ForbiddenAccessException : Exception
|
||||
{
|
||||
public ForbiddenAccessException() : base() { }
|
||||
}
|
||||
22
src/Application/Common/Exceptions/ValidationException.cs
Normal file
22
src/Application/Common/Exceptions/ValidationException.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using FluentValidation.Results;
|
||||
|
||||
namespace Hutopy.Application.Common.Exceptions;
|
||||
|
||||
public class ValidationException : Exception
|
||||
{
|
||||
public ValidationException()
|
||||
: base("One or more validation failures have occurred.")
|
||||
{
|
||||
Errors = new Dictionary<string, string[]>();
|
||||
}
|
||||
|
||||
public ValidationException(IEnumerable<ValidationFailure> failures)
|
||||
: this()
|
||||
{
|
||||
Errors = failures
|
||||
.GroupBy(e => e.PropertyName, e => e.ErrorMessage)
|
||||
.ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
|
||||
}
|
||||
|
||||
public IDictionary<string, string[]> Errors { get; }
|
||||
}
|
||||
12
src/Application/Common/Interfaces/IApplicationDbContext.cs
Normal file
12
src/Application/Common/Interfaces/IApplicationDbContext.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.Common.Interfaces;
|
||||
|
||||
public interface IApplicationDbContext
|
||||
{
|
||||
DbSet<TodoList> TodoLists { get; }
|
||||
|
||||
DbSet<TodoItem> TodoItems { get; }
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
16
src/Application/Common/Interfaces/IIdentityService.cs
Normal file
16
src/Application/Common/Interfaces/IIdentityService.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
|
||||
namespace Hutopy.Application.Common.Interfaces;
|
||||
|
||||
public interface IIdentityService
|
||||
{
|
||||
Task<string?> GetUserNameAsync(string userId);
|
||||
|
||||
Task<bool> IsInRoleAsync(string userId, string role);
|
||||
|
||||
Task<bool> AuthorizeAsync(string userId, string policyName);
|
||||
|
||||
Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password);
|
||||
|
||||
Task<Result> DeleteUserAsync(string userId);
|
||||
}
|
||||
6
src/Application/Common/Interfaces/IUser.cs
Normal file
6
src/Application/Common/Interfaces/IUser.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Application.Common.Interfaces;
|
||||
|
||||
public interface IUser
|
||||
{
|
||||
string? Id { get; }
|
||||
}
|
||||
12
src/Application/Common/Mappings/MappingExtensions.cs
Normal file
12
src/Application/Common/Mappings/MappingExtensions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
|
||||
namespace Hutopy.Application.Common.Mappings;
|
||||
|
||||
public static class MappingExtensions
|
||||
{
|
||||
public static Task<PaginatedList<TDestination>> PaginatedListAsync<TDestination>(this IQueryable<TDestination> queryable, int pageNumber, int pageSize) where TDestination : class
|
||||
=> PaginatedList<TDestination>.CreateAsync(queryable.AsNoTracking(), pageNumber, pageSize);
|
||||
|
||||
public static Task<List<TDestination>> ProjectToListAsync<TDestination>(this IQueryable queryable, IConfigurationProvider configuration) where TDestination : class
|
||||
=> queryable.ProjectTo<TDestination>(configuration).AsNoTracking().ToListAsync();
|
||||
}
|
||||
19
src/Application/Common/Models/LookupDto.cs
Normal file
19
src/Application/Common/Models/LookupDto.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.Common.Models;
|
||||
|
||||
public class LookupDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
|
||||
private class Mapping : Profile
|
||||
{
|
||||
public Mapping()
|
||||
{
|
||||
CreateMap<TodoList, LookupDto>();
|
||||
CreateMap<TodoItem, LookupDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Application/Common/Models/PaginatedList.cs
Normal file
29
src/Application/Common/Models/PaginatedList.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace Hutopy.Application.Common.Models;
|
||||
|
||||
public class PaginatedList<T>
|
||||
{
|
||||
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 bool HasPreviousPage => PageNumber > 1;
|
||||
|
||||
public bool HasNextPage => PageNumber < TotalPages;
|
||||
|
||||
public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageNumber, int pageSize)
|
||||
{
|
||||
var count = await source.CountAsync();
|
||||
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
|
||||
|
||||
return new PaginatedList<T>(items, count, pageNumber, pageSize);
|
||||
}
|
||||
}
|
||||
24
src/Application/Common/Models/Result.cs
Normal file
24
src/Application/Common/Models/Result.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace Hutopy.Application.Common.Models;
|
||||
|
||||
public class Result
|
||||
{
|
||||
internal Result(bool succeeded, IEnumerable<string> errors)
|
||||
{
|
||||
Succeeded = succeeded;
|
||||
Errors = errors.ToArray();
|
||||
}
|
||||
|
||||
public bool Succeeded { get; init; }
|
||||
|
||||
public string[] Errors { get; init; }
|
||||
|
||||
public static Result Success()
|
||||
{
|
||||
return new Result(true, Array.Empty<string>());
|
||||
}
|
||||
|
||||
public static Result Failure(IEnumerable<string> errors)
|
||||
{
|
||||
return new Result(false, errors);
|
||||
}
|
||||
}
|
||||
23
src/Application/Common/Security/AuthorizeAttribute.cs
Normal file
23
src/Application/Common/Security/AuthorizeAttribute.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Hutopy.Application.Common.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the class this attribute is applied to requires authorization.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public class AuthorizeAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthorizeAttribute"/> class.
|
||||
/// </summary>
|
||||
public AuthorizeAttribute() { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma delimited list of roles that are allowed to access the resource.
|
||||
/// </summary>
|
||||
public string Roles { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the policy name that determines access to the resource.
|
||||
/// </summary>
|
||||
public string Policy { get; set; } = string.Empty;
|
||||
}
|
||||
25
src/Application/DependencyInjection.cs
Normal file
25
src/Application/DependencyInjection.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Reflection;
|
||||
using Hutopy.Application.Common.Behaviours;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddAutoMapper(Assembly.GetExecutingAssembly());
|
||||
|
||||
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
|
||||
services.AddMediatR(cfg =>
|
||||
{
|
||||
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>));
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehaviour<,>));
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
|
||||
//cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
6
src/Application/GlobalUsings.cs
Normal file
6
src/Application/GlobalUsings.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
global using Ardalis.GuardClauses;
|
||||
global using AutoMapper;
|
||||
global using AutoMapper.QueryableExtensions;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
global using FluentValidation;
|
||||
global using MediatR;
|
||||
@@ -0,0 +1,40 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Entities;
|
||||
using Hutopy.Domain.Events;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Commands.CreateTodoItem;
|
||||
|
||||
public record CreateTodoItemCommand : IRequest<int>
|
||||
{
|
||||
public int ListId { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
}
|
||||
|
||||
public class CreateTodoItemCommandHandler : 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
|
||||
{
|
||||
ListId = request.ListId,
|
||||
Title = request.Title,
|
||||
Done = false
|
||||
};
|
||||
|
||||
entity.AddDomainEvent(new TodoItemCreatedEvent(entity));
|
||||
|
||||
_context.TodoItems.Add(entity);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Hutopy.Application.TodoItems.Commands.CreateTodoItem;
|
||||
|
||||
public class CreateTodoItemCommandValidator : AbstractValidator<CreateTodoItemCommand>
|
||||
{
|
||||
public CreateTodoItemCommandValidator()
|
||||
{
|
||||
RuleFor(v => v.Title)
|
||||
.MaximumLength(200)
|
||||
.NotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Events;
|
||||
using Hutopy.Application.Common.Security;
|
||||
using Hutopy.Domain.Constants;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Commands.DeleteTodoItem;
|
||||
|
||||
[Authorize(Roles = Roles.Administrator)]
|
||||
[Authorize(Policy = Policies.CanDelete)]
|
||||
public record DeleteTodoItemCommand(int Id) : IRequest;
|
||||
|
||||
public class DeleteTodoItemCommandHandler : 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
|
||||
.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
|
||||
Guard.Against.NotFound(request.Id, entity);
|
||||
|
||||
_context.TodoItems.Remove(entity);
|
||||
|
||||
entity.AddDomainEvent(new TodoItemDeletedEvent(entity));
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Commands.UpdateTodoItem;
|
||||
|
||||
public record UpdateTodoItemCommand : IRequest
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
|
||||
public bool Done { get; init; }
|
||||
}
|
||||
|
||||
public class UpdateTodoItemCommandHandler : 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
|
||||
.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
|
||||
Guard.Against.NotFound(request.Id, entity);
|
||||
|
||||
entity.Title = request.Title;
|
||||
entity.Done = request.Done;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Hutopy.Application.TodoItems.Commands.UpdateTodoItem;
|
||||
|
||||
public class UpdateTodoItemCommandValidator : AbstractValidator<UpdateTodoItemCommand>
|
||||
{
|
||||
public UpdateTodoItemCommandValidator()
|
||||
{
|
||||
RuleFor(v => v.Title)
|
||||
.MaximumLength(200)
|
||||
.NotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Enums;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Commands.UpdateTodoItemDetail;
|
||||
|
||||
public record UpdateTodoItemDetailCommand : IRequest
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public int ListId { get; init; }
|
||||
|
||||
public PriorityLevel Priority { get; init; }
|
||||
|
||||
public string? Note { get; init; }
|
||||
}
|
||||
|
||||
public class UpdateTodoItemDetailCommandHandler : 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
|
||||
.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
|
||||
Guard.Against.NotFound(request.Id, entity);
|
||||
|
||||
entity.ListId = request.ListId;
|
||||
entity.Priority = request.Priority;
|
||||
entity.Note = request.Note;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Hutopy.Domain.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.EventHandlers;
|
||||
|
||||
public class TodoItemCompletedEventHandler : 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);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Hutopy.Domain.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.EventHandlers;
|
||||
|
||||
public class TodoItemCreatedEventHandler : 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);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Mappings;
|
||||
using Hutopy.Application.Common.Models;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Queries.GetTodoItemsWithPagination;
|
||||
|
||||
public 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>>
|
||||
{
|
||||
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
|
||||
.Where(x => x.ListId == request.ListId)
|
||||
.OrderBy(x => x.Title)
|
||||
.ProjectTo<TodoItemBriefDto>(_mapper.ConfigurationProvider)
|
||||
.PaginatedListAsync(request.PageNumber, request.PageSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Hutopy.Application.TodoItems.Queries.GetTodoItemsWithPagination;
|
||||
|
||||
public class GetTodoItemsWithPaginationQueryValidator : AbstractValidator<GetTodoItemsWithPaginationQuery>
|
||||
{
|
||||
public GetTodoItemsWithPaginationQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.ListId)
|
||||
.NotEmpty().WithMessage("ListId is required.");
|
||||
|
||||
RuleFor(x => x.PageNumber)
|
||||
.GreaterThanOrEqualTo(1).WithMessage("PageNumber at least greater than or equal to 1.");
|
||||
|
||||
RuleFor(x => x.PageSize)
|
||||
.GreaterThanOrEqualTo(1).WithMessage("PageSize at least greater than or equal to 1.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.TodoItems.Queries.GetTodoItemsWithPagination;
|
||||
|
||||
public class TodoItemBriefDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public int ListId { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
|
||||
public bool Done { get; init; }
|
||||
|
||||
private class Mapping : Profile
|
||||
{
|
||||
public Mapping()
|
||||
{
|
||||
CreateMap<TodoItem, TodoItemBriefDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.CreateTodoList;
|
||||
|
||||
public record CreateTodoListCommand : IRequest<int>
|
||||
{
|
||||
public string? Title { get; init; }
|
||||
}
|
||||
|
||||
public class CreateTodoListCommandHandler : 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();
|
||||
|
||||
entity.Title = request.Title;
|
||||
|
||||
_context.TodoLists.Add(entity);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.CreateTodoList;
|
||||
|
||||
public class CreateTodoListCommandValidator : AbstractValidator<CreateTodoListCommand>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public CreateTodoListCommandValidator(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
|
||||
RuleFor(v => v.Title)
|
||||
.NotEmpty()
|
||||
.MaximumLength(200)
|
||||
.MustAsync(BeUniqueTitle)
|
||||
.WithMessage("'{PropertyName}' must be unique.")
|
||||
.WithErrorCode("Unique");
|
||||
}
|
||||
|
||||
public async Task<bool> BeUniqueTitle(string title, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _context.TodoLists
|
||||
.AllAsync(l => l.Title != title, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.DeleteTodoList;
|
||||
|
||||
public record DeleteTodoListCommand(int Id) : IRequest;
|
||||
|
||||
public class DeleteTodoListCommandHandler : 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
|
||||
.Where(l => l.Id == request.Id)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
Guard.Against.NotFound(request.Id, entity);
|
||||
|
||||
_context.TodoLists.Remove(entity);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Security;
|
||||
using Hutopy.Domain.Constants;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.PurgeTodoLists;
|
||||
|
||||
[Authorize(Roles = Roles.Administrator)]
|
||||
[Authorize(Policy = Policies.CanPurge)]
|
||||
public record PurgeTodoListsCommand : IRequest;
|
||||
|
||||
public class PurgeTodoListsCommandHandler : 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);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.UpdateTodoList;
|
||||
|
||||
public record UpdateTodoListCommand : IRequest
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
}
|
||||
|
||||
public class UpdateTodoListCommandHandler : 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
|
||||
.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
|
||||
Guard.Against.NotFound(request.Id, entity);
|
||||
|
||||
entity.Title = request.Title;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Commands.UpdateTodoList;
|
||||
|
||||
public class UpdateTodoListCommandValidator : AbstractValidator<UpdateTodoListCommand>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public UpdateTodoListCommandValidator(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
|
||||
RuleFor(v => v.Title)
|
||||
.NotEmpty()
|
||||
.MaximumLength(200)
|
||||
.MustAsync(BeUniqueTitle)
|
||||
.WithMessage("'{PropertyName}' must be unique.")
|
||||
.WithErrorCode("Unique");
|
||||
}
|
||||
|
||||
public async Task<bool> BeUniqueTitle(UpdateTodoListCommand model, string title, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _context.TodoLists
|
||||
.Where(l => l.Id != model.Id)
|
||||
.AllAsync(l => l.Title != title, cancellationToken);
|
||||
}
|
||||
}
|
||||
38
src/Application/TodoLists/Queries/GetTodos/GetTodos.cs
Normal file
38
src/Application/TodoLists/Queries/GetTodos/GetTodos.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Hutopy.Application.Common.Security;
|
||||
using Hutopy.Domain.Enums;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Queries.GetTodos;
|
||||
|
||||
[Authorize]
|
||||
public record GetTodosQuery : IRequest<TodosVm>;
|
||||
|
||||
public class GetTodosQueryHandler : 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
|
||||
{
|
||||
PriorityLevels = Enum.GetValues(typeof(PriorityLevel))
|
||||
.Cast<PriorityLevel>()
|
||||
.Select(p => new LookupDto { Id = (int)p, Title = p.ToString() })
|
||||
.ToList(),
|
||||
|
||||
Lists = await _context.TodoLists
|
||||
.AsNoTracking()
|
||||
.ProjectTo<TodoListDto>(_mapper.ConfigurationProvider)
|
||||
.OrderBy(t => t.Title)
|
||||
.ToListAsync(cancellationToken)
|
||||
};
|
||||
}
|
||||
}
|
||||
27
src/Application/TodoLists/Queries/GetTodos/TodoItemDto.cs
Normal file
27
src/Application/TodoLists/Queries/GetTodos/TodoItemDto.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Queries.GetTodos;
|
||||
|
||||
public class TodoItemDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
|
||||
public int ListId { get; init; }
|
||||
|
||||
public string? Title { get; init; }
|
||||
|
||||
public bool Done { get; init; }
|
||||
|
||||
public int Priority { get; init; }
|
||||
|
||||
public string? Note { get; init; }
|
||||
|
||||
private class Mapping : Profile
|
||||
{
|
||||
public Mapping()
|
||||
{
|
||||
CreateMap<TodoItem, TodoItemDto>().ForMember(d => d.Priority,
|
||||
opt => opt.MapFrom(s => (int)s.Priority));
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Application/TodoLists/Queries/GetTodos/TodoListDto.cs
Normal file
27
src/Application/TodoLists/Queries/GetTodos/TodoListDto.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
|
||||
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; }
|
||||
|
||||
private class Mapping : Profile
|
||||
{
|
||||
public Mapping()
|
||||
{
|
||||
CreateMap<TodoList, TodoListDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/Application/TodoLists/Queries/GetTodos/TodosVm.cs
Normal file
10
src/Application/TodoLists/Queries/GetTodos/TodosVm.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
|
||||
namespace Hutopy.Application.TodoLists.Queries.GetTodos;
|
||||
|
||||
public class TodosVm
|
||||
{
|
||||
public IReadOnlyCollection<LookupDto> PriorityLevels { get; init; } = Array.Empty<LookupDto>();
|
||||
|
||||
public IReadOnlyCollection<TodoListDto> Lists { get; init; } = Array.Empty<TodoListDto>();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Hutopy.Application.WeatherForecasts.Queries.GetWeatherForecasts;
|
||||
|
||||
public record GetWeatherForecastsQuery : IRequest<IEnumerable<WeatherForecast>>;
|
||||
|
||||
public class GetWeatherForecastsQueryHandler : IRequestHandler<GetWeatherForecastsQuery, IEnumerable<WeatherForecast>>
|
||||
{
|
||||
private static readonly string[] Summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
public async Task<IEnumerable<WeatherForecast>> Handle(GetWeatherForecastsQuery request, CancellationToken cancellationToken)
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
{
|
||||
var rng = new Random();
|
||||
|
||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = DateTime.Now.AddDays(index),
|
||||
TemperatureC = rng.Next(-20, 55),
|
||||
Summary = Summaries[rng.Next(Summaries.Length)]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Hutopy.Application.WeatherForecasts.Queries.GetWeatherForecasts;
|
||||
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateTime Date { get; init; }
|
||||
|
||||
public int TemperatureC { get; init; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
|
||||
public string? Summary { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user