First commit. Include junk from template to remove

This commit is contained in:
Dominic Villemure
2024-03-09 20:25:30 -05:00
commit bbcefcf76f
140 changed files with 8151 additions and 0 deletions

View 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>

View 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();
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -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;
}
}
}

View 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();
}
}

View File

@@ -0,0 +1,6 @@
namespace Hutopy.Application.Common.Exceptions;
public class ForbiddenAccessException : Exception
{
public ForbiddenAccessException() : base() { }
}

View 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; }
}

View 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);
}

View 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);
}

View File

@@ -0,0 +1,6 @@
namespace Hutopy.Application.Common.Interfaces;
public interface IUser
{
string? Id { get; }
}

View 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();
}

View 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>();
}
}
}

View 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);
}
}

View 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);
}
}

View 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;
}

View 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;
}
}

View 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;

View File

@@ -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;
}
}

View File

@@ -0,0 +1,11 @@
namespace Hutopy.Application.TodoItems.Commands.CreateTodoItem;
public class CreateTodoItemCommandValidator : AbstractValidator<CreateTodoItemCommand>
{
public CreateTodoItemCommandValidator()
{
RuleFor(v => v.Title)
.MaximumLength(200)
.NotEmpty();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,11 @@
namespace Hutopy.Application.TodoItems.Commands.UpdateTodoItem;
public class UpdateTodoItemCommandValidator : AbstractValidator<UpdateTodoItemCommand>
{
public UpdateTodoItemCommandValidator()
{
RuleFor(v => v.Title)
.MaximumLength(200)
.NotEmpty();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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.");
}
}

View File

@@ -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>();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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)
};
}
}

View 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));
}
}
}

View 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>();
}
}
}

View 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>();
}

View File

@@ -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)]
});
}
}

View File

@@ -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; }
}