diff --git a/Application.Tests.Unit/Articles/CreateArticleCommandHandlerTests.cs b/Application.Tests.Unit/Articles/CreateArticleCommandHandlerTests.cs index 817f7bc..9ef78d6 100644 --- a/Application.Tests.Unit/Articles/CreateArticleCommandHandlerTests.cs +++ b/Application.Tests.Unit/Articles/CreateArticleCommandHandlerTests.cs @@ -2,9 +2,10 @@ using Application.Data; using Application.Features.Articles.CreateArticle; using Domain.Entities; -using MediatR; +using FluentValidation; using MockQueryable.Moq; using Slugify; +using FluentValidation.Results; namespace Application.Tests.Unit.Articles; @@ -13,7 +14,7 @@ public class CreateArticleCommandHandlerTests private readonly Mock _mockDbContext = new(); private readonly Mock _mockSlugHelper = new(); private readonly Mock _mockCurrentUserService = new(); - private readonly Mock _mockPublisher = new(); + private readonly Mock> _mockValidator = new(); private readonly CreateArticleCommandHandler _handler; public CreateArticleCommandHandlerTests() @@ -22,7 +23,7 @@ public CreateArticleCommandHandlerTests() _mockDbContext.Object, _mockSlugHelper.Object, _mockCurrentUserService.Object, - _mockPublisher.Object + _mockValidator.Object ); } @@ -36,11 +37,13 @@ public async void Handle_Should_ReturnFailureResult_WhenGeneratedIdIsEmpty() _mockDbContext.Setup(x => x.Articles).Returns(mockArticlesDbSet.Object); _mockSlugHelper.Setup(x => x.GenerateSlug(" ")).Returns(""); + _mockValidator.Setup(v => v.Validate(It.IsAny())) + .Returns(new ValidationResult()); var command = new CreateArticleCommand(" ", "Lorem ipsum", "", []); //Act - var result = await _handler.Handle(command, default); + var result = await _handler.HandleAsync(command, default); //Assert result.IsError.Should().BeTrue(); @@ -57,11 +60,13 @@ public async void Handle_Should_NotCallSaveChangesAsync_WhenGeneratedIdIsEmpty() _mockDbContext.Setup(x => x.Articles).Returns(mockArticlesDbSet.Object); _mockSlugHelper.Setup(x => x.GenerateSlug(" ")).Returns(""); + _mockValidator.Setup(v => v.Validate(It.IsAny())) + .Returns(new ValidationResult()); var command = new CreateArticleCommand(" ", "Lorem ipsum", "", []); //Act - await _handler.Handle(command, default); + await _handler.HandleAsync(command, default); //Assert _mockDbContext.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Never); @@ -77,11 +82,13 @@ public async void Handle_Should_ReturnFailureResult_WhenGeneratedIdIsNotUnique() _mockDbContext.Setup(x => x.Articles).Returns(mockArticlesDbSet.Object); _mockSlugHelper.Setup(x => x.GenerateSlug("something cool")).Returns("something-cool"); + _mockValidator.Setup(v => v.Validate(It.IsAny())) + .Returns(new ValidationResult()); var command = new CreateArticleCommand("something cool", "Lorem ipsum", "", []); //Act - var result = await _handler.Handle(command, default); + var result = await _handler.HandleAsync(command, default); //Assert result.IsError.Should().BeTrue(); @@ -98,11 +105,13 @@ public async void Handle_Should_NotCallSaveChangesAsync_WhenGeneratedIdIsNotUniq _mockDbContext.Setup(x => x.Articles).Returns(mockArticlesDbSet.Object); _mockSlugHelper.Setup(x => x.GenerateSlug("something cool")).Returns("something-cool"); + _mockValidator.Setup(v => v.Validate(It.IsAny())) + .Returns(new ValidationResult()); var command = new CreateArticleCommand("something cool", "Lorem ipsum", "", []); //Act - var result = await _handler.Handle(command, default); + var result = await _handler.HandleAsync(command, default); //Assert _mockDbContext.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Never); @@ -126,10 +135,12 @@ public async void Handle_Should_CallSaveChangesAsync_WhenGeneratedIdIsUnique() _mockDbContext.Setup(x => x.Categories).Returns(mockCategoriesDbSet.Object); _mockDbContext.Setup(x => x.Revisions).Returns(mockRevisionsDbSet.Object); _mockSlugHelper.Setup(x => x.GenerateSlug("something extra cool")).Returns("something-extra-cool"); + _mockValidator.Setup(v => v.Validate(It.IsAny())) + .Returns(new ValidationResult()); var command = new CreateArticleCommand("something extra cool", "Lorem ipsum", "", []); //Act - await _handler.Handle(command, default); + await _handler.HandleAsync(command, default); //Assert _mockDbContext.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once); @@ -153,11 +164,13 @@ public async void Handle_Should_ReturnSuccessResultWithArticleId_WhenGeneratedId _mockDbContext.Setup(x => x.Categories).Returns(mockCategoriesDbSet.Object); _mockDbContext.Setup(x => x.Revisions).Returns(mockRevisionsDbSet.Object); _mockSlugHelper.Setup(x => x.GenerateSlug("something extra cool")).Returns("something-extra-cool"); + _mockValidator.Setup(v => v.Validate(It.IsAny())) + .Returns(new ValidationResult()); var command = new CreateArticleCommand("something extra cool", "Lorem ipsum", "", []); //Act - var result = await _handler.Handle(command, default); + var result = await _handler.HandleAsync(command, default); //Assert _mockDbContext.Verify(x => x.Articles.AddAsync( @@ -194,12 +207,14 @@ public async void Handle_Should_CreateRevisionWithContent_WhenGeneratedIdIsUniqu _mockDbContext.Setup(x => x.Revisions).Returns(mockRevisionsDbSet.Object); _mockSlugHelper.Setup(x => x.GenerateSlug("something extra cool")).Returns("something-extra-cool"); _mockCurrentUserService.Setup(x => x.UserId).Returns("test-user-id"); + _mockValidator.Setup(v => v.Validate(It.IsAny())) + .Returns(new ValidationResult()); var expectedCategoryIds = new List {"category1", "category2"}; var command = new CreateArticleCommand("something extra cool", "Lorem ipsum", "", ["category1", "category2", "category3"]); //Act - var result = await _handler.Handle(command, default); + var result = await _handler.HandleAsync(command, default); //Assert _mockDbContext.Verify(x => x.Revisions.AddAsync( @@ -215,39 +230,4 @@ public async void Handle_Should_CreateRevisionWithContent_WhenGeneratedIdIsUniqu Times.Once ); } - - [Fact] - public async void Handle_Should_PublishArticleCreatedEvent_WhenGeneratedIdIsUnique() - { - //Arrange - var mockArticlesDbSet = new List
{new() {Id = "something-cool", Title = "something-cool"}} - .AsQueryable() - .BuildMockDbSet(); - var mockCategoriesDbSet = new List() - .AsQueryable() - .BuildMockDbSet(); - var mockRevisionsDbSet = new List() - .AsQueryable() - .BuildMockDbSet(); - - _mockDbContext.Setup(x => x.Articles).Returns(mockArticlesDbSet.Object); - _mockDbContext.Setup(x => x.Categories).Returns(mockCategoriesDbSet.Object); - _mockDbContext.Setup(x => x.Revisions).Returns(mockRevisionsDbSet.Object); - _mockSlugHelper.Setup(x => x.GenerateSlug("something extra cool")).Returns("something-extra-cool"); - _mockCurrentUserService.Setup(x => x.UserId).Returns("test-user-id"); - var command = new CreateArticleCommand("something extra cool", "Lorem ipsum", "", []); - var expectedCategoryIds = new List {"category1", "category2"}; - - //Act - var result = await _handler.Handle(command, default); - - //Assert - _mockPublisher.Verify( - x => x.Publish( - It.Is(e => e.Id == result.Value.Id), - It.IsAny() - ), - Times.Once - ); - } } \ No newline at end of file diff --git a/Application/Application.csproj b/Application/Application.csproj index 7ad2828..7b41381 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -12,7 +12,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Application/Common/Behaviours/QueryCachingBehavior.cs b/Application/Common/Behaviours/QueryCachingBehavior.cs deleted file mode 100644 index 26754cb..0000000 --- a/Application/Common/Behaviours/QueryCachingBehavior.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Application.Common.Caching; -using Application.Common.Messaging; -using MediatR; - -namespace Application.Common.Behaviours; - -public class QueryCachingBehavior(ICacheService cacheService) : IPipelineBehavior - where TRequest : ICachedQuery -{ - public async Task Handle(TRequest request, RequestHandlerDelegate next, - CancellationToken token) - { - if (request.IgnoreCaching) return await next(); - return await cacheService.GetOrCreateAsync(request.Key, _ => next(), request.Expiration, token); - } -} \ No newline at end of file diff --git a/Application/Common/Behaviours/ValidationBehavior.cs b/Application/Common/Behaviours/ValidationBehavior.cs deleted file mode 100644 index da38cdf..0000000 --- a/Application/Common/Behaviours/ValidationBehavior.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Reflection; -using ErrorOr; -using FluentValidation; -using FluentValidation.Results; -using MediatR; -using ValidationException = Application.Common.Exceptions.ValidationException; - -namespace Application.Common.Behaviours; - -public class ValidationBehavior : IPipelineBehavior - where TRequest : IRequest - where TResponse : IErrorOr -{ - private readonly IValidator? _validator; - - public ValidationBehavior(IValidator? validator = null) - { - _validator = validator; - } - - public async Task Handle( - TRequest request, - RequestHandlerDelegate next, - CancellationToken token) - { - if (_validator == null) return await next(); - - var validationResult = await _validator.ValidateAsync(request, token); - - if (validationResult.IsValid) return await next(); - - return TryCreateResponseFromErrors(validationResult.Errors, out var response) - ? response - : throw new ValidationException(validationResult.Errors); - } - - private static bool TryCreateResponseFromErrors(List validationFailures, out TResponse response) - { - var errors = validationFailures.ConvertAll(x => Error.Validation( - x.PropertyName, - x.ErrorMessage)); - - response = (TResponse?) typeof(TResponse) - .GetMethod( - nameof(ErrorOr.From), - BindingFlags.Static | BindingFlags.Public, - new[] {typeof(List)})? - .Invoke(null, new[] {errors})!; - - return response is not null; - } -} \ No newline at end of file diff --git a/Application/Common/Messaging/ICachedQuery.cs b/Application/Common/Messaging/ICachedQuery.cs index 0222386..7efef32 100644 --- a/Application/Common/Messaging/ICachedQuery.cs +++ b/Application/Common/Messaging/ICachedQuery.cs @@ -3,10 +3,10 @@ namespace Application.Common.Messaging; public interface ICachedQuery : IQuery, ICachedQuery; public interface ICachedQuery -{ +{ string Key { get; } - + TimeSpan? Expiration { get; } - + bool IgnoreCaching { get; } } \ No newline at end of file diff --git a/Application/Common/Messaging/ICommand.cs b/Application/Common/Messaging/ICommand.cs new file mode 100644 index 0000000..01d217d --- /dev/null +++ b/Application/Common/Messaging/ICommand.cs @@ -0,0 +1,3 @@ +namespace Application.Common.Messaging; + +public interface ICommand; \ No newline at end of file diff --git a/Application/Common/Messaging/ICommandHandler.cs b/Application/Common/Messaging/ICommandHandler.cs new file mode 100644 index 0000000..be0c025 --- /dev/null +++ b/Application/Common/Messaging/ICommandHandler.cs @@ -0,0 +1,8 @@ +namespace Application.Common.Messaging; + +using ErrorOr; + +public interface ICommandHandler where TCommand : ICommand +{ + Task> HandleAsync(TCommand command, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Application/Common/Messaging/IQuery.cs b/Application/Common/Messaging/IQuery.cs index 1a935f3..9b559b1 100644 --- a/Application/Common/Messaging/IQuery.cs +++ b/Application/Common/Messaging/IQuery.cs @@ -1,6 +1,3 @@ -using ErrorOr; -using MediatR; - namespace Application.Common.Messaging; -public interface IQuery : IRequest>; \ No newline at end of file +public interface IQuery; \ No newline at end of file diff --git a/Application/Common/Messaging/IQueryHandler.cs b/Application/Common/Messaging/IQueryHandler.cs new file mode 100644 index 0000000..61f85a4 --- /dev/null +++ b/Application/Common/Messaging/IQueryHandler.cs @@ -0,0 +1,8 @@ +namespace Application.Common.Messaging; + +using ErrorOr; + +public interface IQueryHandler where TQuery : IQuery +{ + Task> HandleAsync(TQuery query, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Application/Common/Utils/CachingHelper.cs b/Application/Common/Utils/CachingHelper.cs new file mode 100644 index 0000000..9978ffb --- /dev/null +++ b/Application/Common/Utils/CachingHelper.cs @@ -0,0 +1,24 @@ +using Application.Common.Caching; +using Application.Common.Messaging; + +namespace Application.Common.Utils; + +public static class CachingHelper +{ + public static async Task GetOrCacheAsync( + ICacheService cacheService, + TRequest request, + Func> dataRetriever, + CancellationToken token) + where TRequest : ICachedQuery + { + if (request.IgnoreCaching) + return await dataRetriever(); + + return await cacheService.GetOrCreateAsync( + request.Key, + _ => dataRetriever(), + request.Expiration, + token); + } +} \ No newline at end of file diff --git a/Application/Common/Utils/ValidatorHelper.cs b/Application/Common/Utils/ValidatorHelper.cs new file mode 100644 index 0000000..65a4a99 --- /dev/null +++ b/Application/Common/Utils/ValidatorHelper.cs @@ -0,0 +1,16 @@ +using ErrorOr; +using FluentValidation; + +namespace Application.Common.Utils; + +public static class ValidatorHelper +{ + public static ErrorOr Validate(IValidator validator, TRequest request) + { + var validationResult = validator.Validate(request); + + return validationResult.IsValid + ? Result.Success + : validationResult.Errors.ConvertAll(e => Error.Validation(e.PropertyName, e.ErrorMessage)); + } +} \ No newline at end of file diff --git a/Application/Extensions/ServiceCollectionExt.cs b/Application/Extensions/ServiceCollectionExt.cs index d462950..13d3339 100644 --- a/Application/Extensions/ServiceCollectionExt.cs +++ b/Application/Extensions/ServiceCollectionExt.cs @@ -1,7 +1,26 @@ using System.Reflection; -using Application.Common.Behaviours; +using Application.Common.Messaging; +using Application.Features.Articles.CreateArticle; +using Application.Features.Articles.DeleteArticle; +using Application.Features.Articles.EditArticle; +using Application.Features.Articles.GetArticle; +using Application.Features.Articles.GetPendingRevisions; +using Application.Features.Articles.GetPendingRevisionsCount; +using Application.Features.Articles.GetRevisionHistory; +using Application.Features.Articles.GetRevisionReviewHistory; +using Application.Features.Articles.ReviewRevision; +using Application.Features.Articles.SearchArticles; +using Application.Features.Articles.SetRedirect; +using Application.Features.Authors.CreateAuthor; +using Application.Features.Authors.EditAuthor; +using Application.Features.Categories.CreateCategory; +using Application.Features.Categories.DeleteCategory; +using Application.Features.Categories.GetCategories; +using Application.Features.Categories.GetCategoriesTree; +using Application.Features.Categories.GetCategoryArticles; +using Application.Features.Navigations.GetNavigationTree; +using Application.Features.Navigations.UpdateNavigationsTree; using FluentValidation; -using MediatR; using Microsoft.Extensions.DependencyInjection; using Slugify; @@ -11,12 +30,33 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddApplicationServices(this IServiceCollection services) { - services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); - services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); - services.AddTransient(typeof(IPipelineBehavior<,>), typeof(QueryCachingBehavior<,>)); services.AddTransient(); + services.AddScoped, CreateArticleCommandHandler>(); + services.AddScoped, DeleteArticleCommandHandler>(); + services.AddScoped, EditArticleCommandHandler>(); + services.AddScoped, GetArticleQueryHandler>(); + services.AddScoped, GetPendingRevisionsQueryHandler>(); + services.AddScoped, GetPendingRevisionsCountQueryHandler>(); + services.AddScoped, GetRevisionHistoryQueryHandler>(); + services.AddScoped, GetRevisionReviewHistoryQueryHandler>(); + services.AddScoped, ReviewRevisionCommandHandler>(); + services.AddScoped, SearchArticlesQueryHandler>(); + services.AddScoped, SetRedirectCommandHandler>(); + + services.AddScoped, CreateAuthorCommandHandler>(); + services.AddScoped, EditAuthorCommandHandler>(); + + services.AddScoped, CreateCategoryCommandHandler>(); + services.AddScoped, DeleteCategoryCommandHandler>(); + services.AddScoped, GetCategoriesQueryHandler>(); + services.AddScoped, GetCategoriesTreeQueryHandler>(); + services.AddScoped, GetCategoryArticlesQueryHandler>(); + + services.AddScoped, GetNavigationsTreeQueryHandler>(); + services.AddScoped, UpdateNavigationsTreeCommandHandler>(); + return services; } } \ No newline at end of file diff --git a/Application/Features/Articles/CreateArticle/ArticleCreatedEvent.cs b/Application/Features/Articles/CreateArticle/ArticleCreatedEvent.cs deleted file mode 100644 index 3cc84fb..0000000 --- a/Application/Features/Articles/CreateArticle/ArticleCreatedEvent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using MediatR; - -namespace Application.Features.Articles.CreateArticle; - -public class ArticleCreatedEvent : INotification -{ - public required string Id { get; init; } - public required string Title { get; init; } - public required string Content { get; init; } - public required List CategoryIds { get; init; } -} \ No newline at end of file diff --git a/Application/Features/Articles/CreateArticle/CreateArticleCommand.cs b/Application/Features/Articles/CreateArticle/CreateArticleCommand.cs index b24d8c0..5a1505b 100644 --- a/Application/Features/Articles/CreateArticle/CreateArticleCommand.cs +++ b/Application/Features/Articles/CreateArticle/CreateArticleCommand.cs @@ -1,5 +1,4 @@ -using ErrorOr; -using MediatR; +using Application.Common.Messaging; namespace Application.Features.Articles.CreateArticle; @@ -8,4 +7,4 @@ public record CreateArticleCommand( string Content, string AuthorsNote, List CategoryIds -) : IRequest>; \ No newline at end of file +) : ICommand; \ No newline at end of file diff --git a/Application/Features/Articles/CreateArticle/CreateArticleCommandHandler.cs b/Application/Features/Articles/CreateArticle/CreateArticleCommandHandler.cs index 0cc6586..bbb2574 100644 --- a/Application/Features/Articles/CreateArticle/CreateArticleCommandHandler.cs +++ b/Application/Features/Articles/CreateArticle/CreateArticleCommandHandler.cs @@ -1,8 +1,10 @@ using Application.Authorization.Abstractions; +using Application.Common.Messaging; +using Application.Common.Utils; using Application.Data; using Domain.Entities; using ErrorOr; -using MediatR; +using FluentValidation; using Microsoft.EntityFrameworkCore; using Slugify; @@ -12,11 +14,17 @@ public class CreateArticleCommandHandler( IApplicationDbContext dbContext, ISlugHelper slugHelper, ICurrentUserService identityService, - IPublisher publisher -) : IRequestHandler> + IValidator validator +) : ICommandHandler { - public async Task> Handle(CreateArticleCommand command, CancellationToken token) + public async Task> HandleAsync(CreateArticleCommand command, CancellationToken token) { + var validationResult = ValidatorHelper.Validate(validator, command); + if (validationResult.IsError) + { + return validationResult.Errors; + } + var id = slugHelper.GenerateSlug(command.Title); if (string.IsNullOrEmpty(id)) return Errors.Article.EmptyId; @@ -52,15 +60,6 @@ public async Task> Handle(CreateArticleCommand co await dbContext.SaveChangesAsync(token); - var articleCreatedEvent = new ArticleCreatedEvent - { - Id = article.Id, - Title = article.Title, - Content = revision.Content, - CategoryIds = categories.Select(e => e.Id).ToList() - }; - await publisher.Publish(articleCreatedEvent, token); - return new CreateArticleResponse(article.Id); } } \ No newline at end of file diff --git a/Application/Features/Articles/DeleteArticle/ArticleDeletedEvent.cs b/Application/Features/Articles/DeleteArticle/ArticleDeletedEvent.cs deleted file mode 100644 index 88a466a..0000000 --- a/Application/Features/Articles/DeleteArticle/ArticleDeletedEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MediatR; - -namespace Application.Features.Articles.DeleteArticle; - -public class ArticleDeletedEvent : INotification -{ - public required string Id { get; init; } -} \ No newline at end of file diff --git a/Application/Features/Articles/DeleteArticle/DeleteArticleCommand.cs b/Application/Features/Articles/DeleteArticle/DeleteArticleCommand.cs index aae45ca..52285da 100644 --- a/Application/Features/Articles/DeleteArticle/DeleteArticleCommand.cs +++ b/Application/Features/Articles/DeleteArticle/DeleteArticleCommand.cs @@ -1,6 +1,5 @@ -using ErrorOr; -using MediatR; +using Application.Common.Messaging; namespace Application.Features.Articles.DeleteArticle; -public record DeleteArticleCommand(string Id) : IRequest>; \ No newline at end of file +public record DeleteArticleCommand(string Id) : ICommand; \ No newline at end of file diff --git a/Application/Features/Articles/DeleteArticle/DeleteArticleCommandHandler.cs b/Application/Features/Articles/DeleteArticle/DeleteArticleCommandHandler.cs index 1403eb7..4e5e532 100644 --- a/Application/Features/Articles/DeleteArticle/DeleteArticleCommandHandler.cs +++ b/Application/Features/Articles/DeleteArticle/DeleteArticleCommandHandler.cs @@ -1,23 +1,22 @@ -using Application.Data; +using Application.Common.Caching; +using Application.Common.Constants; +using Application.Common.Messaging; +using Application.Data; using ErrorOr; -using MediatR; namespace Application.Features.Articles.DeleteArticle; -public class DeleteArticleCommandHandler( - IApplicationDbContext dbContext, - IPublisher publisher -) : IRequestHandler> +public class DeleteArticleCommandHandler(IApplicationDbContext dbContext, ICacheService cacheService) + : ICommandHandler { - public async Task> Handle(DeleteArticleCommand request, CancellationToken token) + public async Task> HandleAsync(DeleteArticleCommand request, CancellationToken token) { var article = await dbContext.Articles.FindAsync(new object[] {request.Id}, token); if (article == null) return Errors.Article.NotFound; dbContext.Articles.Remove(article); await dbContext.SaveChangesAsync(token); - var articleDeletedEvent = new ArticleDeletedEvent {Id = article.Id}; - await publisher.Publish(articleDeletedEvent, token); + await cacheService.RemoveAsync(CachingKeys.Articles.ArticleById(article.Id), token); return new DeleteArticleResponse(article.Id); } diff --git a/Application/Features/Articles/EditArticle/ArticleEditedEvent.cs b/Application/Features/Articles/EditArticle/ArticleEditedEvent.cs deleted file mode 100644 index 806c4ab..0000000 --- a/Application/Features/Articles/EditArticle/ArticleEditedEvent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using MediatR; - -namespace Application.Features.Articles.EditArticle; - -public class ArticleEditedEvent : INotification -{ - public required string Id { get; init; } - public required string Content { get; init; } - public required string AuthorsNote { get; init; } - public required List CategoryIds { get; init; } -} \ No newline at end of file diff --git a/Application/Features/Articles/EditArticle/EditArticleCommand.cs b/Application/Features/Articles/EditArticle/EditArticleCommand.cs index 0da2309..b345d7f 100644 --- a/Application/Features/Articles/EditArticle/EditArticleCommand.cs +++ b/Application/Features/Articles/EditArticle/EditArticleCommand.cs @@ -1,5 +1,4 @@ -using ErrorOr; -using MediatR; +using Application.Common.Messaging; namespace Application.Features.Articles.EditArticle; @@ -8,4 +7,4 @@ public record EditArticleCommand( string Content, string AuthorsNote, List CategoryIds -) : IRequest>; \ No newline at end of file +): ICommand; \ No newline at end of file diff --git a/Application/Features/Articles/EditArticle/EditArticleCommandHandler.cs b/Application/Features/Articles/EditArticle/EditArticleCommandHandler.cs index ed2ff4f..e2fd8c9 100644 --- a/Application/Features/Articles/EditArticle/EditArticleCommandHandler.cs +++ b/Application/Features/Articles/EditArticle/EditArticleCommandHandler.cs @@ -1,8 +1,10 @@ using Application.Authorization.Abstractions; +using Application.Common.Messaging; +using Application.Common.Utils; using Application.Data; using Domain.Entities; using ErrorOr; -using MediatR; +using FluentValidation; using Microsoft.EntityFrameworkCore; namespace Application.Features.Articles.EditArticle; @@ -10,11 +12,17 @@ namespace Application.Features.Articles.EditArticle; public class EditArticleCommandHandler( IApplicationDbContext dbContext, ICurrentUserService identityService, - IPublisher publisher -) : IRequestHandler> + IValidator validator +) : ICommandHandler { - public async Task> Handle(EditArticleCommand request, CancellationToken token) + public async Task> HandleAsync(EditArticleCommand request, CancellationToken token) { + var validationResult = ValidatorHelper.Validate(validator, request); + if (validationResult.IsError) + { + return validationResult.Errors; + } + var article = await dbContext.Articles.Include(e => e.RedirectArticle) .FirstOrDefaultAsync(e => e.Id == request.Id, token); if (article == null) return Errors.Article.NotFound; @@ -41,15 +49,6 @@ public async Task> Handle(EditArticleCommand reques await dbContext.Revisions.AddAsync(revision, token); await dbContext.SaveChangesAsync(token); - var articleEditedEvent = new ArticleEditedEvent - { - Id = article.Id, - Content = revision.Content, - AuthorsNote = request.AuthorsNote, - CategoryIds = requestCategories.Select(e => e.Id).ToList() - }; - await publisher.Publish(articleEditedEvent, token); - return new EditArticleResponse(article.Id); } } \ No newline at end of file diff --git a/Application/Features/Articles/EventHandlers/CacheInvalidationArticleHandler.cs b/Application/Features/Articles/EventHandlers/CacheInvalidationArticleHandler.cs deleted file mode 100644 index b5b1b30..0000000 --- a/Application/Features/Articles/EventHandlers/CacheInvalidationArticleHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Application.Common.Caching; -using Application.Common.Constants; -using Application.Features.Articles.DeleteArticle; -using Application.Features.Articles.ReviewRevision; -using Application.Features.Articles.SetRedirect; -using MediatR; - -namespace Application.Features.Articles.EventHandlers; - -public class CacheInvalidationArticleHandler(ICacheService cacheService) : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler -{ - public async Task Handle(ArticleDeletedEvent notification, CancellationToken token) - { - await cacheService.RemoveAsync(CachingKeys.Articles.ArticleById(notification.Id), token); - } - - public async Task Handle(RevisionReviewedEvent notification, CancellationToken token) - { - await cacheService.RemoveAsync(CachingKeys.Articles.ArticleById(notification.ArticleId), token); - } - - public async Task Handle(RedirectSetEvent notification, CancellationToken token) - { - await cacheService.RemoveAsync(CachingKeys.Articles.ArticleById(notification.ArticleId), token); - await cacheService.RemoveAsync(CachingKeys.Articles.ArticleById(notification.RedirectId), token); - } - - public async Task Handle(ArticleChangedRevisionEvent notification, CancellationToken token) - { - var difference = notification.PreviousRevisionCategoryIds.Except(notification.CurrentRevisionCategoryIds) - .Concat(notification.CurrentRevisionCategoryIds.Except(notification.PreviousRevisionCategoryIds)); - foreach (var s in difference) - { - await cacheService.RemoveAsync(CachingKeys.Categories.CategoryArticlesById(s), token); - } - } -} \ No newline at end of file diff --git a/Application/Features/Articles/GetArticle/GetArticleQueryHandler.cs b/Application/Features/Articles/GetArticle/GetArticleQueryHandler.cs index 77236d5..507767f 100644 --- a/Application/Features/Articles/GetArticle/GetArticleQueryHandler.cs +++ b/Application/Features/Articles/GetArticle/GetArticleQueryHandler.cs @@ -1,24 +1,38 @@ using Application.Authorization.Abstractions; +using Application.Common.Caching; using Application.Common.Constants; +using Application.Common.Messaging; +using Application.Common.Utils; using Application.Data; using Domain.Entities; using ErrorOr; -using MediatR; +using FluentValidation; using Microsoft.EntityFrameworkCore; namespace Application.Features.Articles.GetArticle; public class GetArticleQueryHandler( IApplicationDbContext dbContext, - IIdentityService identityService -) : IRequestHandler> + IIdentityService identityService, + IValidator validator, + ICacheService cacheService +) : IQueryHandler { - public async Task> Handle(GetArticleQuery query, CancellationToken token) + public async Task> HandleAsync(GetArticleQuery query, CancellationToken token) { - if (!string.IsNullOrWhiteSpace(query.Id)) - return await GetByArticleId(query.Id, token); - - return await GetByRevisionId(query.RevisionId.GetValueOrDefault(), token); + var validationResult = ValidatorHelper.Validate(validator, query); + if (validationResult.IsError) + { + return validationResult.Errors; + } + + return await CachingHelper.GetOrCacheAsync(cacheService, query, async () => + { + if (!string.IsNullOrWhiteSpace(query.Id)) + return await GetByArticleId(query.Id, token); + + return await GetByRevisionId(query.RevisionId.GetValueOrDefault(), token); + }, token); } private async Task> GetByArticleId(string id, CancellationToken token) @@ -26,7 +40,7 @@ private async Task> GetByArticleId(string id, Cancel var article = await dbContext.Articles .Include(e => e.CurrentRevision) .Include(e => e.RedirectArticle) - .ThenInclude(e => e.CurrentRevision) + .ThenInclude(e => e!.CurrentRevision) .AsNoTracking() .FirstOrDefaultAsync(e => e.Id == id, token); diff --git a/Application/Features/Articles/GetPendingRevisions/GetPendingRevisionsQueryHandler.cs b/Application/Features/Articles/GetPendingRevisions/GetPendingRevisionsQueryHandler.cs index 173c01e..9ccd6d3 100644 --- a/Application/Features/Articles/GetPendingRevisions/GetPendingRevisionsQueryHandler.cs +++ b/Application/Features/Articles/GetPendingRevisions/GetPendingRevisionsQueryHandler.cs @@ -1,16 +1,14 @@ +using Application.Common.Messaging; using Application.Data; -using Domain.Entities; using ErrorOr; -using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Features.Articles.GetPendingRevisions; -public class GetPendingRevisionsQueryHandler - (IApplicationDbContext dbContext) : IRequestHandler> +public class GetPendingRevisionsQueryHandler(IApplicationDbContext dbContext) + : IQueryHandler { - public async Task> Handle(GetPendingRevisionsQuery query, - CancellationToken token) + public async Task> HandleAsync(GetPendingRevisionsQuery query, CancellationToken token) { var pendingRevisions = await dbContext.Revisions .Where(e => e.LatestReviewId == null) diff --git a/Application/Features/Articles/GetPendingRevisionsCount/GetPendingRevisionsQueryHandler.cs b/Application/Features/Articles/GetPendingRevisionsCount/GetPendingRevisionsQueryHandler.cs index 98d2671..26be007 100644 --- a/Application/Features/Articles/GetPendingRevisionsCount/GetPendingRevisionsQueryHandler.cs +++ b/Application/Features/Articles/GetPendingRevisionsCount/GetPendingRevisionsQueryHandler.cs @@ -1,14 +1,14 @@ +using Application.Common.Messaging; using Application.Data; using ErrorOr; -using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Features.Articles.GetPendingRevisionsCount; -public class GetPendingRevisionsCountQueryHandler - (IApplicationDbContext dbContext) : IRequestHandler> +public class GetPendingRevisionsCountQueryHandler(IApplicationDbContext dbContext) + : IQueryHandler { - public async Task> Handle(GetPendingRevisionsCountQuery query, + public async Task> HandleAsync(GetPendingRevisionsCountQuery query, CancellationToken token) { var pendingRevisionsCount = await dbContext.Revisions diff --git a/Application/Features/Articles/GetRevisionHistory/GetRevisionHistoryQueryHandler.cs b/Application/Features/Articles/GetRevisionHistory/GetRevisionHistoryQueryHandler.cs index dbbd068..3d009fd 100644 --- a/Application/Features/Articles/GetRevisionHistory/GetRevisionHistoryQueryHandler.cs +++ b/Application/Features/Articles/GetRevisionHistory/GetRevisionHistoryQueryHandler.cs @@ -1,14 +1,14 @@ +using Application.Common.Messaging; using Application.Data; using ErrorOr; -using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Features.Articles.GetRevisionHistory; -public class GetRevisionHistoryQueryHandler - (IApplicationDbContext dbContext) : IRequestHandler> +public class GetRevisionHistoryQueryHandler(IApplicationDbContext dbContext) + : IQueryHandler { - public async Task> Handle(GetRevisionHistoryQuery query, + public async Task> HandleAsync(GetRevisionHistoryQuery query, CancellationToken token) { var article = await dbContext.Articles @@ -18,12 +18,12 @@ public async Task> Handle(GetRevisionHistory if (article == null) return Errors.Article.NotFound; - if (article.RedirectArticle != null) + if (article.RedirectArticle != null) article = article.RedirectArticle; var revisions = await dbContext.Revisions .Where(e => e.ArticleId == article.Id) - .OrderByDescending(e=>e.Timestamp) + .OrderByDescending(e => e.Timestamp) .Select(e => new GetRevisionHistoryResponse.Element( e.Id, new GetRevisionHistoryResponse.Author(e.Author.Id, e.Author.Name), diff --git a/Application/Features/Articles/GetRevisionReviewHistory/GetRevisionReviewHistoryQueryHandler.cs b/Application/Features/Articles/GetRevisionReviewHistory/GetRevisionReviewHistoryQueryHandler.cs index aa0e19e..11af175 100644 --- a/Application/Features/Articles/GetRevisionReviewHistory/GetRevisionReviewHistoryQueryHandler.cs +++ b/Application/Features/Articles/GetRevisionReviewHistory/GetRevisionReviewHistoryQueryHandler.cs @@ -1,15 +1,14 @@ +using Application.Common.Messaging; using Application.Data; using ErrorOr; -using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Features.Articles.GetRevisionReviewHistory; -public class GetRevisionReviewHistoryQueryHandler - (IApplicationDbContext dbContext) : IRequestHandler> +public class GetRevisionReviewHistoryQueryHandler(IApplicationDbContext dbContext) + : IQueryHandler { - public async Task> Handle(GetRevisionReviewHistoryQuery query, + public async Task> HandleAsync(GetRevisionReviewHistoryQuery query, CancellationToken token) { if (!await dbContext.Revisions.AnyAsync(e => e.Id == query.Id, token)) return Errors.Revision.NotFound; diff --git a/Application/Features/Articles/ReviewRevision/ArticleChangedRevisionEvent.cs b/Application/Features/Articles/ReviewRevision/ArticleChangedRevisionEvent.cs deleted file mode 100644 index ef07de2..0000000 --- a/Application/Features/Articles/ReviewRevision/ArticleChangedRevisionEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -using MediatR; - -namespace Application.Features.Articles.ReviewRevision; - -public class ArticleChangedRevisionEvent : INotification -{ - public required string ArticleId { get; init; } - public required Guid? PreviousRevisionId { get; init; } - public required Guid? CurrentRevisionId { get; init; } - public required List PreviousRevisionCategoryIds { get; init; } - public required List CurrentRevisionCategoryIds { get; init; } -} \ No newline at end of file diff --git a/Application/Features/Articles/ReviewRevision/ReviewRevisionCommand.cs b/Application/Features/Articles/ReviewRevision/ReviewRevisionCommand.cs index 75fd35a..b6a2527 100644 --- a/Application/Features/Articles/ReviewRevision/ReviewRevisionCommand.cs +++ b/Application/Features/Articles/ReviewRevision/ReviewRevisionCommand.cs @@ -1,12 +1,10 @@ +using Application.Common.Messaging; using Domain.Entities; -using ErrorOr; -using MediatR; namespace Application.Features.Articles.ReviewRevision; public record ReviewRevisionCommand( - Guid RevisionId, - ReviewStatus Status, - string Review - ) - : IRequest>; \ No newline at end of file + Guid RevisionId, + ReviewStatus Status, + string Review +): ICommand; \ No newline at end of file diff --git a/Application/Features/Articles/ReviewRevision/ReviewRevisionCommandHandler.cs b/Application/Features/Articles/ReviewRevision/ReviewRevisionCommandHandler.cs index 7da7b22..9f44fa1 100644 --- a/Application/Features/Articles/ReviewRevision/ReviewRevisionCommandHandler.cs +++ b/Application/Features/Articles/ReviewRevision/ReviewRevisionCommandHandler.cs @@ -1,8 +1,12 @@ using Application.Authorization.Abstractions; +using Application.Common.Caching; +using Application.Common.Constants; +using Application.Common.Messaging; +using Application.Common.Utils; using Application.Data; using Domain.Entities; using ErrorOr; -using MediatR; +using FluentValidation; using Microsoft.EntityFrameworkCore; namespace Application.Features.Articles.ReviewRevision; @@ -10,16 +14,23 @@ namespace Application.Features.Articles.ReviewRevision; public class ReviewRevisionCommandHandler( IApplicationDbContext dbContext, ICurrentUserService identityService, - IPublisher publisher -) : IRequestHandler> + ICacheService cacheService, + IValidator validator +) : ICommandHandler { - public async Task> Handle(ReviewRevisionCommand command, + public async Task> HandleAsync(ReviewRevisionCommand command, CancellationToken token) { + var validationResult = ValidatorHelper.Validate(validator, command); + if (validationResult.IsError) + { + return validationResult.Errors; + } + var revision = await dbContext.Revisions .Include(e => e.Article) .ThenInclude(e => e.CurrentRevision) - .ThenInclude(e => e.Categories) + .ThenInclude(e => e!.Categories) .Include(e => e.Categories) .Include(e => e.LatestReview) .FirstOrDefaultAsync(e => e.Id == command.RevisionId, token); @@ -40,7 +51,7 @@ public async Task> Handle(ReviewRevisionCommand Revision = revision }; - ArticleChangedRevisionEvent? articleChangedRevisionEvent = null; + HashSet affectedCategories = []; revision.LatestReview = review; @@ -49,7 +60,7 @@ public async Task> Handle(ReviewRevisionCommand revision.Content = "[REDACTED]"; revision.AuthorsNote = "[REDACTED]"; } - + if (article.CurrentRevision == revision && command is {Status: ReviewStatus.Removed or ReviewStatus.Rejected}) { var rollbackRevision = await dbContext.Revisions @@ -60,42 +71,34 @@ public async Task> Handle(ReviewRevisionCommand .OrderByDescending(e => e.Timestamp) .FirstOrDefaultAsync(token); - ChangeArticleRevision(article, rollbackRevision, out articleChangedRevisionEvent); + ChangeArticleRevision(article, rollbackRevision, out affectedCategories); } if (command is {Status: ReviewStatus.Accepted}) { - ChangeArticleRevision(article, revision, out articleChangedRevisionEvent); + ChangeArticleRevision(article, revision, out affectedCategories); } await dbContext.SaveChangesAsync(token); - - var revisionReviewedEvent = new RevisionReviewedEvent + + await cacheService.RemoveAsync(CachingKeys.Articles.ArticleById(article.Id), token); + foreach (var id in affectedCategories) { - ArticleId = article.Id, - RevisionId = revision.Id, - Status = command.Status, - Review = command.Review, - }; - await publisher.Publish(revisionReviewedEvent, token); - - if (articleChangedRevisionEvent != null) await publisher.Publish(articleChangedRevisionEvent, token); + await cacheService.RemoveAsync(CachingKeys.Categories.CategoryArticlesById(id), token); + } return new ReviewRevisionResponse(review.Id); } - private static void ChangeArticleRevision(Article article, Revision? revision, out ArticleChangedRevisionEvent articleChangedRevisionEvent) + private static void ChangeArticleRevision(Article article, Revision? revision, out HashSet affectedCategories) { - var previousRevisionId = article.CurrentRevision?.Id; - var previousRevisionCategoriesIds = article.CurrentRevision?.Categories.Select(e => e.Id).ToList() ?? new List(); + var previousRevisionCategoriesIds = article.CurrentRevision?.Categories.Select(e => e.Id).ToHashSet() ?? []; + article.CurrentRevision = revision; - articleChangedRevisionEvent = new ArticleChangedRevisionEvent - { - ArticleId = article.Id, - PreviousRevisionId = previousRevisionId, - CurrentRevisionId = article.CurrentRevision?.Id, - PreviousRevisionCategoryIds = previousRevisionCategoriesIds, - CurrentRevisionCategoryIds = revision?.Categories.Select(e => e.Id).ToList() ?? new List() - }; + + var currentRevisionCategoryIds = revision?.Categories.Select(e => e.Id).ToHashSet() ?? []; + + previousRevisionCategoriesIds.SymmetricExceptWith(currentRevisionCategoryIds); + affectedCategories = previousRevisionCategoriesIds; } } \ No newline at end of file diff --git a/Application/Features/Articles/ReviewRevision/RevisionReviewedEvent.cs b/Application/Features/Articles/ReviewRevision/RevisionReviewedEvent.cs deleted file mode 100644 index 1dc3d38..0000000 --- a/Application/Features/Articles/ReviewRevision/RevisionReviewedEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Domain.Entities; -using MediatR; - -namespace Application.Features.Articles.ReviewRevision; - -public class RevisionReviewedEvent : INotification -{ - public required string ArticleId { get; init; } - public required Guid RevisionId { get; init; } - public required ReviewStatus Status { get; init; } - public required string Review { get; init; } -} \ No newline at end of file diff --git a/Application/Features/Articles/SearchArticles/SearchArticlesQueryHandler.cs b/Application/Features/Articles/SearchArticles/SearchArticlesQueryHandler.cs index bc624a8..ec28160 100644 --- a/Application/Features/Articles/SearchArticles/SearchArticlesQueryHandler.cs +++ b/Application/Features/Articles/SearchArticles/SearchArticlesQueryHandler.cs @@ -1,14 +1,14 @@ +using Application.Common.Messaging; using Application.Data; using ErrorOr; -using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Features.Articles.SearchArticles; -public class SearchArticlesQueryHandler - (IApplicationDbContext dbContext) : IRequestHandler> +public class SearchArticlesQueryHandler(IApplicationDbContext dbContext) + : IQueryHandler { - public async Task> Handle(SearchArticlesQuery query, CancellationToken token) + public async Task> HandleAsync(SearchArticlesQuery query, CancellationToken token) { var articles = dbContext.Articles .Where(e => e.RedirectArticleId == null && e.CurrentRevisionId != null); @@ -16,18 +16,18 @@ public async Task> Handle(SearchArticlesQuery qu if (!string.IsNullOrWhiteSpace(query.SearchTerm)) { articles = articles.Where(e => EF.Functions.ILike(e.Title, $"%{query.SearchTerm}%")) - .OrderByDescending(e => EF.Functions.TrigramsSimilarity(e.Title, query.SearchTerm)); + .OrderByDescending(e => EF.Functions.TrigramsSimilarity(e.Title, query.SearchTerm)); } - + var totalCount = await articles.AsNoTracking().CountAsync(token); - + var pagedArticles = await articles - .Skip((query.Page-1) * query.PageSize) + .Skip((query.Page - 1) * query.PageSize) .Take(query.PageSize) - .Select(e=> new SearchArticlesResponse.Element(e.Id, e.Title)) + .Select(e => new SearchArticlesResponse.Element(e.Id, e.Title)) .AsNoTracking() .ToListAsync(token); - + return new SearchArticlesResponse(query.Page, pagedArticles.Count, totalCount, pagedArticles); } } \ No newline at end of file diff --git a/Application/Features/Articles/SetRedirect/RedirectSetEvent.cs b/Application/Features/Articles/SetRedirect/RedirectSetEvent.cs deleted file mode 100644 index a6beedd..0000000 --- a/Application/Features/Articles/SetRedirect/RedirectSetEvent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using MediatR; - -namespace Application.Features.Articles.SetRedirect; - -public class RedirectSetEvent : INotification -{ - public required string ArticleId { get; init; } - public required string RedirectId { get; init; } -} \ No newline at end of file diff --git a/Application/Features/Articles/SetRedirect/SetRedirectCommand.cs b/Application/Features/Articles/SetRedirect/SetRedirectCommand.cs index f877d67..9635341 100644 --- a/Application/Features/Articles/SetRedirect/SetRedirectCommand.cs +++ b/Application/Features/Articles/SetRedirect/SetRedirectCommand.cs @@ -1,7 +1,5 @@ -using Domain.Entities; -using ErrorOr; -using MediatR; +using Application.Common.Messaging; namespace Application.Features.Articles.SetRedirect; -public record SetRedirectCommand(string ArticleId, string RedirectId) : IRequest>; \ No newline at end of file +public record SetRedirectCommand(string ArticleId, string RedirectId) : ICommand; \ No newline at end of file diff --git a/Application/Features/Articles/SetRedirect/SetRedirectCommandHandler.cs b/Application/Features/Articles/SetRedirect/SetRedirectCommandHandler.cs index ea84f46..23c1ca4 100644 --- a/Application/Features/Articles/SetRedirect/SetRedirectCommandHandler.cs +++ b/Application/Features/Articles/SetRedirect/SetRedirectCommandHandler.cs @@ -1,20 +1,28 @@ -using Application.Authorization.Abstractions; +using Application.Common.Caching; +using Application.Common.Constants; +using Application.Common.Messaging; +using Application.Common.Utils; using Application.Data; -using Domain.Entities; using ErrorOr; -using MediatR; +using FluentValidation; using Microsoft.EntityFrameworkCore; namespace Application.Features.Articles.SetRedirect; public class SetRedirectCommandHandler( IApplicationDbContext dbContext, - IPublisher publisher -) : IRequestHandler> + ICacheService cacheService, + IValidator validator +) : ICommandHandler { - public async Task> Handle(SetRedirectCommand command, - CancellationToken token) + public async Task> HandleAsync(SetRedirectCommand command, CancellationToken token) { + var validationResult = ValidatorHelper.Validate(validator, command); + if (validationResult.IsError) + { + return validationResult.Errors; + } + var article = await dbContext.Articles.FirstOrDefaultAsync(e => e.Id == command.ArticleId, token); if (article == null) return Errors.Article.NotFound; if (article.RedirectArticleId != null) return Errors.Article.RedirectExists; @@ -37,12 +45,8 @@ await dbContext.Revisions await dbContext.SaveChangesAsync(token); - var revisionReviewedEvent = new RedirectSetEvent - { - ArticleId = article.Id, - RedirectId = redirectArticle.Id, - }; - await publisher.Publish(revisionReviewedEvent, token); + await cacheService.RemoveAsync(CachingKeys.Articles.ArticleById(article.Id), token); + await cacheService.RemoveAsync(CachingKeys.Articles.ArticleById(redirectArticle.Id), token); return new SetRedirectResponse(redirectArticle.Id); } diff --git a/Application/Features/Authors/CreateAuthor/AuthorCreatedEvent.cs b/Application/Features/Authors/CreateAuthor/AuthorCreatedEvent.cs deleted file mode 100644 index 4f7ec71..0000000 --- a/Application/Features/Authors/CreateAuthor/AuthorCreatedEvent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using MediatR; - -namespace Application.Features.Authors.CreateAuthor; - -public class AuthorCreatedEvent : INotification -{ - public required string Id { get; init; } - public required string Name { get; init; } -} \ No newline at end of file diff --git a/Application/Features/Authors/CreateAuthor/CreateAuthorCommand.cs b/Application/Features/Authors/CreateAuthor/CreateAuthorCommand.cs index be92681..c856b17 100644 --- a/Application/Features/Authors/CreateAuthor/CreateAuthorCommand.cs +++ b/Application/Features/Authors/CreateAuthor/CreateAuthorCommand.cs @@ -1,6 +1,5 @@ -using ErrorOr; -using MediatR; +using Application.Common.Messaging; namespace Application.Features.Authors.CreateAuthor; -public record CreateAuthorCommand(string Id, string Name) : IRequest>; \ No newline at end of file +public record CreateAuthorCommand(string Id, string Name) : ICommand; \ No newline at end of file diff --git a/Application/Features/Authors/CreateAuthor/CreateAuthorCommandHandler.cs b/Application/Features/Authors/CreateAuthor/CreateAuthorCommandHandler.cs index 11f271c..db373b2 100644 --- a/Application/Features/Authors/CreateAuthor/CreateAuthorCommandHandler.cs +++ b/Application/Features/Authors/CreateAuthor/CreateAuthorCommandHandler.cs @@ -1,16 +1,15 @@ -using Application.Data; +using Application.Common.Messaging; +using Application.Data; using Domain.Entities; using ErrorOr; -using MediatR; namespace Application.Features.Authors.CreateAuthor; public class CreateAuthorCommandHandler( - IApplicationDbContext dbContext, - IPublisher publisher -) : IRequestHandler> + IApplicationDbContext dbContext +) : ICommandHandler { - public async Task> Handle(CreateAuthorCommand command, CancellationToken token) + public async Task> HandleAsync(CreateAuthorCommand command, CancellationToken token) { var author = new Author {Id = command.Id, Name = command.Name}; @@ -20,13 +19,6 @@ public async Task> Handle(CreateAuthorCommand comm dbContext.Authors.Add(author); await dbContext.SaveChangesAsync(token); - var authorCreatedEvent = new AuthorCreatedEvent - { - Id = author.Id, - Name = author.Name - }; - await publisher.Publish(authorCreatedEvent, token); - return new CreateAuthorResponse(author.Id); } } \ No newline at end of file diff --git a/Application/Features/Authors/EditAuthor/AuthorEditedEvent.cs b/Application/Features/Authors/EditAuthor/AuthorEditedEvent.cs deleted file mode 100644 index af61e18..0000000 --- a/Application/Features/Authors/EditAuthor/AuthorEditedEvent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using MediatR; - -namespace Application.Features.Authors.EditAuthor; - -public class AuthorEditedEvent : INotification -{ - public required string Id { get; init; } - public required string Name { get; init; } -} \ No newline at end of file diff --git a/Application/Features/Authors/EditAuthor/EditAuthorCommand.cs b/Application/Features/Authors/EditAuthor/EditAuthorCommand.cs index d1059d4..025a41c 100644 --- a/Application/Features/Authors/EditAuthor/EditAuthorCommand.cs +++ b/Application/Features/Authors/EditAuthor/EditAuthorCommand.cs @@ -1,6 +1,5 @@ -using ErrorOr; -using MediatR; +using Application.Common.Messaging; namespace Application.Features.Authors.EditAuthor; -public record EditAuthorCommand(string Id, string Name) : IRequest>; \ No newline at end of file +public record EditAuthorCommand(string Id, string Name) : ICommand; \ No newline at end of file diff --git a/Application/Features/Authors/EditAuthor/EditAuthorCommandHandler.cs b/Application/Features/Authors/EditAuthor/EditAuthorCommandHandler.cs index ed0e35c..2cffbcd 100644 --- a/Application/Features/Authors/EditAuthor/EditAuthorCommandHandler.cs +++ b/Application/Features/Authors/EditAuthor/EditAuthorCommandHandler.cs @@ -1,16 +1,15 @@ -using Application.Data; +using Application.Common.Messaging; +using Application.Data; using ErrorOr; -using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Features.Authors.EditAuthor; public class EditAuthorCommandHandler( - IApplicationDbContext dbContext, - IPublisher publisher -) : IRequestHandler> + IApplicationDbContext dbContext +) : ICommandHandler { - public async Task> Handle(EditAuthorCommand command, CancellationToken token) + public async Task> HandleAsync(EditAuthorCommand command, CancellationToken token) { var author = await dbContext.Authors.FirstOrDefaultAsync(e => e.Id == command.Id, token); @@ -20,13 +19,6 @@ public async Task> Handle(EditAuthorCommand command, await dbContext.SaveChangesAsync(token); - var authorEditedEvent = new AuthorEditedEvent - { - Id = author.Id, - Name = author.Name - }; - await publisher.Publish(authorEditedEvent, token); - return new EditAuthorResponse(author.Id); } } \ No newline at end of file diff --git a/Application/Features/Categories/CreateCategory/CategoryCreatedEvent.cs b/Application/Features/Categories/CreateCategory/CategoryCreatedEvent.cs deleted file mode 100644 index f113523..0000000 --- a/Application/Features/Categories/CreateCategory/CategoryCreatedEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediatR; - -namespace Application.Features.Categories.CreateCategory; - -public class CategoryCreatedEvent : INotification -{ - public required string Id { get; init; } - public required string Name { get; init; } - public string? ParentId { get; init; } -} \ No newline at end of file diff --git a/Application/Features/Categories/CreateCategory/CreateCategoryCommand.cs b/Application/Features/Categories/CreateCategory/CreateCategoryCommand.cs index 55a873b..9c35bb6 100644 --- a/Application/Features/Categories/CreateCategory/CreateCategoryCommand.cs +++ b/Application/Features/Categories/CreateCategory/CreateCategoryCommand.cs @@ -1,6 +1,5 @@ -using ErrorOr; -using MediatR; +using Application.Common.Messaging; namespace Application.Features.Categories.CreateCategory; -public record CreateCategoryCommand(string Name, string? ParentId) : IRequest>; \ No newline at end of file +public record CreateCategoryCommand(string Name, string? ParentId) : ICommand; \ No newline at end of file diff --git a/Application/Features/Categories/CreateCategory/CreateCategoryCommandHandler.cs b/Application/Features/Categories/CreateCategory/CreateCategoryCommandHandler.cs index 6879880..69a1d62 100644 --- a/Application/Features/Categories/CreateCategory/CreateCategoryCommandHandler.cs +++ b/Application/Features/Categories/CreateCategory/CreateCategoryCommandHandler.cs @@ -1,7 +1,11 @@ -using Application.Data; +using Application.Common.Caching; +using Application.Common.Constants; +using Application.Common.Messaging; +using Application.Common.Utils; +using Application.Data; using Domain.Entities; using ErrorOr; -using MediatR; +using FluentValidation; using Microsoft.EntityFrameworkCore; using Slugify; @@ -10,11 +14,18 @@ namespace Application.Features.Categories.CreateCategory; public class CreateCategoryCommandHandler( IApplicationDbContext dbContext, ISlugHelper slugHelper, - IPublisher publisher -) : IRequestHandler> + ICacheService cacheService, + IValidator validator +) : ICommandHandler { - public async Task> Handle(CreateCategoryCommand command, CancellationToken token) + public async Task> HandleAsync(CreateCategoryCommand command, CancellationToken token) { + var validationResult = ValidatorHelper.Validate(validator, command); + if (validationResult.IsError) + { + return validationResult.Errors; + } + var id = slugHelper.GenerateSlug(command.Name); if (string.IsNullOrEmpty(id)) return Errors.Category.EmptyId; @@ -49,13 +60,8 @@ public async Task> Handle(CreateCategoryCommand dbContext.Categories.Add(entity); await dbContext.SaveChangesAsync(token); - var categoryCreatedEvent = new CategoryCreatedEvent - { - Id = entity.Id, - Name = entity.Name, - ParentId = parent?.Id - }; - await publisher.Publish(categoryCreatedEvent, token); + await cacheService.RemoveAsync(CachingKeys.Categories.CategoriesAll, token); + await cacheService.RemoveAsync(CachingKeys.Categories.CategoriesTree, token); return new CreateCategoryResponse(entity.Id); } diff --git a/Application/Features/Categories/DeleteCategory/CategoryDeletedEvent.cs b/Application/Features/Categories/DeleteCategory/CategoryDeletedEvent.cs deleted file mode 100644 index f0d6880..0000000 --- a/Application/Features/Categories/DeleteCategory/CategoryDeletedEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MediatR; - -namespace Application.Features.Categories.DeleteCategory; - -public class CategoryDeletedEvent : INotification -{ - public required string Id { get; init; } -} \ No newline at end of file diff --git a/Application/Features/Categories/DeleteCategory/DeleteCategoryCommand.cs b/Application/Features/Categories/DeleteCategory/DeleteCategoryCommand.cs index 207ee69..d120de9 100644 --- a/Application/Features/Categories/DeleteCategory/DeleteCategoryCommand.cs +++ b/Application/Features/Categories/DeleteCategory/DeleteCategoryCommand.cs @@ -1,6 +1,5 @@ -using ErrorOr; -using MediatR; +using Application.Common.Messaging; namespace Application.Features.Categories.DeleteCategory; -public record DeleteCategoryCommand(string Id) : IRequest>; \ No newline at end of file +public record DeleteCategoryCommand(string Id) : ICommand; \ No newline at end of file diff --git a/Application/Features/Categories/DeleteCategory/DeleteCategoryCommandHandler.cs b/Application/Features/Categories/DeleteCategory/DeleteCategoryCommandHandler.cs index 0595c81..07e3ca7 100644 --- a/Application/Features/Categories/DeleteCategory/DeleteCategoryCommandHandler.cs +++ b/Application/Features/Categories/DeleteCategory/DeleteCategoryCommandHandler.cs @@ -1,15 +1,18 @@ +using Application.Common.Caching; +using Application.Common.Constants; +using Application.Common.Messaging; using Application.Data; +using Application.Features.Articles.DeleteArticle; using ErrorOr; -using MediatR; namespace Application.Features.Categories.DeleteCategory; public class DeleteCategoryCommandHandler( IApplicationDbContext dbContext, - IPublisher publisher -) : IRequestHandler> + ICacheService cacheService +) : ICommandHandler { - public async Task> Handle(DeleteCategoryCommand deleteCategoryCommand, + public async Task> HandleAsync(DeleteCategoryCommand deleteCategoryCommand, CancellationToken token) { var category = await dbContext.Categories.FindAsync(new object[] {deleteCategoryCommand.Id}, token); @@ -17,8 +20,9 @@ public async Task> Handle(DeleteCategoryCommand dbContext.Categories.Remove(category); await dbContext.SaveChangesAsync(token); - var categoryDeletedEvent = new CategoryDeletedEvent {Id = category.Id}; - await publisher.Publish(categoryDeletedEvent, token); + await cacheService.RemoveAsync(CachingKeys.Categories.CategoriesAll, token); + await cacheService.RemoveAsync(CachingKeys.Categories.CategoriesTree, token); + await cacheService.RemoveAsync(CachingKeys.Categories.CategoryArticlesById(category.Id), token); return new DeleteCategoryResponse(category.Id); } diff --git a/Application/Features/Categories/EventHandlers/CacheInvalidationCategoriesHandler.cs b/Application/Features/Categories/EventHandlers/CacheInvalidationCategoriesHandler.cs deleted file mode 100644 index 6c6fd3d..0000000 --- a/Application/Features/Categories/EventHandlers/CacheInvalidationCategoriesHandler.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Application.Common.Caching; -using Application.Common.Constants; -using Application.Features.Categories.CreateCategory; -using Application.Features.Categories.DeleteCategory; -using MediatR; - -namespace Application.Features.Categories.EventHandlers; - -public class CacheInvalidationCategoriesHandler(ICacheService cacheService) : - INotificationHandler, - INotificationHandler -{ - public async Task Handle(CategoryCreatedEvent notification, CancellationToken token) - { - await cacheService.RemoveAsync(CachingKeys.Categories.CategoriesAll, token); - await cacheService.RemoveAsync(CachingKeys.Categories.CategoriesTree, token); - } - - public async Task Handle(CategoryDeletedEvent notification, CancellationToken token) - { - await cacheService.RemoveAsync(CachingKeys.Categories.CategoriesAll, token); - await cacheService.RemoveAsync(CachingKeys.Categories.CategoriesTree, token); - await cacheService.RemoveAsync(CachingKeys.Categories.CategoryArticlesById(notification.Id), token); - } -} \ No newline at end of file diff --git a/Application/Features/Categories/GetCategories/GetCategoriesQueryHandler.cs b/Application/Features/Categories/GetCategories/GetCategoriesQueryHandler.cs index dd5b2fe..40d548e 100644 --- a/Application/Features/Categories/GetCategories/GetCategoriesQueryHandler.cs +++ b/Application/Features/Categories/GetCategories/GetCategoriesQueryHandler.cs @@ -1,21 +1,25 @@ +using Application.Common.Caching; +using Application.Common.Messaging; +using Application.Common.Utils; using Application.Data; using ErrorOr; -using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Features.Categories.GetCategories; -public class GetCategoriesQueryHandler - (IApplicationDbContext dbContext) : IRequestHandler> +public class GetCategoriesQueryHandler(IApplicationDbContext dbContext, ICacheService cacheService) + : IQueryHandler { - public async Task> Handle(GetCategoriesQuery request, - CancellationToken token) + public async Task> HandleAsync(GetCategoriesQuery request, CancellationToken token) { - var list = await dbContext.Categories - .Select(e => new GetCategoriesResponse.Element(e.Id, e.Name, e.ParentId)) - .AsNoTracking() - .ToListAsync(token); + return await CachingHelper.GetOrCacheAsync(cacheService, request, async () => + { + var list = await dbContext.Categories + .Select(e => new GetCategoriesResponse.Element(e.Id, e.Name, e.ParentId)) + .AsNoTracking() + .ToListAsync(token); - return new GetCategoriesResponse(list); + return new GetCategoriesResponse(list); + }, token); } } \ No newline at end of file diff --git a/Application/Features/Categories/GetCategoriesTree/GetCategoriesTreeQueryHandler.cs b/Application/Features/Categories/GetCategoriesTree/GetCategoriesTreeQueryHandler.cs index 62113a8..abd416a 100644 --- a/Application/Features/Categories/GetCategoriesTree/GetCategoriesTreeQueryHandler.cs +++ b/Application/Features/Categories/GetCategoriesTree/GetCategoriesTreeQueryHandler.cs @@ -1,34 +1,36 @@ +using Application.Common.Caching; +using Application.Common.Messaging; +using Application.Common.Utils; using Application.Data; -using Application.Features.Articles.GetPendingRevisions; -using Domain.Entities; using ErrorOr; -using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Features.Categories.GetCategoriesTree; -public class GetCategoriesTreeQueryHandler - (IApplicationDbContext dbContext) : IRequestHandler> +public class GetCategoriesTreeQueryHandler(IApplicationDbContext dbContext, ICacheService cacheService) + : IQueryHandler { - public async Task> Handle(GetCategoriesTreeQuery request, - CancellationToken token) + public async Task> HandleAsync(GetCategoriesTreeQuery request, CancellationToken token) { - var list = await dbContext.Categories - .Include(e => e.SubCategories) - .ToListAsync(token); + return await CachingHelper.GetOrCacheAsync(cacheService, request, async () => + { + var list = await dbContext.Categories + .Include(e => e.SubCategories) + .ToListAsync(token); - var lookup = list.ToLookup(e => e.ParentId, e => new GetCategoriesTreeResponse.Element( - e.Id, - e.Name, - new List() - ) - ); - - foreach (var node in lookup.SelectMany(e => e)) - node.Children.AddRange(lookup[node.Id]); - - var rootNodes = lookup[null].ToList(); + var lookup = list.ToLookup(e => e.ParentId, e => new GetCategoriesTreeResponse.Element( + e.Id, + e.Name, + new List() + ) + ); - return new GetCategoriesTreeResponse(rootNodes); + foreach (var node in lookup.SelectMany(e => e)) + node.Children.AddRange(lookup[node.Id]); + + var rootNodes = lookup[null].ToList(); + + return new GetCategoriesTreeResponse(rootNodes); + }, token); } } \ No newline at end of file diff --git a/Application/Features/Categories/GetCategoryArticles/GetCategoryArticlesQueryHandler.cs b/Application/Features/Categories/GetCategoryArticles/GetCategoryArticlesQueryHandler.cs index cd02083..64ba9fe 100644 --- a/Application/Features/Categories/GetCategoryArticles/GetCategoryArticlesQueryHandler.cs +++ b/Application/Features/Categories/GetCategoryArticles/GetCategoryArticlesQueryHandler.cs @@ -1,27 +1,32 @@ +using Application.Common.Caching; +using Application.Common.Messaging; +using Application.Common.Utils; using Application.Data; using ErrorOr; -using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Features.Categories.GetCategoryArticles; -public class GetCategoryArticlesQueryHandler - (IApplicationDbContext dbContext) : IRequestHandler> +public class GetCategoryArticlesQueryHandler(IApplicationDbContext dbContext, ICacheService cacheService) + : IQueryHandler { - public async Task> Handle(GetCategoryArticlesQuery request, + public async Task> HandleAsync(GetCategoryArticlesQuery request, CancellationToken token) { - var category = await dbContext.Categories.AsNoTracking() - .FirstOrDefaultAsync(e => e.Id == request.Id, token); + return await CachingHelper.GetOrCacheAsync>(cacheService, request, async () => + { + var category = await dbContext.Categories.AsNoTracking() + .FirstOrDefaultAsync(e => e.Id == request.Id, token); - if (category == null) return Errors.Category.NotFound; + if (category == null) return Errors.Category.NotFound; - var articlesList = await dbContext.Articles - .Where(e => e.RedirectArticleId == null && e.CurrentRevision.Categories.Any(x => x.Id == request.Id)) - .Select(e=> new GetCategoryArticlesResponse.Element(e.Id, e.Title)) - .AsNoTracking() - .ToListAsync(token); - - return new GetCategoryArticlesResponse(articlesList); + var articlesList = await dbContext.Articles + .Where(e => e.RedirectArticleId == null && e.CurrentRevision!.Categories.Any(x => x.Id == request.Id)) + .Select(e=> new GetCategoryArticlesResponse.Element(e.Id, e.Title)) + .AsNoTracking() + .ToListAsync(token); + + return new GetCategoryArticlesResponse(articlesList); + }, token); } } \ No newline at end of file diff --git a/Application/Features/Navigations/EventHandlers/CacheInvalidationNavigationsHandler.cs b/Application/Features/Navigations/EventHandlers/CacheInvalidationNavigationsHandler.cs deleted file mode 100644 index 117179d..0000000 --- a/Application/Features/Navigations/EventHandlers/CacheInvalidationNavigationsHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Application.Common.Caching; -using Application.Common.Constants; -using Application.Features.Navigations.UpdateNavigationsTree; -using MediatR; - -namespace Application.Features.Navigations.EventHandlers; - -public class CacheInvalidationNavigationsHandler(ICacheService cacheService) : - INotificationHandler -{ - public async Task Handle(NavigationsTreeUpdatedEvent notification, CancellationToken token) - { - await cacheService.RemoveAsync(CachingKeys.Navigation.NavigationsTree, token); - } -} \ No newline at end of file diff --git a/Application/Features/Navigations/GetNavigationTree/GetNavigationsTreeQueryHandler.cs b/Application/Features/Navigations/GetNavigationTree/GetNavigationsTreeQueryHandler.cs index c18502b..a70170e 100644 --- a/Application/Features/Navigations/GetNavigationTree/GetNavigationsTreeQueryHandler.cs +++ b/Application/Features/Navigations/GetNavigationTree/GetNavigationsTreeQueryHandler.cs @@ -1,36 +1,41 @@ +using Application.Common.Caching; +using Application.Common.Messaging; +using Application.Common.Utils; using Application.Data; using ErrorOr; -using MediatR; using Microsoft.EntityFrameworkCore; namespace Application.Features.Navigations.GetNavigationTree; -public class GetNavigationsTreeQueryHandler - (IApplicationDbContext dbContext) : IRequestHandler> +public class GetNavigationsTreeQueryHandler(IApplicationDbContext dbContext, ICacheService cacheService) + : IQueryHandler { - public async Task> Handle(GetNavigationsTreeQuery request, + public async Task> HandleAsync(GetNavigationsTreeQuery request, CancellationToken token) { - var navigations = await dbContext.Navigations - .Include(e => e.Children) - .AsNoTracking() - .ToListAsync(token); + return await CachingHelper.GetOrCacheAsync(cacheService, request, async () => + { + var navigations = await dbContext.Navigations + .Include(e => e.Children) + .AsNoTracking() + .ToListAsync(token); - var lookup = navigations.ToLookup(e => e.ParentId, e => new GetNavigationsTreeResponse.Element( - e.Id, - e.Weight, - e.Name, - e.Uri, - e.Icon, - new List() - ) - ); - - foreach (var node in lookup.SelectMany(e => e)) - node.Children.AddRange(lookup[node.Id].OrderByDescending(e=>e.Weight)); - - var rootNodes = lookup[null].OrderByDescending(e=>e.Weight).ToList(); + var lookup = navigations.ToLookup(e => e.ParentId, e => new GetNavigationsTreeResponse.Element( + e.Id, + e.Weight, + e.Name, + e.Uri, + e.Icon, + [] + ) + ); - return new GetNavigationsTreeResponse(rootNodes); + foreach (var node in lookup.SelectMany(e => e)) + node.Children.AddRange(lookup[node.Id].OrderByDescending(e=>e.Weight)); + + var rootNodes = lookup[null].OrderByDescending(e=>e.Weight).ToList(); + + return new GetNavigationsTreeResponse(rootNodes); + }, token); } } \ No newline at end of file diff --git a/Application/Features/Navigations/UpdateNavigationsTree/NavigationsTreeUpdatedEvent.cs b/Application/Features/Navigations/UpdateNavigationsTree/NavigationsTreeUpdatedEvent.cs deleted file mode 100644 index 2dbbe3d..0000000 --- a/Application/Features/Navigations/UpdateNavigationsTree/NavigationsTreeUpdatedEvent.cs +++ /dev/null @@ -1,5 +0,0 @@ -using MediatR; - -namespace Application.Features.Navigations.UpdateNavigationsTree; - -public class NavigationsTreeUpdatedEvent : INotification; \ No newline at end of file diff --git a/Application/Features/Navigations/UpdateNavigationsTree/UpdateNavigationsTreeCommand.cs b/Application/Features/Navigations/UpdateNavigationsTree/UpdateNavigationsTreeCommand.cs index 6e5f9ac..384478a 100644 --- a/Application/Features/Navigations/UpdateNavigationsTree/UpdateNavigationsTreeCommand.cs +++ b/Application/Features/Navigations/UpdateNavigationsTree/UpdateNavigationsTreeCommand.cs @@ -1,10 +1,8 @@ -using Application.Features.Navigations.GetNavigationTree; -using ErrorOr; -using MediatR; +using Application.Common.Messaging; namespace Application.Features.Navigations.UpdateNavigationsTree; -public record UpdateNavigationsTreeCommand(List Data) : IRequest> +public record UpdateNavigationsTreeCommand(List Data) : ICommand { public record Element(string Name, string? Uri, string? Icon, List Children); } \ No newline at end of file diff --git a/Application/Features/Navigations/UpdateNavigationsTree/UpdateNavigationsTreeCommandHandler.cs b/Application/Features/Navigations/UpdateNavigationsTree/UpdateNavigationsTreeCommandHandler.cs index 1656c6d..da80ca4 100644 --- a/Application/Features/Navigations/UpdateNavigationsTree/UpdateNavigationsTreeCommandHandler.cs +++ b/Application/Features/Navigations/UpdateNavigationsTree/UpdateNavigationsTreeCommandHandler.cs @@ -1,18 +1,29 @@ +using Application.Common.Caching; +using Application.Common.Constants; +using Application.Common.Messaging; +using Application.Common.Utils; using Application.Data; using Domain.Entities; using ErrorOr; -using MediatR; +using FluentValidation; namespace Application.Features.Navigations.UpdateNavigationsTree; public class UpdateNavigationsTreeCommandHandler( IApplicationDbContext dbContext, - IPublisher publisher -) : IRequestHandler> + ICacheService cacheService, + IValidator validator +) : ICommandHandler { - public async Task> Handle(UpdateNavigationsTreeCommand request, + public async Task> HandleAsync(UpdateNavigationsTreeCommand request, CancellationToken token) { + var validationResult = ValidatorHelper.Validate(validator, request); + if (validationResult.IsError) + { + return validationResult.Errors; + } + foreach (var item in dbContext.Navigations) dbContext.Navigations.Remove(item); @@ -47,8 +58,7 @@ public async Task> Handle(UpdateNavigatio await dbContext.Navigations.AddRangeAsync(navigationsToAdd, token); await dbContext.SaveChangesAsync(token); - var navigationsTreeUpdatedEvent = new NavigationsTreeUpdatedEvent(); - await publisher.Publish(navigationsTreeUpdatedEvent, token); + await cacheService.RemoveAsync(CachingKeys.Navigation.NavigationsTree, token); return new UpdateNavigationsTreeResponse(); } diff --git a/Infrastructure/Caching/CacheService.cs b/Infrastructure/Caching/CacheService.cs index 7a429d3..41ee429 100644 --- a/Infrastructure/Caching/CacheService.cs +++ b/Infrastructure/Caching/CacheService.cs @@ -17,7 +17,7 @@ public async Task GetOrCreateAsync(string key, Func(); - if (mediator == null) return; + //TODO: Reimplement without MediatR - var createAuthorCommand = new CreateAuthorCommand(idClaim.Value, principal.Identity.Name); - - var createAuthorResult = await mediator.Send(createAuthorCommand); - if (createAuthorResult.IsError && createAuthorResult.FirstError == DuplicateId) - { - var editAuthorCommand = new EditAuthorCommand(idClaim.Value, principal.Identity.Name); - await mediator.Send(editAuthorCommand); - } + // var mediator = context.HttpContext.RequestServices.GetService(); + // if (mediator == null) return; + // + // var createAuthorCommand = new CreateAuthorCommand(idClaim.Value, principal.Identity.Name); + // + // var createAuthorResult = await mediator.Send(createAuthorCommand); + // if (createAuthorResult.IsError && createAuthorResult.FirstError == DuplicateId) + // { + // var editAuthorCommand = new EditAuthorCommand(idClaim.Value, principal.Identity.Name); + // await mediator.Send(editAuthorCommand); + // } } } \ No newline at end of file diff --git a/WebApi/Features/Articles/ArticlesModule.cs b/WebApi/Features/Articles/ArticlesModule.cs index e043a0b..79d231b 100644 --- a/WebApi/Features/Articles/ArticlesModule.cs +++ b/WebApi/Features/Articles/ArticlesModule.cs @@ -1,4 +1,5 @@ -using Application.Features.Articles.CreateArticle; +using Application.Common.Messaging; +using Application.Features.Articles.CreateArticle; using Application.Features.Articles.DeleteArticle; using Application.Features.Articles.EditArticle; using Application.Features.Articles.GetArticle; @@ -9,7 +10,6 @@ using Application.Features.Articles.ReviewRevision; using Application.Features.Articles.SearchArticles; using Application.Features.Articles.SetRedirect; -using MediatR; using Microsoft.AspNetCore.Authorization; using WebApi.Extensions; using WebApi.Features.Articles.Requests; @@ -22,10 +22,13 @@ public static class ArticlesModule public static void AddArticlesEndpoints(this IEndpointRouteBuilder app) { app.MapPost("/api/articles", - async Task (IMediator mediator, CreateArticleRequest request) => + async Task ( + ICommandHandler createArticleCommandHandler, + CreateArticleRequest request, + CancellationToken cancellationToken) => { var command = new CreateArticleCommand(request.Title, request.Content, request.AuthorsNote, request.CategoryIds); - var result = await mediator.Send(command); + var result = await createArticleCommandHandler.HandleAsync(command, cancellationToken); return result.MatchFirst( value => Results.Created($"/api/articles/{value.Id}", value), error => error.ToIResult() @@ -42,10 +45,15 @@ async Task (IMediator mediator, CreateArticleRequest request) => .WithOpenApi(); app.MapGet("/api/articles", - async Task (IMediator mediator, string? searchTerm, int page = 1, int pageSize = 50) => + async Task ( + IQueryHandler searchArticlesQueryHandler, + CancellationToken cancellationToken, + string? searchTerm, + int page = 1, + int pageSize = 50) => { var query = new SearchArticlesQuery(searchTerm, page, pageSize); - var result = await mediator.Send(query); + var result = await searchArticlesQueryHandler.HandleAsync(query, cancellationToken); return result.MatchFirst( value => Results.Ok(value), error => error.ToIResult() @@ -57,9 +65,12 @@ async Task (IMediator mediator, string? searchTerm, int page = 1, int p .WithOpenApi(); app.MapGet("/api/articles/{id}", - async Task (string id, IMediator mediator) => + async Task ( + string id, + IQueryHandler getArticleQueryHandler, + CancellationToken cancellationToken) => { - var result = await mediator.Send(new GetArticleQuery(id)); + var result = await getArticleQueryHandler.HandleAsync(new GetArticleQuery(id), cancellationToken); return result.MatchFirst( value => Results.Ok(value), error => error.ToIResult() @@ -71,11 +82,14 @@ async Task (string id, IMediator mediator) => .ProducesProblem(StatusCodes.Status400BadRequest) .ProducesProblem(StatusCodes.Status404NotFound) .WithOpenApi(); - + app.MapGet("/api/articles/revision:{id:guid}", - async Task (Guid id, IMediator mediator) => + async Task ( + Guid id, + IQueryHandler getArticleQueryHandler, + CancellationToken cancellationToken) => { - var result = await mediator.Send(new GetArticleQuery(null, id)); + var result = await getArticleQueryHandler.HandleAsync(new GetArticleQuery(null, id), cancellationToken); return result.MatchFirst( value => Results.Ok(value), error => error.ToIResult() @@ -89,10 +103,14 @@ async Task (Guid id, IMediator mediator) => .WithOpenApi(); app.MapPut("/api/articles/{id}", - async Task (string id, IMediator mediator, EditArticleRequest request) => + async Task ( + string id, + ICommandHandler editArticleCommandHandler, + EditArticleRequest request, + CancellationToken cancellationToken) => { var command = new EditArticleCommand(id, request.Content, request.AuthorsNote, request.CategoryIds); - var result = await mediator.Send(command); + var result = await editArticleCommandHandler.HandleAsync(command, cancellationToken); return result.MatchFirst( value => Results.Created($"/api/articles/{value.Id}", value), error => error.ToIResult() @@ -110,10 +128,14 @@ async Task (string id, IMediator mediator, EditArticleRequest request) .WithOpenApi(); app.MapPut("/api/articles/{id}/redirect", - async Task (string id, IMediator mediator, SerArticleRedirectRequest request) => + async Task ( + string id, + ICommandHandler setRedirectCommandHandler, + SerArticleRedirectRequest request, + CancellationToken cancellationToken) => { var command = new SetRedirectCommand(id, request.RedirectArticleId); - var result = await mediator.Send(command); + var result = await setRedirectCommandHandler.HandleAsync(command, cancellationToken); return result.MatchFirst( value => Results.Created($"/api/articles/{value.Id}", true), error => error.ToIResult() @@ -131,10 +153,12 @@ async Task (string id, IMediator mediator, SerArticleRedirectRequest re .WithOpenApi(); app.MapGet("/api/articles/revisions/pending", - async Task (IMediator mediator) => + async Task ( + IQueryHandler getPendingRevisionsQueryHandler, + CancellationToken cancellationToken) => { - var command = new GetPendingRevisionsQuery(); - var result = await mediator.Send(command); + var query = new GetPendingRevisionsQuery(); + var result = await getPendingRevisionsQueryHandler.HandleAsync(query, cancellationToken); return result.MatchFirst( value => Results.Ok(value), error => error.ToIResult() @@ -149,10 +173,12 @@ async Task (IMediator mediator) => .WithOpenApi(); app.MapGet("/api/articles/revisions/pending/count", - async Task (IMediator mediator) => + async Task ( + IQueryHandler getPendingRevisionsCountQueryHandler, + CancellationToken cancellationToken) => { - var command = new GetPendingRevisionsCountQuery(); - var result = await mediator.Send(command); + var query = new GetPendingRevisionsCountQuery(); + var result = await getPendingRevisionsCountQueryHandler.HandleAsync(query, cancellationToken); return result.MatchFirst( value => Results.Ok(value), error => error.ToIResult() @@ -168,10 +194,13 @@ async Task (IMediator mediator) => .WithOpenApi(); app.MapGet("/api/articles/{id}/revisions", - async Task (string id, IMediator mediator) => + async Task ( + string id, + IQueryHandler getRevisionHistoryQueryHandler, + CancellationToken cancellationToken) => { - var command = new GetRevisionHistoryQuery(id); - var result = await mediator.Send(command); + var query = new GetRevisionHistoryQuery(id); + var result = await getRevisionHistoryQueryHandler.HandleAsync(query, cancellationToken); return result.MatchFirst( value => Results.Ok(value), error => error.ToIResult() @@ -184,10 +213,13 @@ async Task (string id, IMediator mediator) => .WithOpenApi(); app.MapGet("/api/articles/revisions/{id}/reviews", - async Task (Guid id, IMediator mediator) => + async Task ( + Guid id, + IQueryHandler getRevisionHistoryQueryHandler, + CancellationToken cancellationToken) => { - var command = new GetRevisionReviewHistoryQuery(id); - var result = await mediator.Send(command); + var query = new GetRevisionReviewHistoryQuery(id); + var result = await getRevisionHistoryQueryHandler.HandleAsync(query, cancellationToken); return result.MatchFirst( value => Results.Ok(value), error => error.ToIResult() @@ -200,10 +232,14 @@ async Task (Guid id, IMediator mediator) => .WithOpenApi(); app.MapPost("/api/articles/revisions/{id}/reviews", - async Task (Guid id, IMediator mediator, ReviewArticleRevisionRequest request) => + async Task ( + Guid id, + ICommandHandler reviewRevisionCommandHandler, + ReviewArticleRevisionRequest request, + CancellationToken cancellationToken) => { var command = new ReviewRevisionCommand(id, request.Status, request.Review); - var result = await mediator.Send(command); + var result = await reviewRevisionCommandHandler.HandleAsync(command, cancellationToken); return result.MatchFirst( value => Results.Created($"/api/articles/revisions/{id}/reviews/{value.Id}", value), error => error.ToIResult() @@ -220,10 +256,13 @@ async Task (Guid id, IMediator mediator, ReviewArticleRevisionRequest r .WithOpenApi(); app.MapDelete("/api/articles/{id}", - async (string id, IMediator mediator) => + async ( + string id, + ICommandHandler deleteArticleCommandHandler, + CancellationToken cancellationToken) => { var command = new DeleteArticleCommand(id); - var result = await mediator.Send(command); + var result = await deleteArticleCommandHandler.HandleAsync(command, cancellationToken); return result.MatchFirst( value => Results.Ok(value), error => error.ToIResult() diff --git a/WebApi/Features/Categories/CategoriesModule.cs b/WebApi/Features/Categories/CategoriesModule.cs index 42ccc37..d3afa46 100644 --- a/WebApi/Features/Categories/CategoriesModule.cs +++ b/WebApi/Features/Categories/CategoriesModule.cs @@ -1,9 +1,9 @@ -using Application.Features.Categories.CreateCategory; +using Application.Common.Messaging; +using Application.Features.Categories.CreateCategory; using Application.Features.Categories.DeleteCategory; using Application.Features.Categories.GetCategories; using Application.Features.Categories.GetCategoriesTree; using Application.Features.Categories.GetCategoryArticles; -using MediatR; using Microsoft.AspNetCore.Authorization; using WebApi.Extensions; using WebApi.Features.Categories.Requests; @@ -15,10 +15,14 @@ public static class CategoriesModule { public static void AddCategoriesEndpoints(this IEndpointRouteBuilder app) { - app.MapPost("/api/categories", async (IMediator mediator, CreateCategoryRequest request) => + app.MapPost("/api/categories", + async ( + ICommandHandler createCategoryCommandHandler, + CreateCategoryRequest request, + CancellationToken cancellationToken) => { var command = new CreateCategoryCommand(request.Name, request.ParentId); - var result = await mediator.Send(command); + var result = await createCategoryCommandHandler.HandleAsync(command, cancellationToken); return result.MatchFirst( value => Results.Created($"/api/categories/{value.Id}", value), error => error.ToIResult() @@ -36,9 +40,11 @@ public static void AddCategoriesEndpoints(this IEndpointRouteBuilder app) .WithOpenApi(); app.MapGet("/api/categories", - async Task (IMediator mediator) => + async Task ( + IQueryHandler getCategoriesQueryHandler, + CancellationToken cancellationToken) => { - var response = await mediator.Send(new GetCategoriesQuery()); + var response = await getCategoriesQueryHandler.HandleAsync(new GetCategoriesQuery(), cancellationToken); return response.MatchFirst( value => Results.Ok(value), error => error.ToIResult() @@ -50,9 +56,11 @@ async Task (IMediator mediator) => .WithOpenApi(); app.MapGet("/api/categories/tree", - async Task (IMediator mediator) => + async Task ( + IQueryHandler getCategoriesTreeQueryHandler, + CancellationToken cancellationToken) => { - var response = await mediator.Send(new GetCategoriesTreeQuery()); + var response = await getCategoriesTreeQueryHandler.HandleAsync(new GetCategoriesTreeQuery(), cancellationToken); return response.MatchFirst( value => Results.Ok(value), error => error.ToIResult() @@ -64,10 +72,13 @@ async Task (IMediator mediator) => .WithOpenApi(); app.MapGet("/api/categories/{id}/articles", - async Task (string id, IMediator mediator) => + async Task ( + string id, + IQueryHandler getCategoryArticlesQuery, + CancellationToken cancellationToken) => { var query = new GetCategoryArticlesQuery(id); - var response = await mediator.Send(query); + var response = await getCategoryArticlesQuery.HandleAsync(query, cancellationToken); return response.MatchFirst( value => Results.Ok(value), error => error.ToIResult() @@ -80,10 +91,13 @@ async Task (string id, IMediator mediator) => .WithOpenApi(); app.MapDelete("/api/categories/{id}", - async (string id, IMediator mediator) => + async ( + string id, + ICommandHandler deleteCategoryCommandHandler, + CancellationToken cancellationToken) => { var command = new DeleteCategoryCommand(id); - var result = await mediator.Send(command); + var result = await deleteCategoryCommandHandler.HandleAsync(command, cancellationToken); return result.MatchFirst( value => Results.Ok(value), error => error.ToIResult() diff --git a/WebApi/Features/Navigations/NavigationsModule.cs b/WebApi/Features/Navigations/NavigationsModule.cs index c1bd2e4..3368bd6 100644 --- a/WebApi/Features/Navigations/NavigationsModule.cs +++ b/WebApi/Features/Navigations/NavigationsModule.cs @@ -1,6 +1,6 @@ -using Application.Features.Navigations.GetNavigationTree; +using Application.Common.Messaging; +using Application.Features.Navigations.GetNavigationTree; using Application.Features.Navigations.UpdateNavigationsTree; -using MediatR; using Microsoft.AspNetCore.Authorization; using WebApi.Extensions; using WebApi.Features.Navigations.Requests; @@ -13,9 +13,9 @@ public static class NavigationsModule public static void AddNavigationsEndpoints(this IEndpointRouteBuilder app) { app.MapGet("/api/navigations/tree", - async Task (IMediator mediator) => + async Task (IQueryHandler getNavigationsTreeQueryHandler, CancellationToken cancellationToken) => { - var response = await mediator.Send(new GetNavigationsTreeQuery()); + var response = await getNavigationsTreeQueryHandler.HandleAsync(new GetNavigationsTreeQuery(), cancellationToken); return response.MatchFirst( value => Results.Ok(value), error => error.ToIResult() @@ -27,10 +27,10 @@ async Task (IMediator mediator) => .WithOpenApi(); app.MapPut("/api/navigations/tree", - async Task (IMediator mediator, UpdateNavigationsTreeRequest request) => + async Task (ICommandHandler updateNavigationsTreeCommandHandler, UpdateNavigationsTreeRequest request, CancellationToken cancellationToken) => { var command = new UpdateNavigationsTreeCommand(request.Data); - var response = await mediator.Send(command); + var response = await updateNavigationsTreeCommandHandler.HandleAsync(command, cancellationToken); return response.MatchFirst( value => Results.Ok(), error => error.ToIResult() @@ -38,7 +38,7 @@ async Task (IMediator mediator, UpdateNavigationsTreeRequest request) = }) .WithName("UpdateNavigationsTree") .WithTags("Navigations") - .Produces() + .Produces() .ProducesProblem(StatusCodes.Status400BadRequest) .ProducesProblem(StatusCodes.Status401Unauthorized) .ProducesProblem(StatusCodes.Status403Forbidden)