├── tests
├── load-testing
│ ├── load.js
│ ├── soak.js
│ ├── spike.js
│ └── stress.js
├── Blogger.UnitTests
│ ├── GlobalUsings.cs
│ ├── Domain
│ │ ├── SubscriberAggregateTests
│ │ │ └── SubscriberIdTests.cs
│ │ └── ArticleAggregateTests
│ │ │ ├── TagTests.cs
│ │ │ └── AuthorTests.cs
│ ├── Blogger.UnitTests.csproj
│ └── BuildingBlocks
│ │ └── EntityTests.cs
├── Blogger.IntegrationTests
│ ├── Fixtures
│ │ ├── BloggerDbContextFixture.cs
│ │ └── EfDatabaseBaseFixture.cs
│ ├── Blogger.IntegrationTests.csproj
│ ├── Articles
│ │ ├── MakeDraftCommandHandlerTests.cs
│ │ ├── CreateArticleCommandHandlerTests.cs
│ │ ├── GetArchiveQueryHandlerTests.cs
│ │ ├── GetArticleQueryHandlerTests.cs
│ │ ├── GetTaggedArticlesQueryHandlerTests.cs
│ │ ├── GetTagsQueryHandlerTests.cs
│ │ ├── GetPopularTagsQueryHandlerTests.cs
│ │ ├── PublishDraftCommandHandlerTests.cs
│ │ └── GetPopularArticlesHandlerTests.cs
│ ├── Subscribers
│ │ └── SubscribeCommandHandlerTests.cs
│ └── Comments
│ │ ├── ApproveCommentCommandHandlerTests.cs
│ │ ├── ApproveReplyCommandHandlerTests.cs
│ │ ├── GetCommentsHandlerTests.cs
│ │ ├── GetRepliesHandlerTests.cs
│ │ ├── MakeCommentCommandHandlerTests.cs
│ │ └── ReplyToCommentCommandHandlerTests.cs
└── Blogger.FunctionalTests
│ ├── Helper
│ └── StringGenerator.cs
│ └── Blogger.FunctionalTests.csproj
├── Directory.Build.targets
├── liara.json
├── src
├── Blogger.BuildingBlocks
│ ├── Blogger.BuildingBlocks.csproj
│ ├── Domain
│ │ ├── IDomainEvent.cs
│ │ ├── IAggregateRoot.cs
│ │ ├── DomainException.cs
│ │ ├── AggregateRoot.cs
│ │ ├── Entity.cs
│ │ └── ValueObject.cs
│ └── Exceptions
│ │ └── InvalidEmailAddressException.cs
├── Blogger.APIs
│ ├── IAssemblyMarker.cs
│ ├── Abstractions
│ │ └── IEndpoint.cs
│ ├── Endpoints
│ │ ├── Articles
│ │ │ ├── GetTags
│ │ │ │ ├── GetTagsResponse.cs
│ │ │ │ ├── GetTagsMappingProfile.cs
│ │ │ │ └── GetTagsEndpoint.cs
│ │ │ ├── MakeDraft
│ │ │ │ ├── MakeDraftResponse.cs
│ │ │ │ ├── MakeDraftRequest.cs
│ │ │ │ ├── MakeDraftMappingProfile.cs
│ │ │ │ ├── MakeDraftEndpoint.cs
│ │ │ │ └── MakeDraftRequestValidator.cs
│ │ │ ├── GetPopularTags
│ │ │ │ ├── GetPopularTagsResponse.cs
│ │ │ │ ├── GetPopularTagsMappingProfile.cs
│ │ │ │ └── GetPopularTagsEndpoint.cs
│ │ │ ├── CreateArticle
│ │ │ │ ├── CreateArticleResponse.cs
│ │ │ │ ├── CreateArticleRequest.cs
│ │ │ │ ├── CreateArticleMappingProfile.cs
│ │ │ │ ├── CreateArticleEndPoint.cs
│ │ │ │ └── CreateArticleRequestValidator.cs
│ │ │ ├── GetArticle
│ │ │ │ ├── GetArticleRequest.cs
│ │ │ │ ├── GetArticleRequestValidator.cs
│ │ │ │ ├── GetArticleResponse.cs
│ │ │ │ ├── GetArticleEndpoint.cs
│ │ │ │ └── GetArticleMappingProfile.cs
│ │ │ ├── GetTaggedArticles
│ │ │ │ ├── GetTaggedArticlesRequest.cs
│ │ │ │ ├── GetTaggedArticlesResponse.cs
│ │ │ │ ├── GetTaggedArticlesMappingProfile.cs
│ │ │ │ └── GetTaggedArticlesEndpoint.cs
│ │ │ ├── PublishDraft
│ │ │ │ ├── PublishDraftRequest.cs
│ │ │ │ ├── PublishDraftRequestValidator.cs
│ │ │ │ ├── PublishDraftMappingProfile.cs
│ │ │ │ └── PublishDraftEndpoint.cs
│ │ │ ├── GetPopularArticles
│ │ │ │ ├── GetPopularArticlesResponse.cs
│ │ │ │ ├── GetPopularArticlesMappingProfile.cs
│ │ │ │ └── GetPopularArticlesEndpoint.cs
│ │ │ ├── UpdateDraft
│ │ │ │ ├── UpdateDraftRequest.cs
│ │ │ │ ├── UpdateDraftMappingProfile.cs
│ │ │ │ ├── UpdateDraftEndpoint.cs
│ │ │ │ └── UpdateDraftRequestValidator.cs
│ │ │ ├── GetArticles
│ │ │ │ ├── GetArticlesRequest.cs
│ │ │ │ ├── GetArticlesResponse.cs
│ │ │ │ ├── GetArticlesEndpoint.cs
│ │ │ │ └── GetArticlesMappingProfile.cs
│ │ │ └── GetArchive
│ │ │ │ ├── GetArchiveResponse.cs
│ │ │ │ ├── GetArchiveMappingProfile.cs
│ │ │ │ └── GetArchiveEndpoint.cs
│ │ ├── Comments
│ │ │ ├── MakeComment
│ │ │ │ ├── MakeCommentResponse.cs
│ │ │ │ ├── MakeCommentRequest.cs
│ │ │ │ ├── MakeCommentMappingProfile.cs
│ │ │ │ ├── MakeCommentRequestValidator.cs
│ │ │ │ └── MakeCommentEndpoint.cs
│ │ │ ├── ReplyToCommet
│ │ │ │ ├── ReplyToCommentResponse.cs
│ │ │ │ ├── ReplyToCommentRequestModel.cs
│ │ │ │ ├── ReplyToCommentMappingProfile.cs
│ │ │ │ ├── ReplyToCommentRequestValidator.cs
│ │ │ │ └── ReplyToCommentEndpoint.cs
│ │ │ ├── ApproveComment
│ │ │ │ ├── ApproveCommentRequest.cs
│ │ │ │ ├── ApproveCommentRequestValidator.cs
│ │ │ │ ├── ApproveCommentMappingProfile.cs
│ │ │ │ └── ApproveCommentEndpoint.cs
│ │ │ ├── GetReplies
│ │ │ │ ├── GetRepliesRequest.cs
│ │ │ │ ├── GetRepliesResponse.cs
│ │ │ │ ├── GetRepliesValidator.cs
│ │ │ │ ├── GetRepliesMappingProfile.cs
│ │ │ │ └── GetRepliesEndpoint.cs
│ │ │ ├── GetComments
│ │ │ │ ├── GetCommentsRequest.cs
│ │ │ │ ├── GetCommentsResponse.cs
│ │ │ │ ├── GetCommentsValidator.cs
│ │ │ │ ├── GetCommentsMappingProfile.cs
│ │ │ │ └── GetCommentsEndpoint.cs
│ │ │ └── ApproveReply
│ │ │ │ ├── ApproveReplyRequest.cs
│ │ │ │ ├── ApproveReplyRequestValidator.cs
│ │ │ │ ├── ApproveReplyMappingProfile.cs
│ │ │ │ └── ApproveReplyEndpoint.cs
│ │ ├── Subscribers
│ │ │ └── Subscribe
│ │ │ │ ├── SubscribeRequest.cs
│ │ │ │ ├── SubscribeRequestValidator.cs
│ │ │ │ ├── SubscribeMappingProfile.cs
│ │ │ │ └── SubscribeEndpoint.cs
│ │ └── EndpointSchema.cs
│ ├── Blogger.APIs.http
│ ├── appsettings.json
│ ├── appsettings.Development.json
│ ├── Blogger.APIs.csproj
│ ├── Dockerfile
│ ├── Program.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Filters
│ │ └── EndpointValidatorFilter.cs
│ ├── ErrorHandling
│ │ └── GlobalExceptionHandler.cs
│ ├── GlobalUsings.cs
│ └── DependencyInjection.cs
├── Blogger.Domain
│ ├── IAssemblyMarker.cs
│ ├── ArticleAggregate
│ │ ├── Models
│ │ │ ├── TagModel.cs
│ │ │ └── ArchiveModel.cs
│ │ ├── ArticleStatus.cs
│ │ ├── DraftTagsMissingException.cs
│ │ ├── IArticleService.cs
│ │ ├── Tag.cs
│ │ ├── ArticleId.cs
│ │ ├── Like.cs
│ │ ├── Author.cs
│ │ └── IArticleRepository.cs
│ ├── CommentAggregate
│ │ ├── MakeCommentEvent.cs
│ │ ├── UnapprovedCommentException.cs
│ │ ├── InvalidReplyApprovalLinkException.cs
│ │ ├── ReplyId.cs
│ │ ├── CommentId.cs
│ │ ├── Client.cs
│ │ ├── ICommentRepository.cs
│ │ ├── Reply.cs
│ │ ├── ApproveLink.cs
│ │ └── Comment.cs
│ ├── GlobalUsings.cs
│ ├── Blogger.Domain.csproj
│ └── SubscriberAggregate
│ │ ├── ISubscriberRepository.cs
│ │ ├── SubscriberId.cs
│ │ └── Subscriber.cs
├── Blogger.Application
│ ├── IAssemblyMarker.cs
│ ├── Articles
│ │ ├── GetTags
│ │ │ ├── GetTagsQueryResponse.cs
│ │ │ ├── GetTagsQuery.cs
│ │ │ └── GetTagsQueryHandler.cs
│ │ ├── GetPopularTags
│ │ │ ├── GetPopularTagsQueryResponse.cs
│ │ │ ├── GetPopularTagsQuery.cs
│ │ │ └── GetPopularTagsQueryHandler.cs
│ │ ├── MakeDraft
│ │ │ ├── MakingDraftCommandResponse.cs
│ │ │ ├── MakingDraftCommand.cs
│ │ │ ├── DraftAlreadyExistsException.cs
│ │ │ └── MakingDraftCommandHandler.cs
│ │ ├── PublishDraft
│ │ │ ├── PublishDraftCommand.cs
│ │ │ └── PublishDraftCommandHandler.cs
│ │ ├── GetArchive
│ │ │ ├── GetArchiveQuery.cs
│ │ │ ├── GetArchiveQueryResponse.cs
│ │ │ └── GetArchiveQueryHandler.cs
│ │ ├── CreateArticle
│ │ │ ├── CreateArticleCommandResponse.cs
│ │ │ ├── CreateArticleCommand.cs
│ │ │ ├── ArticleAlreadyExistsException.cs
│ │ │ └── CreateArticleCommandHandler.cs
│ │ ├── GetArticle
│ │ │ ├── GetArticleQuery.cs
│ │ │ ├── GetArticleQueryHandler.cs
│ │ │ └── GetArticleQueryResponse.cs
│ │ ├── GetPopularArticles
│ │ │ ├── GetPopularArticlesQueryResponse.cs
│ │ │ ├── GetPopularArticlesQuery.cs
│ │ │ └── GetPopularArticlesHandler.cs
│ │ ├── GetTaggedArticles
│ │ │ ├── GetTaggedArticlesQuery.cs
│ │ │ ├── GetTaggedArticlesQueryHandler.cs
│ │ │ └── GetTaggedArticlesQueryResponse.cs
│ │ ├── UpdateDraft
│ │ │ ├── UpdateDraftCommand.cs
│ │ │ ├── DraftNotFoundException.cs
│ │ │ ├── DraftTitleDuplicatedException.cs
│ │ │ └── UpdateDraftCommandHandler.cs
│ │ ├── GetArticles
│ │ │ ├── GetArticlesQuery.cs
│ │ │ ├── GetArticlesQueryHandler.cs
│ │ │ └── GetArticlesQueryResponse.cs
│ │ └── ArticleService.cs
│ ├── ApplicationServices
│ │ ├── ILinkGenerator.cs
│ │ └── IEmailService.cs
│ ├── Subscribers
│ │ ├── Subscribe
│ │ │ ├── SubscribeCommand.cs
│ │ │ ├── DuplicateSubscribtionException.cs
│ │ │ └── SubscribeCommandHandler.cs
│ │ ├── ISubscriberService.cs
│ │ └── SubscriberService.cs
│ ├── Comments
│ │ ├── ApproveComment
│ │ │ ├── ApproveCommentCommand.cs
│ │ │ ├── ApproveCommentCommandResponse.cs
│ │ │ ├── InvalidCommentApprovalLinkException.cs
│ │ │ └── ApproveCommentCommandHandler.cs
│ │ ├── GetComments
│ │ │ ├── GetCommentsQuery.cs
│ │ │ ├── GetCommentsQueryResponse.cs
│ │ │ └── GetCommentsHandler.cs
│ │ ├── MakeComment
│ │ │ ├── MakeCommentCommandResponse.cs
│ │ │ ├── MakeCommentCommand.cs
│ │ │ ├── NotFoundArticleException.cs
│ │ │ ├── NotValidClientException.cs
│ │ │ └── MakeCommentCommandHandler.cs
│ │ ├── ReplyToComment
│ │ │ ├── ReplyToCommentCommandResponse.cs
│ │ │ ├── ReplyToCommentCommand.cs
│ │ │ ├── NotFoundArticleException.cs
│ │ │ └── ReplyToCommentCommandHandler.cs
│ │ ├── GetReplies
│ │ │ ├── GetRepliesQuery.cs
│ │ │ ├── GetRepliesQueryResponse.cs
│ │ │ └── GetRepliesHandler.cs
│ │ └── ApproveReply
│ │ │ ├── ApproveReplyCommand.cs
│ │ │ ├── ApproveReplyCommandResponse.cs
│ │ │ ├── CommentNotFoundException.cs
│ │ │ └── ApproveReplyCommandHandler.cs
│ ├── GlobalUsings.cs
│ ├── ApplicationSettings.cs
│ ├── Blogger.Application.csproj
│ └── DependencyInjection.cs
└── Blogger.Infrastructure
│ ├── IAssemblyMarker.cs
│ ├── Services
│ ├── Externals
│ │ ├── EmailSettings.cs
│ │ └── EmailService.cs
│ └── LinkGenerator.cs
│ ├── Persistence
│ ├── BloggerDbContextFactory.cs
│ ├── BloggerDbContext.cs
│ ├── Repositories
│ │ ├── SubscriberRepository.cs
│ │ └── CommentRepository.cs
│ ├── Configurations
│ │ └── SubscriberConfiguration.cs
│ └── BloggerDbContext.Schema.cs
│ ├── GlobalUsings.cs
│ ├── GlobalSuppressions.cs
│ ├── DependencyInjection.cs
│ └── Blogger.Infrastructure.csproj
├── global.json
├── .dockerignore
├── Directory.Build.props
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── thisisnabi.dev.apis.yml
├── SECURITY.md
├── LICENSE
├── README.md
└── Directory.Packages.props
/tests/load-testing/load.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/load-testing/soak.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/load-testing/spike.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/load-testing/stress.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Directory.Build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/liara.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "thisisnabi-dev-apis",
3 | "port": 80
4 | }
5 |
--------------------------------------------------------------------------------
/src/Blogger.BuildingBlocks/Blogger.BuildingBlocks.csproj:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "8.0.6",
4 | "rollForward": "minor"
5 | }
6 | }
--------------------------------------------------------------------------------
/src/Blogger.APIs/IAssemblyMarker.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs;
2 | public interface IAssemblyMarker
3 | {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Blogger.Domain/IAssemblyMarker.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Domain;
2 | public interface IAssemblyMarker
3 | {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Blogger.Application/IAssemblyMarker.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application;
2 | public interface IAssemblyMarker
3 | {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Blogger.Infrastructure/IAssemblyMarker.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Infrastructure;
2 | public interface IAssemblyMarker
3 | {
4 | }
5 |
--------------------------------------------------------------------------------
/src/Blogger.Domain/ArticleAggregate/Models/TagModel.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Domain.ArticleAggregate.Models;
2 | public sealed record TagModel(Tag Tag, int Count);
--------------------------------------------------------------------------------
/src/Blogger.BuildingBlocks/Domain/IDomainEvent.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.BuildingBlocks.Domain;
2 | public interface IDomainEvent
3 | {
4 | DateTime OccurredOn { get; }
5 | }
6 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Abstractions/IEndpoint.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Abstractions;
2 |
3 | public interface IEndpoint
4 | {
5 | void MapEndpoint(IEndpointRouteBuilder app);
6 | }
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetTags/GetTagsResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetTags;
2 |
3 | public record GetTagsResponse(string Name, int Count);
4 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/MakeDraft/MakeDraftResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.MakeDraft;
2 |
3 | public record MakeDraftResponse(string DraftId);
4 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/GetTags/GetTagsQueryResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.GetTags;
2 |
3 | public record GetTagsQueryResponse(Tag Tag, int Count);
4 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Blogger.APIs.http:
--------------------------------------------------------------------------------
1 | @Blogger.APIs_HostAddress = http://localhost:5138
2 |
3 | GET {{Blogger.APIs_HostAddress}}/weatherforecast/
4 | Accept: application/json
5 |
6 | ###
7 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/MakeComment/MakeCommentResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.MakeComment;
2 |
3 | public record MakeCommentResponse(string CommentId);
4 |
--------------------------------------------------------------------------------
/src/Blogger.Application/ApplicationServices/ILinkGenerator.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.ApplicationServices;
2 | public interface ILinkGenerator
3 | {
4 | string Generate();
5 | }
6 |
--------------------------------------------------------------------------------
/tests/Blogger.UnitTests/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | // built-int
2 |
3 |
4 | // third-parties
5 | global using FluentAssertions;
6 |
7 |
8 | // solution
9 | global using Blogger.BuildingBlocks.Domain;
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetPopularTags/GetPopularTagsResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetPopularTags;
2 |
3 | public record GetPopularTagsResponse(string Name);
4 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/GetPopularTags/GetPopularTagsQueryResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.GetPopularTags;
2 |
3 | public record GetPopularTagsQueryResponse(Tag Tag);
4 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/MakeDraft/MakingDraftCommandResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.MakeDraft;
2 |
3 | public record MakeDraftCommandResponse(ArticleId DraftId);
4 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/PublishDraft/PublishDraftCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.PublishDraft;
2 |
3 | public record PublishDraftCommand(ArticleId DraftId) : IRequest;
--------------------------------------------------------------------------------
/src/Blogger.Application/Subscribers/Subscribe/SubscribeCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Subscribers.Subscribe;
2 | public record SubscribeCommand(SubscriberId SubscriberId) : IRequest;
3 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/CreateArticle/CreateArticleResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.CreateArticle;
2 |
3 | public record CreateArticleResponse(string ArticleId);
4 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/ReplyToCommet/ReplyToCommentResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.ReplyToCommet;
2 |
3 | public record ReplyToCommentResponse(string ReplyId);
4 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/GetTags/GetTagsQuery.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.GetTags;
2 | public record GetTagsQuery()
3 | : IRequest>;
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/ApproveComment/ApproveCommentRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.ApproveComment;
2 |
3 | public record ApproveCommentRequest([FromQuery] string Link);
4 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/GetArchive/GetArchiveQuery.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.GetArchive;
2 | public record GetArchiveQuery() : IRequest>;
--------------------------------------------------------------------------------
/src/Blogger.Domain/ArticleAggregate/ArticleStatus.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Domain.ArticleAggregate;
2 |
3 | public enum ArticleStatus
4 | {
5 | Draft = 1,
6 | Published = 2,
7 | Deleted
8 | }
9 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/CreateArticle/CreateArticleCommandResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.CreateArticle;
2 |
3 | public record CreateArticleCommandResponse(ArticleId ArticleId);
4 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/GetArticle/GetArticleQuery.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.GetArticle;
2 | public record GetArticleQuery(ArticleId ArticleId)
3 | : IRequest;
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetArticle/GetArticleRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetArticle;
2 |
3 | public record GetArticleRequest([FromRoute(Name = "article-id")] string ArticleId);
4 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/GetReplies/GetRepliesRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.GetReplies;
2 |
3 | public record GetRepliesRequest([FromRoute(Name = "comment-id")] Guid CommentId);
4 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetTaggedArticles/GetTaggedArticlesRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetTaggedArticles;
2 |
3 | public record GetTaggedArticlesRequest([FromQuery] string Tag);
4 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/MakeDraft/MakeDraftRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.MakeDraft;
2 |
3 | public record MakeDraftRequest(string Title, string Body, string Summary, string[] Tags);
4 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/PublishDraft/PublishDraftRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.PublishDraft;
2 |
3 | public record PublishDraftRequest([FromRoute(Name = "draft-id")] string DraftId);
4 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/GetComments/GetCommentsRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.GetComments;
2 |
3 | public record GetCommentsRequest([FromRoute(Name = "article-id")] string ArticleId);
4 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetPopularArticles/GetPopularArticlesResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetPopularArticles;
2 |
3 | public record GetPopularArticlesResponse(string Title, string ArticleId);
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/ApproveComment/ApproveCommentCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Comments.ApproveComment;
2 |
3 | public record ApproveCommentCommand(string Link) : IRequest;
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/MakeComment/MakeCommentRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.MakeComment;
2 |
3 | public record MakeCommetRequest(string ArticleId, string Content, string FullName, string Email);
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/CreateArticle/CreateArticleRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.CreateArticle;
2 |
3 | public record CreateArticleRequest(string Title, string Body, string Summary, string[] Tags);
4 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/GetPopularTags/GetPopularTagsQuery.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.GetPopularTags;
2 | public record GetPopularTagsQuery(int Size)
3 | : IRequest>;
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/GetComments/GetCommentsQuery.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Comments.GetComments;
2 | public record GetCommentsQuery(ArticleId ArticleId)
3 | : IRequest>;
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/GetReplies/GetRepliesResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.GetReplies;
2 |
3 | public record GetRepliesResponse(
4 | string FullName,
5 | DateTime CreatedOnUtc,
6 | string Content);
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/GetPopularArticles/GetPopularArticlesQueryResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.GetPopularArticles;
2 |
3 | public record GetPopularArticlesQueryResponse(ArticleId ArticleId, string Title);
4 |
--------------------------------------------------------------------------------
/src/Blogger.BuildingBlocks/Domain/IAggregateRoot.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.BuildingBlocks.Domain;
2 |
3 | public interface IAggregateRoot
4 | {
5 | IReadOnlyCollection Events { get; }
6 |
7 | void ClearEvents();
8 | }
9 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/UpdateDraft/UpdateDraftRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.UpdateDraft;
2 |
3 | public record UpdateDraftRequest(string DraftId, string Title, string Body, string Summary, string[] Tags);
4 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/MakeComment/MakeCommentCommandResponse.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.CommentAggregate;
2 |
3 | namespace Blogger.Application.Comments.MakeComment;
4 | public record MakeCommentCommandResponse(CommentId CommentId);
5 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/ApproveReply/ApproveReplyRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.ApproveReply;
2 |
3 | public record ApproveReplyRequest([FromQuery] string Link, [FromQuery(Name = "comment-id")] Guid CommentId);
4 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/ReplyToComment/ReplyToCommentCommandResponse.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.CommentAggregate;
2 |
3 | namespace Blogger.Application.Comments.ReplyToComment;
4 |
5 | public record ReplyToCommentCommandResponse(ReplyId ReplyId);
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/GetTaggedArticles/GetTaggedArticlesQuery.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.GetTaggedArticles;
2 | public record GetTaggedArticlesQuery(Tag Tag)
3 | : IRequest>;
--------------------------------------------------------------------------------
/src/Blogger.Application/Subscribers/ISubscriberService.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Subscribers;
2 |
3 | public interface ISubscriberService
4 | {
5 | Task IsDuplicated(SubscriberId subscriberId, CancellationToken cancellationToken);
6 | }
7 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/GetComments/GetCommentsResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.GetComments;
2 |
3 | public record GetCommentsResponse(
4 | string Id,
5 | string FullName,
6 | DateTime CreatedOnUtc,
7 | string Content);
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Subscribers/Subscribe/SubscribeRequest.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace Blogger.APIs.Endpoints.Subscribers.Subscribe;
4 |
5 | public record SubscribeRequest([FromBody][EmailAddress] string Email);
6 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/GetPopularArticles/GetPopularArticlesQuery.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.GetPopularArticles;
2 | public record GetPopularArticlesQuery(int Size)
3 | : IRequest>;
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/MakeDraft/MakingDraftCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.MakeDraft;
2 |
3 | public record MakeDraftCommand(string Title, string Body, string Summary, IReadOnlyList Tags)
4 | : IRequest;
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/UpdateDraft/UpdateDraftCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.UpdateDraft;
2 |
3 | public record UpdateDraftCommand(ArticleId DraftId, string Title, string Body, string Summary, IReadOnlyList Tags)
4 | : IRequest;
--------------------------------------------------------------------------------
/src/Blogger.Application/ApplicationServices/IEmailService.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.ApplicationServices;
2 | public interface IEmailService
3 | {
4 | Task SendAsync(string email, string subject, string content, CancellationToken cancellationToken);
5 | }
6 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/GetArticles/GetArticlesQuery.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.GetArticles;
2 | public record GetArticlesQuery(int PageNumber = 1, int PageSize = 10, string Title = "")
3 | : IRequest>;
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetArticles/GetArticlesRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetArticles;
2 |
3 | public record GetArticlesRequest(
4 | [FromQuery] int Page = 1,
5 | [FromQuery] int Size = 10,
6 | [FromQuery] string Title = "");
7 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/GetReplies/GetRepliesQuery.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.CommentAggregate;
2 |
3 | namespace Blogger.Application.Comments.GetReplies;
4 | public record GetRepliesQuery(CommentId CommentId)
5 | : IRequest>;
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/CreateArticle/CreateArticleCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.CreateArticle;
2 |
3 | public record CreateArticleCommand(string Title, string Body, string Summary, IReadOnlyList Tags)
4 | : IRequest;
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/ApproveReply/ApproveReplyCommand.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.CommentAggregate;
2 |
3 | namespace Blogger.Application.Comments.ApproveReply;
4 |
5 | public record ApproveReplyCommand(CommentId CommentId, string Link) : IRequest;
--------------------------------------------------------------------------------
/src/Blogger.BuildingBlocks/Domain/DomainException.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.BuildingBlocks.Domain;
2 |
3 | public abstract class DomainException : Exception
4 | {
5 | protected DomainException() : base() { }
6 |
7 | protected DomainException(string? message) : base(message) { }
8 | }
--------------------------------------------------------------------------------
/src/Blogger.Domain/CommentAggregate/MakeCommentEvent.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Domain.CommentAggregate;
2 |
3 | public class MakeCommentEvent : IDomainEvent
4 | {
5 | public DateTime OccurredOn => DateTime.UtcNow;
6 |
7 | public CommentId CommentId { get; set; } = null!;
8 | }
9 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/ApproveComment/ApproveCommentCommandResponse.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.CommentAggregate;
2 |
3 | namespace Blogger.Application.Comments.ApproveComment;
4 |
5 | public record ApproveCommentCommandResponse(ArticleId ArticleId, CommentId CommentId) : IRequest;
--------------------------------------------------------------------------------
/src/Blogger.Domain/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | // built-in
2 | global using System.Collections.Immutable;
3 | global using System.Net.Mail;
4 |
5 | // third-party
6 |
7 |
8 | // solution
9 | global using Blogger.BuildingBlocks.Domain;
10 | global using Blogger.Domain.CommentAggregate;
11 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/ApproveReply/ApproveReplyCommandResponse.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.CommentAggregate;
2 |
3 | namespace Blogger.Application.Comments.ApproveReply;
4 |
5 | public record ApproveReplyCommandResponse(ArticleId ArticleId, CommentId CommentId, ReplyId ReplyId) : IRequest;
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/EndpointSchema.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints;
2 |
3 | public static class EndpointSchema
4 | {
5 | public const string ArticleTag = "Articles";
6 | public const string CommentTag = "Comments";
7 | public const string SubscriberTag = "Subscribers";
8 | }
9 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/MakeComment/MakeCommentCommand.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.CommentAggregate;
2 |
3 | namespace Blogger.Application.Comments.MakeComment;
4 |
5 | public record MakeCommentCommand(ArticleId ArticleId, Client Client, string Content) : IRequest;
--------------------------------------------------------------------------------
/src/Blogger.Domain/ArticleAggregate/Models/ArchiveModel.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Domain.ArticleAggregate.Models;
2 |
3 | public record ArchiveModel(int Year, int Month, IEnumerable Articles);
4 | public record ArticleArchiveModel(ArticleId ArticleId, string Title, int DayOfMonth);
5 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/ReplyToComment/ReplyToCommentCommand.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.CommentAggregate;
2 |
3 | namespace Blogger.Application.Comments.ReplyToComment;
4 |
5 | public record ReplyToCommentCommand(CommentId CommentId, Client Client, string Content)
6 | : IRequest;
--------------------------------------------------------------------------------
/src/Blogger.Application/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | // built-in
2 | global using System.Collections.Immutable;
3 |
4 | // third-party
5 | global using MediatR;
6 |
7 | // solution
8 | global using Blogger.Domain.ArticleAggregate;
9 | global using Blogger.Domain.SubscriberAggregate;
10 | global using Blogger.BuildingBlocks.Exceptions;
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/GetArchive/GetArchiveQueryResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles.GetArchive;
2 |
3 | public record GetArchiveQueryResponse(int Year, int Month, IReadOnlyCollection Articles);
4 |
5 | public record ArticleArchiveResponse(ArticleId ArticleId, string Title, int Day);
--------------------------------------------------------------------------------
/src/Blogger.Domain/Blogger.Domain.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetArchive/GetArchiveResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetArchive;
2 |
3 | public record GetArchiveResponse(
4 | int Year,
5 | int Month,
6 | IReadOnlyList Articles);
7 |
8 | public record GetArchiveItemResponse(string ArticleId, string Title, int Day);
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetArticles/GetArticlesResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetArticles;
2 |
3 | public record GetArticlesResponse(
4 | string ArticleId,
5 | string Title,
6 | string Body,
7 | string Summary,
8 | DateTime PublishedOnUtc,
9 | int ReadOnMinutes,
10 | string[] Tags);
--------------------------------------------------------------------------------
/src/Blogger.Application/ApplicationSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application;
2 | public static class ApplicationSettings
3 | {
4 | public static class ApproveLink
5 | {
6 | public const int ExpirationOnHours = 10;
7 | public const string ConfirmEmailSubject = "Confirm Your Engagement - thisisnabi.dev";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Blogger.Domain/CommentAggregate/UnapprovedCommentException.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Domain.CommentAggregate;
2 |
3 | public class UnapprovedCommentException : DomainException
4 | {
5 | private const string _messages = "Reply is not allowed for unapproved comments.";
6 | public UnapprovedCommentException() : base(_messages)
7 | {
8 | }
9 | }
--------------------------------------------------------------------------------
/src/Blogger.Domain/ArticleAggregate/DraftTagsMissingException.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Domain.ArticleAggregate;
2 |
3 | public class DraftTagsMissingException : DomainException
4 | {
5 | private const string _messages = "Cannot publish draft without tags.";
6 | public DraftTagsMissingException() : base(_messages)
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/GetReplies/GetRepliesValidator.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.GetReplies;
2 |
3 | public class GetRepliesValidator : AbstractValidator
4 | {
5 | public GetRepliesValidator()
6 | {
7 | RuleFor(x => x.CommentId)
8 | .NotEmpty()
9 | .NotNull();
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Blogger.Domain/CommentAggregate/InvalidReplyApprovalLinkException.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Domain.CommentAggregate;
2 |
3 | public class InvalidReplyApprovalLinkException : DomainException
4 | {
5 | private const string _message = "Invalid Reply approved link.";
6 | public InvalidReplyApprovalLinkException() : base(_message)
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetTaggedArticles/GetTaggedArticlesResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetTaggedArticles;
2 |
3 | public record GetTaggedArticlesResponse(
4 | string ArticleId,
5 | string Title,
6 | string Body,
7 | string Summary,
8 | DateTime PublishedOnUtc,
9 | int ReadOnMinutes,
10 | string[] Tags);
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/GetComments/GetCommentsValidator.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.GetComments;
2 |
3 | public class GetCommentsValidator : AbstractValidator
4 | {
5 | public GetCommentsValidator()
6 | {
7 | RuleFor(x => x.ArticleId)
8 | .NotEmpty()
9 | .NotNull();
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetArticle/GetArticleRequestValidator.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetArticle;
2 |
3 | public class GetArticleRequestValidator : AbstractValidator
4 | {
5 | public GetArticleRequestValidator()
6 | {
7 | RuleFor(x => x.ArticleId)
8 | .NotEmpty()
9 | .NotNull();
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Subscribers/Subscribe/SubscribeRequestValidator.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Subscribers.Subscribe;
2 |
3 | public class SubscribeRequestValidator : AbstractValidator
4 | {
5 | public SubscribeRequestValidator()
6 | {
7 | RuleFor(x => x.Email)
8 | .NotEmpty()
9 | .NotNull();
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/PublishDraft/PublishDraftRequestValidator.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.PublishDraft;
2 |
3 | public class PublishDraftRequestValidator : AbstractValidator
4 | {
5 | public PublishDraftRequestValidator()
6 | {
7 | RuleFor(x => x.DraftId)
8 | .NotEmpty()
9 | .NotNull();
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/UpdateDraft/DraftNotFoundException.cs:
--------------------------------------------------------------------------------
1 | using Blogger.BuildingBlocks.Domain;
2 |
3 |
4 | namespace Blogger.Application.Articles.UpdateDraft;
5 | public class DraftNotFoundException : DomainException
6 | {
7 | private const string _message = "Draft not found.";
8 |
9 | public DraftNotFoundException() : base(_message)
10 | {
11 |
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Blogger.Application.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/Blogger.Domain/ArticleAggregate/IArticleService.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.ArticleAggregate;
2 |
3 | namespace Blogger.Application.Articles;
4 | public interface IArticleService
5 | {
6 | Task IsArticleIdValidAsync(ArticleId articleId, CancellationToken cancellationToken);
7 |
8 | Task HasIdAsync(ArticleId articleId, CancellationToken cancellationToken);
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/ApproveComment/ApproveCommentRequestValidator.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.ApproveComment;
2 |
3 | public class ApproveCommentRequestValidator : AbstractValidator
4 | {
5 | public ApproveCommentRequestValidator()
6 | {
7 | RuleFor(x => x.Link)
8 | .NotEmpty()
9 | .NotNull();
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/ApproveReply/CommentNotFoundException.cs:
--------------------------------------------------------------------------------
1 | using Blogger.BuildingBlocks.Domain;
2 |
3 |
4 | namespace Blogger.Application.Comments.ApproveReply;
5 | public class CommentNotFoundException : DomainException
6 | {
7 | private const string _message = "Comment not found.";
8 |
9 | public CommentNotFoundException() : base(_message)
10 | {
11 |
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/MakeComment/NotFoundArticleException.cs:
--------------------------------------------------------------------------------
1 | using Blogger.BuildingBlocks.Domain;
2 |
3 |
4 | namespace Blogger.Application.Comments.MakeComment;
5 | public class NotFoundArticleException : DomainException
6 | {
7 | private const string _message = "Article not found.";
8 |
9 | public NotFoundArticleException() : base(_message)
10 | {
11 |
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/MakeComment/NotValidClientException.cs:
--------------------------------------------------------------------------------
1 | using Blogger.BuildingBlocks.Domain;
2 |
3 |
4 | namespace Blogger.Application.Comments.MakeComment;
5 |
6 | public class NotValidClientException : DomainException
7 | {
8 | private const string _messages = "Invalid client id.";
9 |
10 | public NotValidClientException() : base(_messages)
11 | {
12 |
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/ReplyToComment/NotFoundArticleException.cs:
--------------------------------------------------------------------------------
1 | using Blogger.BuildingBlocks.Domain;
2 |
3 |
4 | namespace Blogger.Application.Comments.ReplyToComment;
5 | public class NotFoundCommentException : DomainException
6 | {
7 | private const string _message = "Invalid comment for Reply!";
8 |
9 | public NotFoundCommentException() : base(_message)
10 | {
11 |
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetArticle/GetArticleResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetArticle;
2 |
3 | public record GetArticleResponse(string ArticleId,
4 | string Title,
5 | string Body,
6 | string Summary,
7 | int ReadOnMinutes,
8 | string AuthorFullName,
9 | string AuthorAvatar,
10 | string AuthorJobTitle,
11 | DateTime PublishedOnUtc,
12 | string[] Tags);
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/ApproveComment/ApproveCommentMappingProfile.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.ApproveComment;
2 |
3 | public class ApproveCommentMappingProfile : IRegister
4 | {
5 | public void Register(TypeAdapterConfig config)
6 | {
7 | config.ForType()
8 | .Map(x => x.Link, src => src.Link);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Subscribers/Subscribe/DuplicateSubscribtionException.cs:
--------------------------------------------------------------------------------
1 | using Blogger.BuildingBlocks.Domain;
2 |
3 |
4 | namespace Blogger.Application.Subscribers.Subscribe;
5 |
6 | public class DuplicateSubscribtionException : DomainException
7 | {
8 | private const string _messages = "Duplicated registration!";
9 |
10 | public DuplicateSubscribtionException() : base(_messages)
11 | {
12 |
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/PublishDraft/PublishDraftMappingProfile.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.PublishDraft;
2 |
3 | public class PublishDraftMappingProfile : IRegister
4 | {
5 | public void Register(TypeAdapterConfig config)
6 | {
7 | config.ForType()
8 | .Map(x => x.DraftId, src => ArticleId.Create(src.DraftId));
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetPopularTags/GetPopularTagsMappingProfile.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetPopularTags;
2 |
3 | public class GetPopularTagsMappingProfile : IRegister
4 | {
5 | public void Register(TypeAdapterConfig config)
6 | {
7 | config.ForType()
8 | .Map(x => x.Name, src => src.Tag.ToString());
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Blogger.Infrastructure/Services/Externals/EmailSettings.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Infrastructure.Services.Externals;
2 |
3 | public class EmailSettings
4 | {
5 | public string From { get; set; } = null!;
6 |
7 | public string SmtpHost { get; set; } = null!;
8 |
9 | public int SmtpPort { get; set; }
10 |
11 | public string UserName { get; set; } = null!;
12 |
13 | public string Password { get; set; } = null!;
14 | }
15 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/ApproveComment/InvalidCommentApprovalLinkException.cs:
--------------------------------------------------------------------------------
1 | using Blogger.BuildingBlocks.Domain;
2 |
3 |
4 | namespace Blogger.Application.Comments.ApproveComment;
5 |
6 | public class InvalidCommentApprovalLinkException : DomainException
7 | {
8 | private const string _message = "Invalid comment approved link.";
9 | public InvalidCommentApprovalLinkException() : base(_message)
10 | {
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetTags/GetTagsMappingProfile.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetTags;
2 |
3 | public class GetTagsMappingProfile : IRegister
4 | {
5 | public void Register(TypeAdapterConfig config)
6 | {
7 | config.ForType()
8 | .Map(x => x.Count, src => src.Count)
9 | .Map(x => x.Name, src => src.Tag.ToString());
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/GetReplies/GetRepliesQueryResponse.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.CommentAggregate;
2 |
3 | namespace Blogger.Application.Comments.GetReplies;
4 |
5 | public record GetRepliesQueryResponse(string FullName, DateTime CreatedOnUtc, string Content)
6 | {
7 |
8 | public static explicit operator GetRepliesQueryResponse(Reply Reply)
9 | => new GetRepliesQueryResponse(Reply.Client.FullName, Reply.CreatedOnUtc, Reply.Content);
10 | }
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/GetPopularArticles/GetPopularArticlesMappingProfile.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.GetPopularArticles;
2 |
3 | public class GetPopularArticlesMappingProfile : IRegister
4 | {
5 | public void Register(TypeAdapterConfig config)
6 | {
7 | config.ForType()
8 | .Map(x => x.ArticleId, src => src.ArticleId.ToString());
9 | }
10 | }
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/ReplyToCommet/ReplyToCommentRequestModel.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.ReplyToCommet;
2 |
3 | public record ReplyToCommentRequestModel([FromRoute(Name = "comment-id")] Guid CommentId,
4 | [FromBody] ReplyToCommentRequest body);
5 |
6 | public record ReplyToCommentRequest(string Content,
7 | string FullName,
8 | string Email);
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/MakeDraft/DraftAlreadyExistsException.cs:
--------------------------------------------------------------------------------
1 | using Blogger.BuildingBlocks.Domain;
2 |
3 | namespace Blogger.Application.Articles.MakeDraft;
4 |
5 | public sealed class DraftAlreadyExistsException : DomainException
6 | {
7 | private const string _messages = "Draft with Title `{0}` already exists.";
8 | public DraftAlreadyExistsException(string articleId)
9 | : base(string.Format(_messages, articleId))
10 | {
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/Blogger.IntegrationTests/Fixtures/BloggerDbContextFixture.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Infrastructure.Persistence;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 |
5 | namespace Blogger.IntegrationTests.Fixtures;
6 |
7 | public class BloggerDbContextFixture : EfDatabaseBaseFixture
8 | {
9 | protected override BloggerDbContext BuildDbContext(DbContextOptions options)
10 | {
11 | return new BloggerDbContext(options);
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Subscribers/Subscribe/SubscribeMappingProfile.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.SubscriberAggregate;
2 |
3 | namespace Blogger.APIs.Endpoints.Subscribers.Subscribe;
4 |
5 | public class SubscribeMappingProfile : IRegister
6 | {
7 | public void Register(TypeAdapterConfig config)
8 | {
9 | config.ForType()
10 | .Map(x => x.SubscriberId, src => SubscriberId.Create(src.Email));
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "ConnectionStrings": {
9 | "SvcDbContext": "data source=.;initial catalog=thisisnabi.blogger;TrustServerCertificate=True;Trusted_Connection=True;"
10 | },
11 | "EmailSettings": {
12 | "From": "",
13 | "SmtpHost": "smtp.gmail.com",
14 | "SmtpPort": 587
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/ApproveReply/ApproveReplyRequestValidator.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.ApproveReply;
2 |
3 | public class ApproveReplyRequestValidator : AbstractValidator
4 | {
5 | public ApproveReplyRequestValidator()
6 | {
7 | RuleFor(x => x.CommentId)
8 | .NotEmpty()
9 | .NotNull();
10 |
11 | RuleFor(x => x.Link)
12 | .NotEmpty()
13 | .NotNull();
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/UpdateDraft/DraftTitleDuplicatedException.cs:
--------------------------------------------------------------------------------
1 | using Blogger.BuildingBlocks.Domain;
2 |
3 |
4 | namespace Blogger.Application.Articles.UpdateDraft;
5 | public class DraftTitleDuplicatedException : DomainException
6 | {
7 | private const string _message = "A draft with the same title already exists. Draft title: {0}";
8 |
9 | public DraftTitleDuplicatedException(string title) : base(string.Format(_message, title))
10 | {
11 |
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/CreateArticle/ArticleAlreadyExistsException.cs:
--------------------------------------------------------------------------------
1 | using Blogger.BuildingBlocks.Domain;
2 |
3 | namespace Blogger.Application.Articles.CreateArticle;
4 |
5 | public sealed class ArticleAlreadyExistsException : DomainException
6 | {
7 | private const string _messages = "Article with Title `{0}` already exists.";
8 | public ArticleAlreadyExistsException(string articleId)
9 | : base(string.Format(_messages, articleId))
10 | {
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Blogger.Domain/SubscriberAggregate/ISubscriberRepository.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace Blogger.Domain.SubscriberAggregate;
3 |
4 | public interface ISubscriberRepository
5 | {
6 | Task CreateAsync(Subscriber subscriber, CancellationToken cancellationToken);
7 | Task FindByIdAsync(SubscriberId subscriberId);
8 | Task IsExistsAsync(SubscriberId subscriberId, CancellationToken cancellationToken);
9 | Task SavaChangesAsync(CancellationToken cancellationToken);
10 | }
11 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/ApproveReply/ApproveReplyMappingProfile.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.ApproveReply;
2 |
3 | public class ApproveReplyMappingProfile : IRegister
4 | {
5 | public void Register(TypeAdapterConfig config)
6 | {
7 | config.ForType()
8 | .Map(x => x.CommentId, src => CommentId.Create(src.CommentId))
9 | .Map(x => x.Link, src => src.Link);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/GetReplies/GetRepliesMappingProfile.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.GetReplies;
2 |
3 | public class GetRepliesMappingProfile : IRegister
4 | {
5 | public void Register(TypeAdapterConfig config)
6 | {
7 | config.ForType()
8 | .Map(x => x.CommentId, src => CommentId.Create(src.CommentId));
9 |
10 | config.ForType();
11 | }
12 | }
--------------------------------------------------------------------------------
/src/Blogger.Application/Comments/GetComments/GetCommentsQueryResponse.cs:
--------------------------------------------------------------------------------
1 | using Blogger.Domain.CommentAggregate;
2 |
3 | namespace Blogger.Application.Comments.GetComments;
4 |
5 | public record GetCommentsQueryResponse(CommentId CommentId,string FullName, DateTime CreatedOnUtc, string Content)
6 | {
7 |
8 | public static explicit operator GetCommentsQueryResponse(Comment comment)
9 | => new GetCommentsQueryResponse(comment.Id,comment.Client.FullName, comment.CreatedOnUtc, comment.Content);
10 | }
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.classpath
2 | **/.dockerignore
3 | **/.env
4 | **/.git
5 | **/.gitignore
6 | **/.project
7 | **/.settings
8 | **/.toolstarget
9 | **/.vs
10 | **/.vscode
11 | **/*.*proj.user
12 | **/*.dbmdl
13 | **/*.jfm
14 | **/azds.yaml
15 | **/bin
16 | **/charts
17 | **/docker-compose*
18 | **/Dockerfile*
19 | **/node_modules
20 | **/npm-debug.log
21 | **/obj
22 | **/secrets.dev.yaml
23 | **/values.dev.yaml
24 | LICENSE
25 | README.md
26 | !**/.gitignore
27 | !.git/HEAD
28 | !.git/config
29 | !.git/packed-refs
30 | !.git/refs/heads/**
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | enable
5 | enable
6 | true
7 |
8 | latest
9 | latest-minimum
10 | minimum
11 |
12 | 0.1.0
13 |
14 | $(NoWarn);IDE0290
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Subscribers/SubscriberService.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Subscribers;
2 |
3 | public class SubscriberService(ISubscriberRepository subscriberRepository) : ISubscriberService
4 | {
5 | private readonly ISubscriberRepository _subscriberRepository = subscriberRepository;
6 |
7 | public async Task IsDuplicated(SubscriberId subscriberId, CancellationToken cancellationToken)
8 | {
9 | var exists = await _subscriberRepository.IsExistsAsync(subscriberId, cancellationToken);
10 | return exists;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Comments/GetComments/GetCommentsMappingProfile.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Comments.GetComments;
2 |
3 | public class GetCommentsMappingProfile : IRegister
4 | {
5 | public void Register(TypeAdapterConfig config)
6 | {
7 | config.ForType()
8 | .Map(x => x.ArticleId, src => ArticleId.Create(src.ArticleId));
9 |
10 | config.ForType()
11 | .Map(x => x.Id, src => src.CommentId.ToString());
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Blogger.APIs/Endpoints/Articles/UpdateDraft/UpdateDraftMappingProfile.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.APIs.Endpoints.Articles.UpdateDraft;
2 |
3 | public class UpdateDraftMappingProfile : IRegister
4 | {
5 | public void Register(TypeAdapterConfig config)
6 | {
7 | config.ForType()
8 | .Map(x => x.DraftId, src => ArticleId.Create(src.DraftId))
9 | .Map(x => x.Tags, src => src.Tags.Select(x => Tag.Create(x))
10 | .ToImmutableList());
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Blogger.Application/Articles/ArticleService.cs:
--------------------------------------------------------------------------------
1 | namespace Blogger.Application.Articles;
2 | public class ArticleService(IArticleRepository articleRepository) : IArticleService
3 | {
4 | public Task HasIdAsync(ArticleId articleId, CancellationToken cancellationToken)
5 | {
6 | return IsArticleIdValidAsync(articleId, cancellationToken);
7 | }
8 |
9 | public async Task IsArticleIdValidAsync(ArticleId articleId, CancellationToken cancellationToken)
10 | {
11 | return await articleRepository.GetArticleByIdAsync(articleId, cancellationToken) is not null;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Blogger.Domain/ArticleAggregate/Tag.cs:
--------------------------------------------------------------------------------
1 | using Humanizer;
2 |
3 | namespace Blogger.Domain.ArticleAggregate;
4 | public class Tag : ValueObject
5 | {
6 | public string Value { get; init; } = null!;
7 |
8 | public override IEnumerable