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; }
|
||||
}
|
||||
12
src/Domain/Common/BaseAuditableEntity.cs
Normal file
12
src/Domain/Common/BaseAuditableEntity.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Hutopy.Domain.Common;
|
||||
|
||||
public abstract class BaseAuditableEntity : BaseEntity
|
||||
{
|
||||
public DateTimeOffset Created { get; set; }
|
||||
|
||||
public string? CreatedBy { get; set; }
|
||||
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
|
||||
public string? LastModifiedBy { get; set; }
|
||||
}
|
||||
30
src/Domain/Common/BaseEntity.cs
Normal file
30
src/Domain/Common/BaseEntity.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Hutopy.Domain.Common;
|
||||
|
||||
public abstract class BaseEntity
|
||||
{
|
||||
// This can easily be modified to be BaseEntity<T> and public T Id to support different key types.
|
||||
// Using non-generic integer types for simplicity
|
||||
public int Id { get; set; }
|
||||
|
||||
private readonly List<BaseEvent> _domainEvents = new();
|
||||
|
||||
[NotMapped]
|
||||
public IReadOnlyCollection<BaseEvent> DomainEvents => _domainEvents.AsReadOnly();
|
||||
|
||||
public void AddDomainEvent(BaseEvent domainEvent)
|
||||
{
|
||||
_domainEvents.Add(domainEvent);
|
||||
}
|
||||
|
||||
public void RemoveDomainEvent(BaseEvent domainEvent)
|
||||
{
|
||||
_domainEvents.Remove(domainEvent);
|
||||
}
|
||||
|
||||
public void ClearDomainEvents()
|
||||
{
|
||||
_domainEvents.Clear();
|
||||
}
|
||||
}
|
||||
7
src/Domain/Common/BaseEvent.cs
Normal file
7
src/Domain/Common/BaseEvent.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Hutopy.Domain.Common;
|
||||
|
||||
public abstract class BaseEvent : INotification
|
||||
{
|
||||
}
|
||||
45
src/Domain/Common/ValueObject.cs
Normal file
45
src/Domain/Common/ValueObject.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace Hutopy.Domain.Common;
|
||||
|
||||
// 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)
|
||||
{
|
||||
if (left is null ^ right is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return left?.Equals(right!) != false;
|
||||
}
|
||||
|
||||
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
|
||||
{
|
||||
return !(EqualOperator(left, right));
|
||||
}
|
||||
|
||||
protected abstract IEnumerable<object> GetEqualityComponents();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null || obj.GetType() != GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var other = (ValueObject)obj;
|
||||
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCode();
|
||||
|
||||
foreach (var component in GetEqualityComponents())
|
||||
{
|
||||
hash.Add(component);
|
||||
}
|
||||
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
7
src/Domain/Constants/Policies.cs
Normal file
7
src/Domain/Constants/Policies.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Hutopy.Domain.Constants;
|
||||
|
||||
public abstract class Policies
|
||||
{
|
||||
public const string CanPurge = nameof(CanPurge);
|
||||
public const string CanDelete = nameof(CanDelete);
|
||||
}
|
||||
6
src/Domain/Constants/Roles.cs
Normal file
6
src/Domain/Constants/Roles.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Domain.Constants;
|
||||
|
||||
public abstract class Roles
|
||||
{
|
||||
public const string Administrator = nameof(Administrator);
|
||||
}
|
||||
12
src/Domain/Domain.csproj
Normal file
12
src/Domain/Domain.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Hutopy.Domain</RootNamespace>
|
||||
<AssemblyName>Hutopy.Domain</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
31
src/Domain/Entities/TodoItem.cs
Normal file
31
src/Domain/Entities/TodoItem.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace Hutopy.Domain.Entities;
|
||||
|
||||
public class TodoItem : BaseAuditableEntity
|
||||
{
|
||||
public int ListId { get; set; }
|
||||
|
||||
public string? Title { get; set; }
|
||||
|
||||
public string? Note { get; set; }
|
||||
|
||||
public PriorityLevel Priority { get; set; }
|
||||
|
||||
public DateTime? Reminder { get; set; }
|
||||
|
||||
private bool _done;
|
||||
public bool Done
|
||||
{
|
||||
get => _done;
|
||||
set
|
||||
{
|
||||
if (value && !_done)
|
||||
{
|
||||
AddDomainEvent(new TodoItemCompletedEvent(this));
|
||||
}
|
||||
|
||||
_done = value;
|
||||
}
|
||||
}
|
||||
|
||||
public TodoList List { get; set; } = null!;
|
||||
}
|
||||
10
src/Domain/Entities/TodoList.cs
Normal file
10
src/Domain/Entities/TodoList.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Hutopy.Domain.Entities;
|
||||
|
||||
public class TodoList : BaseAuditableEntity
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
|
||||
public Colour Colour { get; set; } = Colour.White;
|
||||
|
||||
public IList<TodoItem> Items { get; private set; } = new List<TodoItem>();
|
||||
}
|
||||
9
src/Domain/Enums/PriorityLevel.cs
Normal file
9
src/Domain/Enums/PriorityLevel.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Hutopy.Domain.Enums;
|
||||
|
||||
public enum PriorityLevel
|
||||
{
|
||||
None = 0,
|
||||
Low = 1,
|
||||
Medium = 2,
|
||||
High = 3
|
||||
}
|
||||
11
src/Domain/Events/TodoItemCompletedEvent.cs
Normal file
11
src/Domain/Events/TodoItemCompletedEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Hutopy.Domain.Events;
|
||||
|
||||
public class TodoItemCompletedEvent : BaseEvent
|
||||
{
|
||||
public TodoItemCompletedEvent(TodoItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public TodoItem Item { get; }
|
||||
}
|
||||
11
src/Domain/Events/TodoItemCreatedEvent.cs
Normal file
11
src/Domain/Events/TodoItemCreatedEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Hutopy.Domain.Events;
|
||||
|
||||
public class TodoItemCreatedEvent : BaseEvent
|
||||
{
|
||||
public TodoItemCreatedEvent(TodoItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public TodoItem Item { get; }
|
||||
}
|
||||
11
src/Domain/Events/TodoItemDeletedEvent.cs
Normal file
11
src/Domain/Events/TodoItemDeletedEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Hutopy.Domain.Events;
|
||||
|
||||
public class TodoItemDeletedEvent : BaseEvent
|
||||
{
|
||||
public TodoItemDeletedEvent(TodoItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public TodoItem Item { get; }
|
||||
}
|
||||
9
src/Domain/Exceptions/UnsupportedColourException.cs
Normal file
9
src/Domain/Exceptions/UnsupportedColourException.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Hutopy.Domain.Exceptions;
|
||||
|
||||
public class UnsupportedColourException : Exception
|
||||
{
|
||||
public UnsupportedColourException(string code)
|
||||
: base($"Colour \"{code}\" is unsupported.")
|
||||
{
|
||||
}
|
||||
}
|
||||
6
src/Domain/GlobalUsings.cs
Normal file
6
src/Domain/GlobalUsings.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
global using Hutopy.Domain.Common;
|
||||
global using Hutopy.Domain.Entities;
|
||||
global using Hutopy.Domain.Enums;
|
||||
global using Hutopy.Domain.Events;
|
||||
global using Hutopy.Domain.Exceptions;
|
||||
global using Hutopy.Domain.ValueObjects;
|
||||
69
src/Domain/ValueObjects/Colour.cs
Normal file
69
src/Domain/ValueObjects/Colour.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
namespace Hutopy.Domain.ValueObjects;
|
||||
|
||||
public class Colour(string code) : ValueObject
|
||||
{
|
||||
public static Colour From(string code)
|
||||
{
|
||||
var colour = new Colour(code);
|
||||
|
||||
if (!SupportedColours.Contains(colour))
|
||||
{
|
||||
throw new UnsupportedColourException(code);
|
||||
}
|
||||
|
||||
return colour;
|
||||
}
|
||||
|
||||
public static Colour White => new("#FFFFFF");
|
||||
|
||||
public static Colour Red => new("#FF5733");
|
||||
|
||||
public static Colour Orange => new("#FFC300");
|
||||
|
||||
public static Colour Yellow => new("#FFFF66");
|
||||
|
||||
public static Colour Green => new("#CCFF99");
|
||||
|
||||
public static Colour Blue => new("#6666FF");
|
||||
|
||||
public static Colour Purple => new("#9966CC");
|
||||
|
||||
public static Colour Grey => new("#999999");
|
||||
|
||||
public string Code { get; private set; } = string.IsNullOrWhiteSpace(code)?"#000000":code;
|
||||
|
||||
public static implicit operator string(Colour colour)
|
||||
{
|
||||
return colour.ToString();
|
||||
}
|
||||
|
||||
public static explicit operator Colour(string code)
|
||||
{
|
||||
return From(code);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Code;
|
||||
}
|
||||
|
||||
protected static IEnumerable<Colour> SupportedColours
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return White;
|
||||
yield return Red;
|
||||
yield return Orange;
|
||||
yield return Yellow;
|
||||
yield return Green;
|
||||
yield return Blue;
|
||||
yield return Purple;
|
||||
yield return Grey;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<object> GetEqualityComponents()
|
||||
{
|
||||
yield return Code;
|
||||
}
|
||||
}
|
||||
23
src/Infrastructure/Data/ApplicationDbContext.cs
Normal file
23
src/Infrastructure/Data/ApplicationDbContext.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Reflection;
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Entities;
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data;
|
||||
|
||||
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IApplicationDbContext
|
||||
{
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<TodoList> TodoLists => Set<TodoList>();
|
||||
|
||||
public DbSet<TodoItem> TodoItems => Set<TodoItem>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
}
|
||||
}
|
||||
109
src/Infrastructure/Data/ApplicationDbContextInitialiser.cs
Normal file
109
src/Infrastructure/Data/ApplicationDbContextInitialiser.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Hutopy.Domain.Constants;
|
||||
using Hutopy.Domain.Entities;
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data;
|
||||
|
||||
public static class InitialiserExtensions
|
||||
{
|
||||
public static async Task InitialiseDatabaseAsync(this WebApplication app)
|
||||
{
|
||||
using var scope = app.Services.CreateScope();
|
||||
|
||||
var initialiser = scope.ServiceProvider.GetRequiredService<ApplicationDbContextInitialiser>();
|
||||
|
||||
await initialiser.InitialiseAsync();
|
||||
|
||||
await initialiser.SeedAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class ApplicationDbContextInitialiser
|
||||
{
|
||||
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();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while initialising the database.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SeedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await TrySeedAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while seeding the database.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TrySeedAsync()
|
||||
{
|
||||
// Default roles
|
||||
var administratorRole = new IdentityRole(Roles.Administrator);
|
||||
|
||||
if (_roleManager.Roles.All(r => r.Name != administratorRole.Name))
|
||||
{
|
||||
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))
|
||||
{
|
||||
await _userManager.CreateAsync(administrator, "Administrator1!");
|
||||
if (!string.IsNullOrWhiteSpace(administratorRole.Name))
|
||||
{
|
||||
await _userManager.AddToRolesAsync(administrator, new [] { administratorRole.Name });
|
||||
}
|
||||
}
|
||||
|
||||
// Default data
|
||||
// Seed, if necessary
|
||||
if (!_context.TodoLists.Any())
|
||||
{
|
||||
_context.TodoLists.Add(new TodoList
|
||||
{
|
||||
Title = "Todo List",
|
||||
Items =
|
||||
{
|
||||
new TodoItem { Title = "Make a todo list 📃" },
|
||||
new TodoItem { Title = "Check off the first item ✅" },
|
||||
new TodoItem { Title = "Realise you've already done two things on the list! 🤯"},
|
||||
new TodoItem { Title = "Reward yourself with a nice, long nap 🏆" },
|
||||
}
|
||||
});
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Configurations;
|
||||
|
||||
public class TodoItemConfiguration : IEntityTypeConfiguration<TodoItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TodoItem> builder)
|
||||
{
|
||||
builder.Property(t => t.Title)
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Hutopy.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Configurations;
|
||||
|
||||
public class TodoListConfiguration : IEntityTypeConfiguration<TodoList>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TodoList> builder)
|
||||
{
|
||||
builder.Property(t => t.Title)
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
|
||||
builder
|
||||
.OwnsOne(b => b.Colour);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Interceptors;
|
||||
|
||||
public class AuditableEntityInterceptor : 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);
|
||||
|
||||
return base.SavingChanges(eventData, result);
|
||||
}
|
||||
|
||||
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
UpdateEntities(eventData.Context);
|
||||
|
||||
return base.SavingChangesAsync(eventData, result, cancellationToken);
|
||||
}
|
||||
|
||||
public void UpdateEntities(DbContext? context)
|
||||
{
|
||||
if (context == null) return;
|
||||
|
||||
foreach (var entry in context.ChangeTracker.Entries<BaseAuditableEntity>())
|
||||
{
|
||||
if (entry.State is EntityState.Added or EntityState.Modified || entry.HasChangedOwnedEntities())
|
||||
{
|
||||
var utcNow = _dateTime.GetUtcNow();
|
||||
if (entry.State == EntityState.Added)
|
||||
{
|
||||
entry.Entity.CreatedBy = _user.Id;
|
||||
entry.Entity.Created = utcNow;
|
||||
}
|
||||
entry.Entity.LastModifiedBy = _user.Id;
|
||||
entry.Entity.LastModified = utcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static bool HasChangedOwnedEntities(this EntityEntry entry) =>
|
||||
entry.References.Any(r =>
|
||||
r.TargetEntry != null &&
|
||||
r.TargetEntry.Metadata.IsOwned() &&
|
||||
(r.TargetEntry.State == EntityState.Added || r.TargetEntry.State == EntityState.Modified));
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Hutopy.Domain.Common;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Interceptors;
|
||||
|
||||
public class DispatchDomainEventsInterceptor : 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();
|
||||
|
||||
return base.SavingChanges(eventData, result);
|
||||
|
||||
}
|
||||
|
||||
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await DispatchDomainEvents(eventData.Context);
|
||||
|
||||
return await base.SavingChangesAsync(eventData, result, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DispatchDomainEvents(DbContext? context)
|
||||
{
|
||||
if (context == null) return;
|
||||
|
||||
var entities = context.ChangeTracker
|
||||
.Entries<BaseEntity>()
|
||||
.Where(e => e.Entity.DomainEvents.Any())
|
||||
.Select(e => e.Entity);
|
||||
|
||||
var domainEvents = entities
|
||||
.SelectMany(e => e.DomainEvents)
|
||||
.ToList();
|
||||
|
||||
entities.ToList().ForEach(e => e.ClearDomainEvents());
|
||||
|
||||
foreach (var domainEvent in domainEvents)
|
||||
await _mediator.Publish(domainEvent);
|
||||
}
|
||||
}
|
||||
399
src/Infrastructure/Data/Migrations/00000000000000_InitialCreate.Designer.cs
generated
Normal file
399
src/Infrastructure/Data/Migrations/00000000000000_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,399 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("00000000000000_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.0-preview.6.23329.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("Done")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("ListId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("Reminder")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListId");
|
||||
|
||||
b.ToTable("TodoItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TodoLists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoItem", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Domain.Entities.TodoList", "List")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.OwnsOne("Hutopy.Domain.ValueObjects.Colour", "Colour", b1 =>
|
||||
{
|
||||
b1.Property<int>("TodoListId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b1.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.HasKey("TodoListId");
|
||||
|
||||
b1.ToTable("TodoLists");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("TodoListId");
|
||||
});
|
||||
|
||||
b.Navigation("Colour")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
UserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
SecurityStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
LockoutEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TodoLists",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Colour_Code = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Created = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TodoLists", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserTokens",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
Value = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TodoItems",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ListId = table.Column<int>(type: "int", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Note = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Priority = table.Column<int>(type: "int", nullable: false),
|
||||
Reminder = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
Done = table.Column<bool>(type: "bit", nullable: false),
|
||||
Created = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
LastModified = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TodoItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TodoItems_TodoLists_ListId",
|
||||
column: x => x.ListId,
|
||||
principalTable: "TodoLists",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetRoleClaims_RoleId",
|
||||
table: "AspNetRoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
table: "AspNetRoles",
|
||||
column: "NormalizedName",
|
||||
unique: true,
|
||||
filter: "[NormalizedName] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserClaims_UserId",
|
||||
table: "AspNetUserClaims",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserLogins_UserId",
|
||||
table: "AspNetUserLogins",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserRoles_RoleId",
|
||||
table: "AspNetUserRoles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedUserName",
|
||||
unique: true,
|
||||
filter: "[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TodoItems_ListId",
|
||||
table: "TodoItems",
|
||||
column: "ListId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoleClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserLogins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TodoItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TodoLists");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Hutopy.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.0-preview.6.23329.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("Done")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("ListId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("Reminder")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListId");
|
||||
|
||||
b.ToTable("TodoItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTimeOffset>("LastModified")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("LastModifiedBy")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TodoLists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Infrastructure.Identity.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoItem", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Domain.Entities.TodoList", "List")
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("ListId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("List");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.OwnsOne("Hutopy.Domain.ValueObjects.Colour", "Colour", b1 =>
|
||||
{
|
||||
b1.Property<int>("TodoListId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b1.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b1.HasKey("TodoListId");
|
||||
|
||||
b1.ToTable("TodoLists");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("TodoListId");
|
||||
});
|
||||
|
||||
b.Navigation("Colour")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("Hutopy.Infrastructure.Identity.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Hutopy.Domain.Entities.TodoList", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/Infrastructure/DependencyInjection.cs
Normal file
58
src/Infrastructure/DependencyInjection.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Domain.Constants;
|
||||
using Hutopy.Infrastructure.Data;
|
||||
using Hutopy.Infrastructure.Data.Interceptors;
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// Replace password in the connection string with env var.
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection") ?? "";
|
||||
var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD");
|
||||
|
||||
connectionString = connectionString.Replace("{DB_PASSWORD}", dbPassword);
|
||||
|
||||
Guard.Against.Null(connectionString, message: "Connection string 'DefaultConnection' not found.");
|
||||
|
||||
services.AddScoped<ISaveChangesInterceptor, AuditableEntityInterceptor>();
|
||||
services.AddScoped<ISaveChangesInterceptor, DispatchDomainEventsInterceptor>();
|
||||
|
||||
services.AddDbContext<ApplicationDbContext>((sp, options) =>
|
||||
{
|
||||
options.AddInterceptors(sp.GetServices<ISaveChangesInterceptor>());
|
||||
|
||||
options.UseSqlServer(connectionString);
|
||||
});
|
||||
|
||||
services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
|
||||
|
||||
services.AddScoped<ApplicationDbContextInitialiser>();
|
||||
|
||||
services.AddAuthentication()
|
||||
.AddBearerToken(IdentityConstants.BearerScheme);
|
||||
|
||||
services.AddAuthorizationBuilder();
|
||||
|
||||
services
|
||||
.AddIdentityCore<ApplicationUser>()
|
||||
.AddRoles<IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddApiEndpoints();
|
||||
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
services.AddTransient<IIdentityService, IdentityService>();
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
options.AddPolicy(Policies.CanPurge, policy => policy.RequireRole(Roles.Administrator)));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
1
src/Infrastructure/GlobalUsings.cs
Normal file
1
src/Infrastructure/GlobalUsings.cs
Normal file
@@ -0,0 +1 @@
|
||||
global using Ardalis.GuardClauses;
|
||||
7
src/Infrastructure/Identity/ApplicationUser.cs
Normal file
7
src/Infrastructure/Identity/ApplicationUser.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Infrastructure.Identity;
|
||||
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
}
|
||||
14
src/Infrastructure/Identity/IdentityResultExtensions.cs
Normal file
14
src/Infrastructure/Identity/IdentityResultExtensions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Infrastructure.Identity;
|
||||
|
||||
public static class IdentityResultExtensions
|
||||
{
|
||||
public static Result ToApplicationResult(this IdentityResult result)
|
||||
{
|
||||
return result.Succeeded
|
||||
? Result.Success()
|
||||
: Result.Failure(result.Errors.Select(e => e.Description));
|
||||
}
|
||||
}
|
||||
80
src/Infrastructure/Identity/IdentityService.cs
Normal file
80
src/Infrastructure/Identity/IdentityService.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Hutopy.Infrastructure.Identity;
|
||||
|
||||
public class IdentityService : 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);
|
||||
|
||||
return user?.UserName;
|
||||
}
|
||||
|
||||
public async Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password)
|
||||
{
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = userName,
|
||||
Email = userName,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
return user != null && await _userManager.IsInRoleAsync(user, role);
|
||||
}
|
||||
|
||||
public async Task<bool> AuthorizeAsync(string userId, string policyName)
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var principal = await _userClaimsPrincipalFactory.CreateAsync(user);
|
||||
|
||||
var result = await _authorizationService.AuthorizeAsync(principal, policyName);
|
||||
|
||||
return result.Succeeded;
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteUserAsync(string 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);
|
||||
|
||||
return result.ToApplicationResult();
|
||||
}
|
||||
}
|
||||
20
src/Infrastructure/Infrastructure.csproj
Normal file
20
src/Infrastructure/Infrastructure.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Hutopy.Infrastructure</RootNamespace>
|
||||
<AssemblyName>Hutopy.Infrastructure</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application\Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
66
src/Web/DependencyInjection.cs
Normal file
66
src/Web/DependencyInjection.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Azure.Identity;
|
||||
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;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddWebServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddDatabaseDeveloperPageExceptionFilter();
|
||||
|
||||
services.AddScoped<IUser, CurrentUser>();
|
||||
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.AddHealthChecks()
|
||||
.AddDbContextCheck<ApplicationDbContext>();
|
||||
|
||||
services.AddExceptionHandler<CustomExceptionHandler>();
|
||||
|
||||
services.AddRazorPages();
|
||||
|
||||
// Customise default API behaviour
|
||||
services.Configure<ApiBehaviorOptions>(options =>
|
||||
options.SuppressModelStateInvalidFilter = true);
|
||||
|
||||
services.AddEndpointsApiExplorer();
|
||||
|
||||
services.AddOpenApiDocument((configure, sp) =>
|
||||
{
|
||||
configure.Title = "Hutopy API";
|
||||
|
||||
// Add JWT
|
||||
configure.AddSecurity("JWT", Enumerable.Empty<string>(), new OpenApiSecurityScheme
|
||||
{
|
||||
Type = OpenApiSecuritySchemeType.ApiKey,
|
||||
Name = "Authorization",
|
||||
In = OpenApiSecurityApiKeyLocation.Header,
|
||||
Description = "Type into the textbox: Bearer {your JWT token}."
|
||||
});
|
||||
|
||||
configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT"));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddKeyVaultIfConfigured(this IServiceCollection services, ConfigurationManager configuration)
|
||||
{
|
||||
var keyVaultUri = configuration["KeyVaultUri"];
|
||||
if (!string.IsNullOrWhiteSpace(keyVaultUri))
|
||||
{
|
||||
configuration.AddAzureKeyVault(
|
||||
new Uri(keyVaultUri),
|
||||
new DefaultAzureCredential());
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
52
src/Web/Endpoints/TodoItems.cs
Normal file
52
src/Web/Endpoints/TodoItems.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Hutopy.Application.Common.Models;
|
||||
using Hutopy.Application.TodoItems.Commands.CreateTodoItem;
|
||||
using Hutopy.Application.TodoItems.Commands.DeleteTodoItem;
|
||||
using Hutopy.Application.TodoItems.Commands.UpdateTodoItem;
|
||||
using Hutopy.Application.TodoItems.Commands.UpdateTodoItemDetail;
|
||||
using Hutopy.Application.TodoItems.Queries.GetTodoItemsWithPagination;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class TodoItems : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.RequireAuthorization()
|
||||
.MapGet(GetTodoItemsWithPagination)
|
||||
.MapPost(CreateTodoItem)
|
||||
.MapPut(UpdateTodoItem, "{id}")
|
||||
.MapPut(UpdateTodoItemDetail, "UpdateDetail/{id}")
|
||||
.MapDelete(DeleteTodoItem, "{id}");
|
||||
}
|
||||
|
||||
public Task<PaginatedList<TodoItemBriefDto>> GetTodoItemsWithPagination(ISender sender, [AsParameters] GetTodoItemsWithPaginationQuery query)
|
||||
{
|
||||
return sender.Send(query);
|
||||
}
|
||||
|
||||
public Task<int> CreateTodoItem(ISender sender, CreateTodoItemCommand command)
|
||||
{
|
||||
return sender.Send(command);
|
||||
}
|
||||
|
||||
public 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)
|
||||
{
|
||||
if (id != command.Id) return Results.BadRequest();
|
||||
await sender.Send(command);
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
public async Task<IResult> DeleteTodoItem(ISender sender, int id)
|
||||
{
|
||||
await sender.Send(new DeleteTodoItemCommand(id));
|
||||
return Results.NoContent();
|
||||
}
|
||||
}
|
||||
42
src/Web/Endpoints/TodoLists.cs
Normal file
42
src/Web/Endpoints/TodoLists.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Hutopy.Application.TodoLists.Commands.CreateTodoList;
|
||||
using Hutopy.Application.TodoLists.Commands.DeleteTodoList;
|
||||
using Hutopy.Application.TodoLists.Commands.UpdateTodoList;
|
||||
using Hutopy.Application.TodoLists.Queries.GetTodos;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class TodoLists : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.RequireAuthorization()
|
||||
.MapGet(GetTodoLists)
|
||||
.MapPost(CreateTodoList)
|
||||
.MapPut(UpdateTodoList, "{id}")
|
||||
.MapDelete(DeleteTodoList, "{id}");
|
||||
}
|
||||
|
||||
public Task<TodosVm> GetTodoLists(ISender sender)
|
||||
{
|
||||
return sender.Send(new GetTodosQuery());
|
||||
}
|
||||
|
||||
public Task<int> CreateTodoList(ISender sender, CreateTodoListCommand command)
|
||||
{
|
||||
return sender.Send(command);
|
||||
}
|
||||
|
||||
public 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)
|
||||
{
|
||||
await sender.Send(new DeleteTodoListCommand(id));
|
||||
return Results.NoContent();
|
||||
}
|
||||
}
|
||||
12
src/Web/Endpoints/Users.cs
Normal file
12
src/Web/Endpoints/Users.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class Users : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.MapIdentityApi<ApplicationUser>();
|
||||
}
|
||||
}
|
||||
18
src/Web/Endpoints/WeatherForecasts.cs
Normal file
18
src/Web/Endpoints/WeatherForecasts.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Hutopy.Application.WeatherForecasts.Queries.GetWeatherForecasts;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class WeatherForecasts : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.RequireAuthorization()
|
||||
.MapGet(GetWeatherForecasts);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<WeatherForecast>> GetWeatherForecasts(ISender sender)
|
||||
{
|
||||
return await sender.Send(new GetWeatherForecastsQuery());
|
||||
}
|
||||
}
|
||||
3
src/Web/GlobalUsings.cs
Normal file
3
src/Web/GlobalUsings.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
global using Ardalis.GuardClauses;
|
||||
global using Hutopy.Web.Infrastructure;
|
||||
global using MediatR;
|
||||
87
src/Web/Infrastructure/CustomExceptionHandler.cs
Normal file
87
src/Web/Infrastructure/CustomExceptionHandler.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Hutopy.Application.Common.Exceptions;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public class CustomExceptionHandler : IExceptionHandler
|
||||
{
|
||||
private readonly Dictionary<Type, Func<HttpContext, Exception, Task>> _exceptionHandlers;
|
||||
|
||||
public CustomExceptionHandler()
|
||||
{
|
||||
// Register known exception types and handlers.
|
||||
_exceptionHandlers = new()
|
||||
{
|
||||
{ typeof(ValidationException), HandleValidationException },
|
||||
{ typeof(NotFoundException), HandleNotFoundException },
|
||||
{ typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException },
|
||||
{ typeof(ForbiddenAccessException), HandleForbiddenAccessException },
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
var exceptionType = exception.GetType();
|
||||
|
||||
if (_exceptionHandlers.ContainsKey(exceptionType))
|
||||
{
|
||||
await _exceptionHandlers[exceptionType].Invoke(httpContext, exception);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task HandleValidationException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
var exception = (ValidationException)ex;
|
||||
|
||||
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ValidationProblemDetails(exception.Errors)
|
||||
{
|
||||
Status = StatusCodes.Status400BadRequest,
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleNotFoundException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
var exception = (NotFoundException)ex;
|
||||
|
||||
httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails()
|
||||
{
|
||||
Status = StatusCodes.Status404NotFound,
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||
Title = "The specified resource was not found.",
|
||||
Detail = exception.Message
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleUnauthorizedAccessException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
|
||||
{
|
||||
Status = StatusCodes.Status401Unauthorized,
|
||||
Title = "Unauthorized",
|
||||
Type = "https://tools.ietf.org/html/rfc7235#section-3.1"
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleForbiddenAccessException(HttpContext httpContext, Exception ex)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
|
||||
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
|
||||
{
|
||||
Status = StatusCodes.Status403Forbidden,
|
||||
Title = "Forbidden",
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3"
|
||||
});
|
||||
}
|
||||
}
|
||||
6
src/Web/Infrastructure/EndpointGroupBase.cs
Normal file
6
src/Web/Infrastructure/EndpointGroupBase.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public abstract class EndpointGroupBase
|
||||
{
|
||||
public abstract void Map(WebApplication app);
|
||||
}
|
||||
46
src/Web/Infrastructure/IEndpointRouteBuilderExtensions.cs
Normal file
46
src/Web/Infrastructure/IEndpointRouteBuilderExtensions.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public static class IEndpointRouteBuilderExtensions
|
||||
{
|
||||
public static IEndpointRouteBuilder MapGet(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern = "")
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapGet(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapPost(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern = "")
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapPost(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapPut(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern)
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapPut(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapDelete(this IEndpointRouteBuilder builder, Delegate handler, [StringSyntax("Route")] string pattern)
|
||||
{
|
||||
Guard.Against.AnonymousMethod(handler);
|
||||
|
||||
builder.MapDelete(pattern, handler)
|
||||
.WithName(handler.Method.Name);
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
18
src/Web/Infrastructure/MethodInfoExtensions.cs
Normal file
18
src/Web/Infrastructure/MethodInfoExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public static class MethodInfoExtensions
|
||||
{
|
||||
public static bool IsAnonymous(this MethodInfo method)
|
||||
{
|
||||
var invalidChars = new[] { '<', '>' };
|
||||
return method.Name.Any(invalidChars.Contains);
|
||||
}
|
||||
|
||||
public static void AnonymousMethod(this IGuardClause guardClause, Delegate input)
|
||||
{
|
||||
if (input.Method.IsAnonymous())
|
||||
throw new ArgumentException("The endpoint name must be specified when using anonymous handlers.");
|
||||
}
|
||||
}
|
||||
37
src/Web/Infrastructure/WebApplicationExtensions.cs
Normal file
37
src/Web/Infrastructure/WebApplicationExtensions.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Hutopy.Web.Infrastructure;
|
||||
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
public static RouteGroupBuilder MapGroup(this WebApplication app, EndpointGroupBase group)
|
||||
{
|
||||
var groupName = group.GetType().Name;
|
||||
|
||||
return app
|
||||
.MapGroup($"/api/{groupName}")
|
||||
.WithGroupName(groupName)
|
||||
.WithTags(groupName)
|
||||
.WithOpenApi();
|
||||
}
|
||||
|
||||
public static WebApplication MapEndpoints(this WebApplication app)
|
||||
{
|
||||
var endpointGroupType = typeof(EndpointGroupBase);
|
||||
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
var endpointGroupTypes = assembly.GetExportedTypes()
|
||||
.Where(t => t.IsSubclassOf(endpointGroupType));
|
||||
|
||||
foreach (var type in endpointGroupTypes)
|
||||
{
|
||||
if (Activator.CreateInstance(type) is EndpointGroupBase instance)
|
||||
{
|
||||
instance.Map(app);
|
||||
}
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
26
src/Web/Pages/Error.cshtml
Normal file
26
src/Web/Pages/Error.cshtml
Normal file
@@ -0,0 +1,26 @@
|
||||
@page
|
||||
@model ErrorModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
25
src/Web/Pages/Error.cshtml.cs
Normal file
25
src/Web/Pages/Error.cshtml.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Hutopy.Web.Pages;
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
private readonly ILogger<ErrorModel> _logger;
|
||||
|
||||
public ErrorModel(ILogger<ErrorModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
36
src/Web/Pages/Shared/_LoginPartial.cshtml
Normal file
36
src/Web/Pages/Shared/_LoginPartial.cshtml
Normal file
@@ -0,0 +1,36 @@
|
||||
@using Hutopy.Infrastructure.Identity
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject UserManager<ApplicationUser> UserManager
|
||||
|
||||
@{
|
||||
string? returnUrl = null;
|
||||
var query = ViewContext.HttpContext.Request.Query;
|
||||
if (query.ContainsKey("returnUrl"))
|
||||
{
|
||||
returnUrl = query["returnUrl"];
|
||||
}
|
||||
}
|
||||
|
||||
<ul class="navbar-nav">
|
||||
@if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity!.Name!</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/">
|
||||
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register" asp-route-returnUrl="@returnUrl">Register</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login" asp-route-returnUrl="@returnUrl">Login</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
3
src/Web/Pages/_ViewImports.cshtml
Normal file
3
src/Web/Pages/_ViewImports.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@using Hutopy.Web
|
||||
@namespace Hutopy.Web.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
64
src/Web/Program.cs
Normal file
64
src/Web/Program.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Hutopy.Infrastructure.Data;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll", builder =>
|
||||
{
|
||||
builder.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddKeyVaultIfConfigured(builder.Configuration);
|
||||
|
||||
builder.Services.AddApplicationServices();
|
||||
builder.Services.AddInfrastructureServices(builder.Configuration);
|
||||
builder.Services.AddWebServices();
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
await app.InitialiseDatabaseAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHealthChecks("/health");
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseSwaggerUi(settings =>
|
||||
{
|
||||
settings.Path = "/api";
|
||||
settings.DocumentPath = "/api/specification.json";
|
||||
});
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller}/{action=Index}/{id?}");
|
||||
|
||||
app.MapRazorPages();
|
||||
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
app.UseExceptionHandler(options => { });
|
||||
|
||||
app.Map("/", () => Results.Redirect("/api"));
|
||||
|
||||
app.MapEndpoints();
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program { }
|
||||
27
src/Web/Properties/launchSettings.json
Normal file
27
src/Web/Properties/launchSettings.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:61846",
|
||||
"sslPort": 44312
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"Hutopy.Web": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Web/Services/CurrentUser.cs
Normal file
17
src/Web/Services/CurrentUser.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
using Hutopy.Application.Common.Interfaces;
|
||||
|
||||
namespace Hutopy.Web.Services;
|
||||
|
||||
public class CurrentUser : IUser
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public CurrentUser(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public string? Id => _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
}
|
||||
44
src/Web/Web.csproj
Normal file
44
src/Web/Web.csproj
Normal file
@@ -0,0 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Hutopy.Web</RootNamespace>
|
||||
<AssemblyName>Hutopy.Web</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application\Application.csproj" />
|
||||
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" />
|
||||
<PackageReference Include="NSwag.AspNetCore" />
|
||||
<PackageReference Include="NSwag.MSBuild">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentValidation.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<!-- Auto-generated Open API specification and Angular TypeScript clients -->
|
||||
<PropertyGroup>
|
||||
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="NSwag" AfterTargets="PostBuildEvent" Condition=" '$(Configuration)' == 'Debug' And '$(SkipNSwag)' != 'True' ">
|
||||
<Exec ConsoleToMSBuild="true" ContinueOnError="true" WorkingDirectory="$(ProjectDir)" EnvironmentVariables="ASPNETCORE_ENVIRONMENT=Development" Command="$(NSwagExe_Net80) run config.nswag /variables:Configuration=$(Configuration)">
|
||||
<Output TaskParameter="ExitCode" PropertyName="NSwagExitCode" />
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="NSwagOutput" />
|
||||
</Exec>
|
||||
|
||||
<Message Text="$(NSwagOutput)" Condition="'$(NSwagExitCode)' == '0'" Importance="low" />
|
||||
<Error Text="$(NSwagOutput)" Condition="'$(NSwagExitCode)' != '0'" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
139
src/Web/Web.http
Normal file
139
src/Web/Web.http
Normal file
@@ -0,0 +1,139 @@
|
||||
# For more info on HTTP files go to https://aka.ms/vs/httpfile
|
||||
@Web_HostAddress = https://localhost:5001
|
||||
|
||||
@Email=administrator@localhost
|
||||
@Password=Administrator1!
|
||||
@BearerToken=<YourToken>
|
||||
|
||||
# POST Users Register
|
||||
POST {{Web_HostAddress}}/api/Users/Register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "{{Email}}",
|
||||
"password": "{{Password}}"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# POST Users Login
|
||||
POST {{Web_HostAddress}}/api/Users/Login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "{{Email}}",
|
||||
"password": "{{Password}}"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# POST Users Refresh
|
||||
POST {{Web_HostAddress}}/api/Users/Refresh
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refreshToken": ""
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# GET WeatherForecast
|
||||
GET {{Web_HostAddress}}/api/WeatherForecasts
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
|
||||
###
|
||||
|
||||
# GET TodoLists
|
||||
GET {{Web_HostAddress}}/api/TodoLists
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
|
||||
###
|
||||
|
||||
# POST TodoLists
|
||||
POST {{Web_HostAddress}}/api/TodoLists
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
// CreateTodoListCommand
|
||||
{
|
||||
"Title": "Backlog"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# PUT TodoLists
|
||||
PUT {{Web_HostAddress}}/api/TodoLists/1
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
// UpdateTodoListCommand
|
||||
{
|
||||
"Id": 1,
|
||||
"Title": "Product Backlog"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# DELETE TodoLists
|
||||
DELETE {{Web_HostAddress}}/api/TodoLists/1
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
|
||||
###
|
||||
|
||||
# GET TodoItems
|
||||
@PageNumber = 1
|
||||
@PageSize = 10
|
||||
GET {{Web_HostAddress}}/api/TodoItems?ListId=1&PageNumber={{PageNumber}}&PageSize={{PageSize}}
|
||||
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
|
||||
###
|
||||
|
||||
# POST TodoItems
|
||||
POST {{Web_HostAddress}}/api/TodoItems
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
// CreateTodoItemCommand
|
||||
{
|
||||
"ListId": 1,
|
||||
"Title": "Eat a burrito 🌯"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
#PUT TodoItems UpdateItemDetails
|
||||
PUT {{Web_HostAddress}}/api/TodoItems/UpdateItemDetails?Id=1
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
// UpdateTodoItemDetailCommand
|
||||
{
|
||||
"Id": 1,
|
||||
"ListId": 1,
|
||||
"Priority": 3,
|
||||
"Note": "This is a good idea!"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# PUT TodoItems
|
||||
PUT {{Web_HostAddress}}/api/TodoItems/1
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
// UpdateTodoItemCommand
|
||||
{
|
||||
"Id": 1,
|
||||
"Title": "Eat a yummy burrito 🌯",
|
||||
"Done": true
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# DELETE TodoItem
|
||||
DELETE {{Web_HostAddress}}/api/TodoItems/1
|
||||
Authorization: Bearer {{BearerToken}}
|
||||
|
||||
###
|
||||
10
src/Web/appsettings.Development.json
Normal file
10
src/Web/appsettings.Development.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.AspNetCore.SpaProxy": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Web/appsettings.json
Normal file
13
src/Web/appsettings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost,1433;Database=TestDeux;User Id=sa;Password={DB_PASSWORD};MultipleActiveResultSets=true;TrustServerCertificate=True"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
63
src/Web/config.nswag
Normal file
63
src/Web/config.nswag
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"runtime": "Net80",
|
||||
"defaultVariables": null,
|
||||
"documentGenerator": {
|
||||
"aspNetCoreToOpenApi": {
|
||||
"project": "Web.csproj",
|
||||
"msBuildProjectExtensionsPath": null,
|
||||
"configuration": null,
|
||||
"runtime": null,
|
||||
"targetFramework": null,
|
||||
"noBuild": true,
|
||||
"msBuildOutputPath": null,
|
||||
"verbose": false,
|
||||
"workingDirectory": null,
|
||||
"requireParametersWithoutDefault": true,
|
||||
"apiGroupNames": null,
|
||||
"defaultPropertyNameHandling": "CamelCase",
|
||||
"defaultReferenceTypeNullHandling": "Null",
|
||||
"defaultDictionaryValueReferenceTypeNullHandling": "NotNull",
|
||||
"defaultResponseReferenceTypeNullHandling": "NotNull",
|
||||
"generateOriginalParameterNames": true,
|
||||
"defaultEnumHandling": "Integer",
|
||||
"flattenInheritanceHierarchy": false,
|
||||
"generateKnownTypes": true,
|
||||
"generateEnumMappingDescription": false,
|
||||
"generateXmlObjects": false,
|
||||
"generateAbstractProperties": false,
|
||||
"generateAbstractSchemas": true,
|
||||
"ignoreObsoleteProperties": false,
|
||||
"allowReferencesWithProperties": false,
|
||||
"useXmlDocumentation": true,
|
||||
"resolveExternalXmlDocumentation": true,
|
||||
"excludedTypeNames": [],
|
||||
"serviceHost": null,
|
||||
"serviceBasePath": null,
|
||||
"serviceSchemes": [],
|
||||
"infoTitle": "Hutopy API",
|
||||
"infoDescription": null,
|
||||
"infoVersion": "1.0.0",
|
||||
"documentTemplate": null,
|
||||
"documentProcessorTypes": [],
|
||||
"operationProcessorTypes": [],
|
||||
"typeNameGeneratorType": null,
|
||||
"schemaNameGeneratorType": null,
|
||||
"contractResolverType": null,
|
||||
"serializerSettingsType": null,
|
||||
"useDocumentProvider": true,
|
||||
"documentName": "v1",
|
||||
"aspNetCoreEnvironment": null,
|
||||
"createWebHostBuilderMethod": null,
|
||||
"startupType": null,
|
||||
"allowNullableBodyParameters": true,
|
||||
"useHttpAttributeNameAsOperationId": false,
|
||||
"output": "wwwroot/api/specification.json",
|
||||
"outputType": "OpenApi3",
|
||||
"newLineBehavior": "Auto",
|
||||
"assemblyPaths": [],
|
||||
"assemblyConfig": null,
|
||||
"referencePaths": [],
|
||||
"useNuGetCache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
1250
src/Web/wwwroot/api/specification.json
Normal file
1250
src/Web/wwwroot/api/specification.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/Web/wwwroot/favicon.ico
Normal file
BIN
src/Web/wwwroot/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Reference in New Issue
Block a user