From 09cc4a0e575fc99b338cd5d39f3762f06960d5fe Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 13 Mar 2026 01:02:16 +0200 Subject: [PATCH 01/59] Add better documentation to Accounts Api Endpoints --- .../ApiEndpoints/AccountsApiEndpoints.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/AccountsApiEndpoints.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/AccountsApiEndpoints.cs index 6f36859f..63f278f8 100644 --- a/BlogWebApp/BlogMinimalApi/ApiEndpoints/AccountsApiEndpoints.cs +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/AccountsApiEndpoints.cs @@ -82,8 +82,12 @@ public void InstallApiRoutes(WebApplication app) ? Results.BadRequest("Invalid name or password") : Results.Ok(jwt); }) - .Produces() - .Produces(400); + .WithName("Login") + .Produces(200) + .Produces(400) + .WithSummary("Login") + .WithDescription("Login") + .HasApiVersion(1.0); // Register @@ -118,8 +122,12 @@ public void InstallApiRoutes(WebApplication app) return Results.NoContent(); }) + .WithName("Register") .Produces(204) - .Produces(400); + .Produces(400) + .WithSummary("Register") + .WithDescription("Register") + .HasApiVersion(1.0); // ------------------------- // PRIVATE ENDPOINTS From bf51d0c79afdb5cbc8226259d70dcd3767671504 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 13 Mar 2026 01:02:47 +0200 Subject: [PATCH 02/59] Add better documentation to Comments Api Endpoints --- .../BlogMinimalApi/ApiEndpoints/CommentsApiEndpoints.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/CommentsApiEndpoints.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/CommentsApiEndpoints.cs index f4c88d5c..f7f69e78 100644 --- a/BlogWebApp/BlogMinimalApi/ApiEndpoints/CommentsApiEndpoints.cs +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/CommentsApiEndpoints.cs @@ -148,8 +148,12 @@ public void InstallApiRoutes(WebApplication app) Id = comment.Id }); }) + .WithName("CreateComment") .Produces>(201) - .Produces(400); + .Produces(400) + .WithSummary("Create comment") + .WithDescription("Create comment") + .HasApiVersion(1.0); // Edit Comment From d22a5e907d8fbefd2dd9d1f55d3495d5c133eaa3 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 13 Mar 2026 01:03:46 +0200 Subject: [PATCH 03/59] Add Health check Api Endpoints --- .../BlogMinimalApi/ApiEndpoints/HealthCheckApiEndpoints.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/HealthCheckApiEndpoints.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/HealthCheckApiEndpoints.cs index 7da6b835..79ac7e46 100644 --- a/BlogWebApp/BlogMinimalApi/ApiEndpoints/HealthCheckApiEndpoints.cs +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/HealthCheckApiEndpoints.cs @@ -31,7 +31,7 @@ public void InstallApiRoutes(WebApplication app) publicGroup.MapGet("/health", async (HealthCheckService healthCheckService) => { var report = await healthCheckService.CheckHealthAsync(); - + var response = new { Status = report.Status.ToString(), From 9b505fc599406f6130abd91dc297fa3f6e1eced4 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 13 Mar 2026 01:04:21 +0200 Subject: [PATCH 04/59] Add better documentation to Messages Api Endpoints --- .../BlogMinimalApi/ApiEndpoints/MessagesApiEndpoints.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/MessagesApiEndpoints.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/MessagesApiEndpoints.cs index f85d66b4..1760b36e 100644 --- a/BlogWebApp/BlogMinimalApi/ApiEndpoints/MessagesApiEndpoints.cs +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/MessagesApiEndpoints.cs @@ -130,8 +130,12 @@ public void InstallApiRoutes(WebApplication app) return Results.Created($"/messages/{message.Id}", null); }) - .Produces(201) - .Produces(400); + .WithName("CreateMessage") + .Produces>(201) + .Produces(400) + .WithSummary("Create message") + .WithDescription("Create message") + .HasApiVersion(1.0); // UPDATE From 9af35e661f761cf223595f15a2829f801a48707a Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 14 Mar 2026 01:04:54 +0200 Subject: [PATCH 05/59] Add better documentation to Posts Api Endpoints --- BlogWebApp/BlogMinimalApi/ApiEndpoints/PostsApiEndpoints.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/PostsApiEndpoints.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/PostsApiEndpoints.cs index 4d0533fe..b89bb887 100644 --- a/BlogWebApp/BlogMinimalApi/ApiEndpoints/PostsApiEndpoints.cs +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/PostsApiEndpoints.cs @@ -167,8 +167,12 @@ public void InstallApiRoutes(WebApplication app) $"{ApiRoutes.PostsController.Posts}/{post.Id}", new CreatedResponse { Id = post.Id }); }) + .WithName("CreatePost") .Produces>(201) - .Produces(400); + .Produces(400) + .WithSummary("Create post") + .WithDescription("Create post") + .HasApiVersion(1.0); // LIKE POST From 2fdb91d18e0e7ffc5f705f1387210cce39314c4d Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 14 Mar 2026 01:07:52 +0200 Subject: [PATCH 06/59] Created Blog vertical slice CQRS Minimal Api project --- .gitignore | 2 + .../BlogVerticalSliceCQRSMinimalApi.csproj | 31 ++++++++++++++ ...logVerticalSliceCQRSMinimalApi.csproj.user | 9 ++++ .../BlogVerticalSliceCQRSMinimalApi.http | 6 +++ .../Properties/launchSettings.json | 23 ++++++++++ .../appsettings.Development.json | 8 ++++ .../appsettings.json | 42 +++++++++++++++++++ 7 files changed, 121 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj.user create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.http create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Properties/launchSettings.json create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/appsettings.Development.json create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/appsettings.json diff --git a/.gitignore b/.gitignore index 4f5f2068..0e18dbb8 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,5 @@ /BlogWebApp/Aspire/Blog.Aspire/Blog.Aspire.AppHost/bin/Debug/net10.0 /BlogWebApp/BlogMinimalApi/bin/Debug/net10.0 /BlogWebApp/BlogMinimalApi/obj +/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/bin/Debug/net10.0 +/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/obj diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj new file mode 100644 index 00000000..e307234f --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + enable + enable + + + + true + $(NoWarn);1591 + + + + + + + + + + + + + + + + + + + + diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj.user b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj.user new file mode 100644 index 00000000..983ecfc0 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj.user @@ -0,0 +1,9 @@ + + + + http + + + ProjectDebugger + + \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.http b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.http new file mode 100644 index 00000000..3c344697 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.http @@ -0,0 +1,6 @@ +@BlogVerticalSliceCQRSMinimalApi_HostAddress = http://localhost:5037 + +GET {{BlogVerticalSliceCQRSMinimalApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Properties/launchSettings.json b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Properties/launchSettings.json new file mode 100644 index 00000000..28ec1b79 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5037", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7197;http://localhost:5037", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/appsettings.Development.json b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/appsettings.json b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/appsettings.json new file mode 100644 index 00000000..5734185b --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/appsettings.json @@ -0,0 +1,42 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=.;Database=BlogAspNetCoreNew;Integrated Security=False;Persist Security Info=False;User ID=1;Password=1111;Encrypt=false;TrustServerCertificate=True" + }, + + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + }, + + "JwtIssuerOptions": { + "Issuer": "BlogWebApi", + "Audience": "http://localhost:5037", + "Secret": "iNivDmHLpUA223sqsfhqGbMRdRj1PVkH", + "TokenLifetime": "01:00:00" + }, + + "EmailExtensionOptions": { + "BaseUrl": "http://localhost:4200/", + "From": "blog.asp.net.core@gmail.com" + }, + + "SmtpOptions": { + "Host": "smtp.gmail.com", + "Port": 587, + "UserName": "", + "Password": "", + "EnableSsl": true + }, + "SwaggerOptions": { + "JsonRoute": "swagger/{documentName}/swagger.json", + "Description": "Blog Web Api", + "UIEndpoint": "v1/swagger.json" + }, + + "CacheInstaller": { + "Enabled": true, + "ConnectionString": "localhost" + } +} From 290e6e6d652a9c3d0665d4f0fd0337de3b064092 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 14 Mar 2026 01:08:59 +0200 Subject: [PATCH 07/59] Add Cache setups --- .../Cache/CachedAttribute.cs | 88 +++++++++++++++++++ .../CacheInstaller.cs | 32 +++++++ 2 files changed, 120 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Cache/CachedAttribute.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/CacheInstaller.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Cache/CachedAttribute.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Cache/CachedAttribute.cs new file mode 100644 index 00000000..91e33d94 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Cache/CachedAttribute.cs @@ -0,0 +1,88 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Cache; + +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Blog.CommonServices.Interfaces; +using Blog.Core.Configuration; + +/// +/// Cached attribute. +/// +/// +/// +/// +/// Initializes a new instance of the class. +/// +/// The lifetime seconds. +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class CachedAttribute(int lifeTimeSeconds) + : Attribute, IAsyncActionFilter +{ + /// + /// The lifetime seconds. + /// + private readonly int _lifeTimeSeconds = lifeTimeSeconds; + + /// + /// Called asynchronously before the action, after model binding is complete. + /// + /// The . + /// The . Invoked to execute the next action filter or the action itself. + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var cacheSettings = context.HttpContext.RequestServices.GetRequiredService(); + if (!cacheSettings.Enabled) + { + await next(); + return; + } + + var cachedService = context.HttpContext.RequestServices.GetRequiredService(); + var cachedKey = GenerateCacheKeyFromRequest(context.HttpContext.Request); + + var cachedResponse = await cachedService.GetCachedResponseAsync(cachedKey); + if (!string.IsNullOrEmpty(cachedResponse)) + { + var contentResult = new ContentResult + { + Content = cachedResponse, + ContentType = "application/json", + StatusCode = 200, + }; + + context.Result = contentResult; + + return; + } + + var executedContext = await next(); + if (executedContext.Result is OkObjectResult okObjectResult) + { + await cachedService.CacheResponseAsync(cachedKey, okObjectResult.Value, TimeSpan.FromSeconds(_lifeTimeSeconds)); + } + } + + /// + /// Generates the cache key from request. + /// + /// The request. + /// + private static string GenerateCacheKeyFromRequest(HttpRequest request) + { + var keyBuilder = new StringBuilder(); + keyBuilder.Append($"{request.Path}"); + + foreach (var (key, value) in request.Query.OrderBy(x => x.Key)) + { + keyBuilder.Append($"{key}-{value}"); + } + + return keyBuilder.ToString(); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/CacheInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/CacheInstaller.cs new file mode 100644 index 00000000..51bf0522 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/CacheInstaller.cs @@ -0,0 +1,32 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Blog.Core.Configuration; + +/// +/// Cache installer. +/// +/// +public class CacheInstaller : IInstaller +{ + /// + /// Installs the services. + /// + /// The services. + /// The configuration. + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + var redisCacheSettings = new RedisCacheConfiguration(); + configuration.GetSection(nameof(RedisCacheConfiguration)).Bind(redisCacheSettings); + services.AddSingleton(redisCacheSettings); + + if (!redisCacheSettings.Enabled) + { + return; + } + + // services.AddSingleton(_ => ConnectionMultiplexer.Connect(redisCacheSettings.ConnectionString)); + services.AddStackExchangeRedisCache(options => options.Configuration = redisCacheSettings.ConnectionString); + } +} \ No newline at end of file From cc503d89d7be08c8615b7196b2f85617e1f581dc Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 14 Mar 2026 01:09:46 +0200 Subject: [PATCH 08/59] Add Swagger configuration --- .../ApplySummariesOperationFilter.cs | 30 +++++ .../SwaggerFilters/LowercaseDocumentFilter.cs | 28 +++++ .../SwaggerGroupOperationFilter.cs | 32 ++++++ .../SwaggerInstaller.cs | 104 ++++++++++++++++++ .../StartupConfigures/ConfigureSwagger.cs | 34 ++++++ 5 files changed, 228 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/ApplySummariesOperationFilter.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/LowercaseDocumentFilter.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/SwaggerGroupOperationFilter.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureSwagger.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/ApplySummariesOperationFilter.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/ApplySummariesOperationFilter.cs new file mode 100644 index 00000000..796ba5ca --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/ApplySummariesOperationFilter.cs @@ -0,0 +1,30 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Filters.SwaggerFilters; + +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.OpenApi; +using Swashbuckle.AspNetCore.SwaggerGen; + +/// +/// Swagger Method Info Generator from summary for +/// +public class ApplySummariesOperationFilter : IOperationFilter +{ + /// + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var controllerActionDescriptor = context.ApiDescription.ActionDescriptor as ControllerActionDescriptor; + if (controllerActionDescriptor == null) + { + return; + } + + var actionName = controllerActionDescriptor.ActionName; + if (actionName != "GetPaged") + { + return; + } + + var resourceName = controllerActionDescriptor.ControllerName; + operation.Summary = $"Returns paged list of the {resourceName} as IPagedList wrapped with OperationResult"; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/LowercaseDocumentFilter.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/LowercaseDocumentFilter.cs new file mode 100644 index 00000000..137d5f11 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/LowercaseDocumentFilter.cs @@ -0,0 +1,28 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Filters.SwaggerFilters; + +using Microsoft.OpenApi; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Linq; + +/// +/// Lowercase routes filter +/// +public class LowercaseDocumentFilter : IDocumentFilter +{ + /// + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + swaggerDoc.Paths = (OpenApiPaths)swaggerDoc.Paths.ToDictionary(entry => LowercaseEverythingButParameters(entry.Key), + entry => entry.Value); + } + + /// + /// Lowercase everything but parameters. + /// + /// The key. + /// string. + private static string LowercaseEverythingButParameters(string key) + { + return string.Join('/', key.Split('/').Select(x => x.Contains("{") ? x : x.ToLower())); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/SwaggerGroupOperationFilter.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/SwaggerGroupOperationFilter.cs new file mode 100644 index 00000000..bc310386 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/SwaggerFilters/SwaggerGroupOperationFilter.cs @@ -0,0 +1,32 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Filters.SwaggerFilters; + +using Blog.Core.Attributes.SwaggerAttributes; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.OpenApi; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Linq; + +/// +/// Swagger operation filter for +/// +public class SwaggerGroupOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (context.ApiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor) + return; + + var attributes = controllerActionDescriptor.EndpointMetadata + .OfType() + .ToList(); + + var tagName = attributes.Count != 0 + ? attributes.First().GroupName + : controllerActionDescriptor.RouteValues["controller"]; + + operation.Tags = new HashSet + { + new (tagName) + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs new file mode 100644 index 00000000..1bf7f3ea --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs @@ -0,0 +1,104 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers +{ + using Asp.Versioning; + using Blog.Contracts.V1; + using Blog.Core.Consts; + using Filters.SwaggerFilters; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.OpenApi; + using Swashbuckle.AspNetCore.Filters; + using System; + using System.IO; + using System.Linq; + using System.Reflection; + + + /// + /// Swagger installer. + /// + /// + public class SwaggerInstaller : IInstaller + { + /// + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + // Swagger Configuration + services + .AddApiVersioning(options => + { + options.ReportApiVersions = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = ApiVersionReader.Combine( + new UrlSegmentApiVersionReader(), + new HeaderApiVersionReader("x-api-version"), + new MediaTypeApiVersionReader("x-api-version")); + }) + .AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }); + //services.ConfigureOptions(); + services.AddSwaggerGen(c => + { + c.ExampleFilters(); + + c.SwaggerDoc("v1", new OpenApiInfo + { + Version = ApiRoutes.Version, + Title = Consts.ApplicationName, + Description = SwaggerConsts.Description, + TermsOfService = new Uri(SwaggerConsts.TermsOfService), + Contact = new OpenApiContact + { + Name = SwaggerConsts.Contact.Name, + Email = SwaggerConsts.Contact.Email, + Url = new Uri(SwaggerConsts.Contact.Url), + }, + License = new OpenApiLicense + { + Name = SwaggerConsts.License.Name, + Url = new Uri(SwaggerConsts.License.Url), + } + }); + + c.ResolveConflictingActions(x => x.First()); + + var url = configuration.GetSection("IdentityServer").GetValue("Url"); + + c.AddSecurityDefinition(SwaggerConsts.SecurityDefinition.Name, new OpenApiSecurityScheme + { + Description = SwaggerConsts.SecurityDefinition.OpenApiSecurityScheme.Description, + Name = SwaggerConsts.SecurityDefinition.OpenApiSecurityScheme.Name, + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = SwaggerConsts.SecurityDefinition.OpenApiSecurityScheme.Scheme, + }); + + /*c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = SwaggerConstants.SecurityRequirement.OpenApiSecurityRequirement.OpenApiReference.Id, + Type = ReferenceType.SecurityScheme + } + }, new List() + } + });*/ + + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath); + + c.OperationFilter(); + }); + + services.AddSwaggerExamplesFromAssemblyOf(); + } + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureSwagger.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureSwagger.cs new file mode 100644 index 00000000..3f6982ff --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureSwagger.cs @@ -0,0 +1,34 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigures; + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Swashbuckle.AspNetCore.SwaggerUI; +using Blog.Core.Configuration; + +/// +/// Configure swagger. +/// +public static class ConfigureSwagger +{ + /// + /// Configures the specified application. + /// + /// The application. + /// The configuration. + public static void Configure(IApplicationBuilder app, IConfiguration configuration) + { + var swaggerOptions = new SwaggerOptions(); + configuration.GetSection(nameof(SwaggerOptions)).Bind(swaggerOptions); + app.UseSwagger(option => { option.RouteTemplate = swaggerOptions.JsonRoute; }); + app.UseSwaggerUI(option => + { + option.SwaggerEndpoint(swaggerOptions.UiEndpoint, swaggerOptions.Description); + //option.HeadContent = $"{Blog.Web.Assembly.Git.Branch.ToUpper()} {BlogAssembly.Git.Commit.ToUpper()}"; + option.DefaultModelExpandDepth(0); + option.DefaultModelRendering(ModelRendering.Model); + option.DefaultModelsExpandDepth(0); + option.DocExpansion(DocExpansion.None); + option.DisplayRequestDuration(); + }); + } +} \ No newline at end of file From 52ac24299af975d354136df247012082762f7c07 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 15 Mar 2026 01:10:13 +0200 Subject: [PATCH 09/59] Add validation filter --- .../Filters/ValidationFilter.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/ValidationFilter.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/ValidationFilter.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/ValidationFilter.cs new file mode 100644 index 00000000..7314aedb --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Filters/ValidationFilter.cs @@ -0,0 +1,54 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Filters; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Linq; +using System.Threading.Tasks; +using Blog.Contracts; +using Blog.Contracts.V1.Responses; + +/// +/// Validation filter. +/// +/// +public class ValidationFilter : IAsyncActionFilter +{ + /// + /// Called asynchronously before the action, after model binding is complete. + /// + /// The . + /// The . Invoked to execute the next action filter or the action itself. + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // Before controller. + if (!context.ModelState.IsValid) + { + var errorsInModelState = context.ModelState + .Where(x => x.Value.Errors.Count > 0) + .ToDictionary(keyValueParam => keyValueParam.Key, keyValueParam => keyValueParam.Value.Errors.Select(x => x.ErrorMessage)).ToArray(); + + var errorResponse = new ErrorResponse(); + + foreach (var error in errorsInModelState) + { + foreach (var subError in error.Value) + { + var errorModel = new ErrorModel + { + FieldName = error.Key, + Message = subError + }; + + errorResponse.Errors.Add(errorModel); + } + } + + context.Result = new BadRequestObjectResult(errorResponse); + + return; + } + + await next(); + // After controller. + } +} \ No newline at end of file From c006ff2812d4d205b095d3cd93539e0eda511e1f Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 15 Mar 2026 01:10:52 +0200 Subject: [PATCH 10/59] Add Swagger extentions --- .../CreateCommentRequestExample.cs | 24 +++ .../UpdateCommentRequestExample.cs | 27 +++ .../CreateMessageRequestExample.cs | 24 +++ .../UpdateMessageRequestExample.cs | 26 +++ .../PostsRequests/CreatePostRequestExample.cs | 34 ++++ .../PostsRequests/UpdatePostRequestExample.cs | 38 ++++ .../SearchParametersRequestExample.cs | 28 +++ .../Requests/SortParametersRequestExample.cs | 24 +++ .../TagsRequests/CreateTagRequestExample.cs | 21 +++ .../TagsRequests/TagRequestExample.cs | 21 +++ .../TagsRequests/UpdateTagRequestExample.cs | 21 +++ .../ChangePasswordRequestExample.cs | 23 +++ .../UsersRequests/LoginRequestExample.cs | 22 +++ .../RegistrationRequestExample.cs | 32 ++++ .../UpdateProfileRequestExample.cs | 26 +++ .../CommentResponseExample.cs | 26 +++ .../PagedCommentsResponseExample.cs | 49 +++++ .../Responses/CreatedResponseExample.cs | 22 +++ .../Responses/MessageResponseExample.cs | 60 ++++++ .../Responses/PageInfoResponseExample.cs | 22 +++ .../PagedPostsResponseExample.cs | 175 ++++++++++++++++++ .../PostsResponses/PostResponseExample.cs | 71 +++++++ .../PostTagRelationsResponseExample.cs | 37 ++++ .../PostsResponses/PostViewResponseExample.cs | 67 +++++++ .../PostWithPagedCommentsResponseExample.cs | 80 ++++++++ .../TagsResponses/PagedTagsResponseExample.cs | 49 +++++ .../TagsResponses/TagResponseExample.cs | 22 +++ .../UsersResponses/AccountResponseExample.cs | 36 ++++ .../ApplicationUserResponseExample.cs | 34 ++++ .../UsersResponses/ProfileResponseExample.cs | 42 +++++ .../UsersResponses/RoleResponseExample.cs | 23 +++ 31 files changed, 1206 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/CommentsRequests/CreateCommentRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/CommentsRequests/UpdateCommentRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/MessagesRequests/CreateMessageRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/MessagesRequests/UpdateMessageRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/PostsRequests/CreatePostRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/PostsRequests/UpdatePostRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/SearchParametersRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/SortParametersRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/CreateTagRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/TagRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/UpdateTagRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/ChangePasswordRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/LoginRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/RegistrationRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/UpdateProfileRequestExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CommentsResponses/CommentResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CommentsResponses/PagedCommentsResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CreatedResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/MessageResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PageInfoResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PagedPostsResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostTagRelationsResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostViewResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostWithPagedCommentsResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/TagsResponses/PagedTagsResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/TagsResponses/TagResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/AccountResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/ApplicationUserResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/ProfileResponseExample.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/RoleResponseExample.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/CommentsRequests/CreateCommentRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/CommentsRequests/CreateCommentRequestExample.cs new file mode 100644 index 00000000..ad68e9bc --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/CommentsRequests/CreateCommentRequestExample.cs @@ -0,0 +1,24 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.CommentsRequests; + +using System; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests.CommentsRequests; +using Blog.Core.Consts; + +/// +/// Create comment request example. +/// +/// +public class CreateCommentRequestExample : IExamplesProvider +{ + /// + public CreateCommentRequest GetExamples() + { + return new CreateCommentRequest + { + PostId = 0, + CommentBody = SwaggerExamplesConsts.CreateCommentRequestExample.CommentBody, + UserId = Guid.NewGuid().ToString(), + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/CommentsRequests/UpdateCommentRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/CommentsRequests/UpdateCommentRequestExample.cs new file mode 100644 index 00000000..0bb7d97b --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/CommentsRequests/UpdateCommentRequestExample.cs @@ -0,0 +1,27 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.CommentsRequests; + +using System; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests.CommentsRequests; +using Blog.Core.Consts; + +/// +/// Update comment request example. +/// +/// +public class UpdateCommentRequestExample : IExamplesProvider +{ + /// + public UpdateCommentRequest GetExamples() + { + return new UpdateCommentRequest + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.UpdateCommentRequestExample.CommentBody, + UserId = Guid.NewGuid().ToString(), + Likes = 10, + Dislikes = 10, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/MessagesRequests/CreateMessageRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/MessagesRequests/CreateMessageRequestExample.cs new file mode 100644 index 00000000..0279f74f --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/MessagesRequests/CreateMessageRequestExample.cs @@ -0,0 +1,24 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.MessagesRequests; + +using System; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests.MessagesRequests; +using Blog.Core.Consts; + +/// +/// Create message request example. +/// +/// +public class CreateMessageRequestExample : IExamplesProvider +{ + /// + public CreateMessageRequest GetExamples() => + new () + { + SenderId = Guid.NewGuid().ToString(), + RecipientId = Guid.NewGuid().ToString(), + Subject = SwaggerExamplesConsts.CreateMessageRequestExample.Subject, + Body = SwaggerExamplesConsts.CreateMessageRequestExample.Body, + MessageType = 0, + }; +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/MessagesRequests/UpdateMessageRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/MessagesRequests/UpdateMessageRequestExample.cs new file mode 100644 index 00000000..80f01eb7 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/MessagesRequests/UpdateMessageRequestExample.cs @@ -0,0 +1,26 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.MessagesRequests; + +using System; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests.MessagesRequests; +using Blog.Core.Consts; + +/// +/// Update message request example. +/// +/// +public class UpdateMessageRequestExample : IExamplesProvider +{ + /// + public UpdateMessageRequest GetExamples() + { + return new UpdateMessageRequest + { + SenderId = Guid.NewGuid().ToString(), + RecipientId = Guid.NewGuid().ToString(), + Subject = SwaggerExamplesConsts.UpdateMessageRequestExample.Subject, + Body = SwaggerExamplesConsts.UpdateMessageRequestExample.Body, + MessageType = 0, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/PostsRequests/CreatePostRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/PostsRequests/CreatePostRequestExample.cs new file mode 100644 index 00000000..d86a36d5 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/PostsRequests/CreatePostRequestExample.cs @@ -0,0 +1,34 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.PostsRequests; + +using Swashbuckle.AspNetCore.Filters; +using System; +using System.Collections.Generic; +using Blog.Contracts.V1.Requests.PostsRequests; +using Blog.Contracts.V1.Requests.TagsRequests; +using Blog.Core.Consts; + +/// +/// Create post request example. +/// +/// +public class CreatePostRequestExample : IExamplesProvider +{ + /// + public CreatePostRequest GetExamples() + { + return new CreatePostRequest + { + Title = SwaggerExamplesConsts.CreatePostRequestExample.Title, + Description = SwaggerExamplesConsts.CreatePostRequestExample.Description, + Content = SwaggerExamplesConsts.CreatePostRequestExample.Content, + ImageUrl = SwaggerExamplesConsts.CreatePostRequestExample.ImageUrl, + AuthorId = Guid.NewGuid().ToString(), + Tags = new List + { + new () { Title = SwaggerExamplesConsts.CreateTagRequestExample.Title + "1" }, + new () { Title = SwaggerExamplesConsts.CreateTagRequestExample.Title + "2" }, + new () { Title = SwaggerExamplesConsts.CreateTagRequestExample.Title + "3" }, + }, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/PostsRequests/UpdatePostRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/PostsRequests/UpdatePostRequestExample.cs new file mode 100644 index 00000000..b473571d --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/PostsRequests/UpdatePostRequestExample.cs @@ -0,0 +1,38 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.PostsRequests; + +using Swashbuckle.AspNetCore.Filters; +using System; +using System.Collections.Generic; +using Blog.Contracts.V1.Requests.PostsRequests; +using Blog.Contracts.V1.Requests.TagsRequests; +using Blog.Core.Consts; + +/// +/// Update post request example. +/// +/// +public class UpdatePostRequestExample : IExamplesProvider +{ + /// + public UpdatePostRequest GetExamples() + { + return new UpdatePostRequest + { + Id = 1, + Title = SwaggerExamplesConsts.UpdatePostRequestExample.Title, + Description = SwaggerExamplesConsts.UpdatePostRequestExample.Description, + Content = SwaggerExamplesConsts.UpdatePostRequestExample.Content, + ImageUrl = SwaggerExamplesConsts.UpdatePostRequestExample.ImageUrl, + AuthorId = Guid.NewGuid().ToString(), + Seen = 10, + Likes = 10, + Dislikes = 10, + Tags = new List + { + new () { Title = SwaggerExamplesConsts.UpdateTagRequestExample.Title + "1" }, + new () { Title = SwaggerExamplesConsts.UpdateTagRequestExample.Title + "2" }, + new () { Title = SwaggerExamplesConsts.UpdateTagRequestExample.Title + "3" }, + }, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/SearchParametersRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/SearchParametersRequestExample.cs new file mode 100644 index 00000000..5e2cc5a8 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/SearchParametersRequestExample.cs @@ -0,0 +1,28 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests; + +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests; +using Blog.Core.Consts; + +/// +/// Search parameters request example. +/// +/// +public class SearchParametersRequestExample : IExamplesProvider +{ + /// + public SearchParametersRequest GetExamples() + { + return new SearchParametersRequest + { + Search = SwaggerExamplesConsts.SearchParametersRequestExample.Search, + SortParameters = new SortParametersRequest + { + OrderBy = SwaggerExamplesConsts.SortParametersRequestExample.OrderBy, + SortBy = SwaggerExamplesConsts.SortParametersRequestExample.SortBy, + CurrentPage = SwaggerExamplesConsts.SortParametersRequestExample.CurrentPage, + PageSize = SwaggerExamplesConsts.SortParametersRequestExample.PageSize, + }, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/SortParametersRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/SortParametersRequestExample.cs new file mode 100644 index 00000000..af057758 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/SortParametersRequestExample.cs @@ -0,0 +1,24 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests; + +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests; +using Blog.Core.Consts; + +/// +/// Sort parameters request example. +/// +/// +public class SortParametersRequestExample : IExamplesProvider +{ + /// + public SortParametersRequest GetExamples() + { + return new SortParametersRequest + { + OrderBy = SwaggerExamplesConsts.SortParametersRequestExample.OrderBy, + SortBy = SwaggerExamplesConsts.SortParametersRequestExample.SortBy, + CurrentPage = SwaggerExamplesConsts.SortParametersRequestExample.CurrentPage, + PageSize = SwaggerExamplesConsts.SortParametersRequestExample.PageSize, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/CreateTagRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/CreateTagRequestExample.cs new file mode 100644 index 00000000..7ab3dc39 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/CreateTagRequestExample.cs @@ -0,0 +1,21 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.TagsRequests; + +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests.TagsRequests; +using Blog.Core.Consts; + +/// +/// Create tag request example. +/// +/// +public class CreateTagRequestExample : IExamplesProvider +{ + /// + public CreateTagRequest GetExamples() + { + return new CreateTagRequest + { + Title = SwaggerExamplesConsts.CreateTagRequestExample.Title, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/TagRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/TagRequestExample.cs new file mode 100644 index 00000000..dfcaca06 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/TagRequestExample.cs @@ -0,0 +1,21 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.TagsRequests; + +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests.TagsRequests; +using Blog.Core.Consts; + +/// +/// Tag request example. +/// +/// +public class TagRequestExample : IExamplesProvider +{ + /// + public TagRequest GetExamples() + { + return new TagRequest + { + Title = SwaggerExamplesConsts.TagRequestExample.Title + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/UpdateTagRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/UpdateTagRequestExample.cs new file mode 100644 index 00000000..4d0d8ca5 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/TagsRequests/UpdateTagRequestExample.cs @@ -0,0 +1,21 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.TagsRequests; + +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests.TagsRequests; +using Blog.Core.Consts; + +/// +/// Update tag request example. +/// +/// +public class UpdateTagRequestExample : IExamplesProvider +{ + /// + public UpdateTagRequest GetExamples() + { + return new UpdateTagRequest + { + Title = SwaggerExamplesConsts.UpdateTagRequestExample.Title + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/ChangePasswordRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/ChangePasswordRequestExample.cs new file mode 100644 index 00000000..55db254b --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/ChangePasswordRequestExample.cs @@ -0,0 +1,23 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.UsersRequests; + +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests.UsersRequests; +using Blog.Core.Consts; + +/// +/// Change password request example. +/// +/// +public class ChangePasswordRequestExample : IExamplesProvider +{ + /// + public ChangePasswordRequest GetExamples() + { + return new ChangePasswordRequest + { + OldPassword = SwaggerExamplesConsts.AccountExample.Password, + NewPassword = SwaggerExamplesConsts.ChangePasswordRequestExample.NewPassword, + }; + + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/LoginRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/LoginRequestExample.cs new file mode 100644 index 00000000..24a9191a --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/LoginRequestExample.cs @@ -0,0 +1,22 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.UsersRequests; + +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests.UsersRequests; +using Blog.Core.Consts; + +/// +/// Login request example. +/// +/// +public class LoginRequestExample : IExamplesProvider +{ + /// + public LoginRequest GetExamples() + { + return new LoginRequest + { + Email = SwaggerExamplesConsts.AccountExample.Email, + Password = SwaggerExamplesConsts.AccountExample.Password, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/RegistrationRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/RegistrationRequestExample.cs new file mode 100644 index 00000000..c80646f9 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/RegistrationRequestExample.cs @@ -0,0 +1,32 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.UsersRequests; + +using System.Collections.Generic; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests.UsersRequests; +using Blog.Core.Consts; + +/// +/// Registration request example. +/// +/// +public class RegistrationRequestExample : IExamplesProvider +{ + /// + public RegistrationRequest GetExamples() + { + return new RegistrationRequest + { + Email = SwaggerExamplesConsts.AccountExample.Email, + Password = SwaggerExamplesConsts.AccountExample.Password, + ConfirmPassword = SwaggerExamplesConsts.AccountExample.Password, + FirstName = SwaggerExamplesConsts.AccountExample.FirstName, + LastName = SwaggerExamplesConsts.AccountExample.LastName, + UserName = SwaggerExamplesConsts.AccountExample.UserName, + PhoneNumber = SwaggerExamplesConsts.AccountExample.PhoneNumber, + Roles = new List + { + Consts.Roles.User + } + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/UpdateProfileRequestExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/UpdateProfileRequestExample.cs new file mode 100644 index 00000000..9ee34bb6 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Requests/UsersRequests/UpdateProfileRequestExample.cs @@ -0,0 +1,26 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Requests.UsersRequests; + +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Requests.UsersRequests; +using Blog.Core.Consts; + +/// +/// Update profile request example. +/// +/// +public class UpdateProfileRequestExample : IExamplesProvider +{ + /// + public UpdateProfileRequest GetExamples() + { + return new UpdateProfileRequest + { + Email = SwaggerExamplesConsts.UpdateProfileRequestExample.Email, + FirstName = SwaggerExamplesConsts.UpdateProfileRequestExample.FirstName, + LastName = SwaggerExamplesConsts.UpdateProfileRequestExample.LastName, + PhoneNumber = SwaggerExamplesConsts.UpdateProfileRequestExample.PhoneNumber, + Password = SwaggerExamplesConsts.UpdateProfileRequestExample.Password, + About = SwaggerExamplesConsts.UpdateProfileRequestExample.About + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CommentsResponses/CommentResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CommentsResponses/CommentResponseExample.cs new file mode 100644 index 00000000..77d76964 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CommentsResponses/CommentResponseExample.cs @@ -0,0 +1,26 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.CommentsResponses; + +using System; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses.CommentsResponses; +using Blog.Core.Consts; + +/// +/// Comment response example. +/// +/// +public class CommentResponseExample : IExamplesProvider +{ + /// + public CommentResponse GetExamples() + { + return new CommentResponse + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody, + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString(), + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CommentsResponses/PagedCommentsResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CommentsResponses/PagedCommentsResponseExample.cs new file mode 100644 index 00000000..a70a7bb3 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CommentsResponses/PagedCommentsResponseExample.cs @@ -0,0 +1,49 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.CommentsResponses; + +using System; +using System.Collections.Generic; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses; +using Blog.Contracts.V1.Responses.CommentsResponses; +using Blog.Core.Consts; + +/// +/// Paged comments response example. +/// +/// +public class PagedCommentsResponseExample : IExamplesProvider +{ + /// + public PagedCommentsResponse GetExamples() + { + return new PagedCommentsResponse + { + Comments = new List { + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "1", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "2", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + }, + + PageInfo = new PageInfoResponse + { + PageNumber = 1, + PageSize = 10, + TotalItems = 100 + }, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CreatedResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CreatedResponseExample.cs new file mode 100644 index 00000000..2fab3dcf --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/CreatedResponseExample.cs @@ -0,0 +1,22 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses; + +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses; + +/// +/// Created response example. +/// +/// +/// IExamplesProvider{CreatedResponse{int}} +/// +public class CreatedResponseExample : IExamplesProvider> +{ + /// + public CreatedResponse GetExamples() + { + return new CreatedResponse + { + Id = 0 + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/MessageResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/MessageResponseExample.cs new file mode 100644 index 00000000..70208e54 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/MessageResponseExample.cs @@ -0,0 +1,60 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses; + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Identity; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses; +using Blog.Contracts.V1.Responses.UsersResponses; +using Blog.Core.Consts; +using Blog.Core.Enums; + +/// +/// Message response example. +/// +/// +public class MessageResponseExample : IExamplesProvider +{ + /// + public MessageResponse GetExamples() + { + return new MessageResponse + { + SenderId = Guid.NewGuid().ToString(), + Sender = new ApplicationUserResponse + { + FirstName = SwaggerExamplesConsts.AccountExample.FirstName, + LastName = SwaggerExamplesConsts.AccountExample.LastName, + Email = SwaggerExamplesConsts.AccountExample.Email, + EmailConfirmed = true, + Roles = new List> + { + new () + { + RoleId = Guid.NewGuid().ToString() + } + } + }, + + RecipientId = Guid.NewGuid().ToString(), + Recipient = new ApplicationUserResponse + { + FirstName = SwaggerExamplesConsts.AccountExample.FirstName, + LastName = SwaggerExamplesConsts.AccountExample.LastName, + Email = SwaggerExamplesConsts.AccountExample.Email, + EmailConfirmed = true, + Roles = new List> + { + new () + { + RoleId = Guid.NewGuid().ToString() + } + } + }, + + Subject = SwaggerExamplesConsts.MessageResponseExample.Subject, + Body = SwaggerExamplesConsts.MessageResponseExample.Body, + MessageType = MessageType.MessageFoAdmins + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PageInfoResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PageInfoResponseExample.cs new file mode 100644 index 00000000..6557214e --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PageInfoResponseExample.cs @@ -0,0 +1,22 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses; + +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses; + +/// +/// Page info response example. +/// +/// +public class PageInfoResponseExample : IExamplesProvider +{ + /// + public PageInfoResponse GetExamples() + { + return new PageInfoResponse + { + PageNumber = 1, + PageSize = 10, + TotalItems = 100 + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PagedPostsResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PagedPostsResponseExample.cs new file mode 100644 index 00000000..0a5c8420 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PagedPostsResponseExample.cs @@ -0,0 +1,175 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.PostsResponses; + +using System; +using System.Collections.Generic; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses; +using Blog.Contracts.V1.Responses.CommentsResponses; +using Blog.Contracts.V1.Responses.PostsResponses; +using Blog.Contracts.V1.Responses.TagsResponses; +using Blog.Core.Consts; + +/// +/// Paged posts response example. +/// +/// +public class PagedPostsResponseExample : IExamplesProvider +{ + /// + public PagedPostsResponse GetExamples() + { + return new PagedPostsResponse + { + Posts = new List + { + new () + { + Id = 0, + Title = SwaggerExamplesConsts.PostViewResponseExample.Title + "1", + Description = SwaggerExamplesConsts.PostViewResponseExample.Description, + Content = SwaggerExamplesConsts.PostViewResponseExample.Content, + ImageUrl = SwaggerExamplesConsts.PostViewResponseExample.ImageUrl, + AuthorId = Guid.NewGuid().ToString(), + CommentsCount = 10, + + Comments = new List + { + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "1", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "2", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + }, + + Tags = new List + { + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagRequestExample.Title + "1" + }, + + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagRequestExample.Title + "2" + }, + }, + }, + + new () + { + Id = 0, + Title = SwaggerExamplesConsts.PostViewResponseExample.Title + "2", + Description = SwaggerExamplesConsts.PostViewResponseExample.Description, + Content = SwaggerExamplesConsts.PostViewResponseExample.Content, + ImageUrl = SwaggerExamplesConsts.PostViewResponseExample.ImageUrl, + AuthorId = Guid.NewGuid().ToString(), + CommentsCount = 10, + + Comments = new List + { + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "1", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "2", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + }, + + Tags = new List + { + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "1" + }, + + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "2" + } + } + }, + + new () + { + Id = 0, + Title = SwaggerExamplesConsts.PostViewResponseExample.Title + "3", + Description = SwaggerExamplesConsts.PostViewResponseExample.Description, + Content = SwaggerExamplesConsts.PostViewResponseExample.Content, + ImageUrl = SwaggerExamplesConsts.PostViewResponseExample.ImageUrl, + AuthorId = Guid.NewGuid().ToString(), + CommentsCount = 10, + + Comments = new List + { + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "1", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "2", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + } + }, + + Tags = new List + { + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "1" + }, + + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "2" + }, + }, + }, + }, + + PageInfo = new PageInfoResponse + { + PageNumber = 1, + PageSize = 10, + TotalItems = 100, + }, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostResponseExample.cs new file mode 100644 index 00000000..6962ce84 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostResponseExample.cs @@ -0,0 +1,71 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.PostsResponses; + +using System; +using System.Collections.Generic; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses.CommentsResponses; +using Blog.Contracts.V1.Responses.PostsResponses; +using Blog.Contracts.V1.Responses.TagsResponses; +using Blog.Core.Consts; + +/// +/// Post response example. +/// +/// +public class PostResponseExample : IExamplesProvider +{ + /// + public PostResponse GetExamples() + { + return new PostResponse + { + Title = SwaggerExamplesConsts.PostViewResponseExample.Title, + Description = SwaggerExamplesConsts.PostViewResponseExample.Description, + Content = SwaggerExamplesConsts.PostViewResponseExample.Content, + ImageUrl = SwaggerExamplesConsts.PostViewResponseExample.ImageUrl, + AuthorId = Guid.NewGuid().ToString(), + + Comments = new List + { + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "1", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "2", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + }, + + PostsTagsRelations = new List + { + new () + { + TagId = 0, + Tag = new TagResponse + { + Title = SwaggerExamplesConsts.TagResponseExample.Title + "1" + } + }, + + new () + { + TagId = 0, + Tag = new TagResponse + { + Title = SwaggerExamplesConsts.TagResponseExample.Title + "2" + } + } + }, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostTagRelationsResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostTagRelationsResponseExample.cs new file mode 100644 index 00000000..7f4da378 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostTagRelationsResponseExample.cs @@ -0,0 +1,37 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.PostsResponses; + +using System; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses.PostsResponses; +using Blog.Contracts.V1.Responses.TagsResponses; +using Blog.Core.Consts; + +/// +/// Post tag relations response example. +/// +/// +public class PostTagRelationsResponseExample : IExamplesProvider +{ + /// + public PostTagRelationsResponse GetExamples() + { + return new PostTagRelationsResponse + { + PostId = 0, + Post = new PostResponse + { + Title = SwaggerExamplesConsts.PostViewResponseExample.Title, + Description = SwaggerExamplesConsts.PostViewResponseExample.Description, + Content = SwaggerExamplesConsts.PostViewResponseExample.Content, + ImageUrl = SwaggerExamplesConsts.PostViewResponseExample.ImageUrl, + AuthorId = Guid.NewGuid().ToString() + }, + + TagId = 0, + Tag = new TagResponse + { + Title = SwaggerExamplesConsts.TagResponseExample.Title + "1" + } + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostViewResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostViewResponseExample.cs new file mode 100644 index 00000000..144e4e6a --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostViewResponseExample.cs @@ -0,0 +1,67 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.PostsResponses; + +using System; +using System.Collections.Generic; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses.CommentsResponses; +using Blog.Contracts.V1.Responses.PostsResponses; +using Blog.Contracts.V1.Responses.TagsResponses; +using Blog.Core.Consts; + +/// +/// Post view response example. +/// +/// +public class PostViewResponseExample : IExamplesProvider +{ + /// + public PostViewResponse GetExamples() + { + return new PostViewResponse + { + Id = 0, + Title = SwaggerExamplesConsts.PostViewResponseExample.Title, + Description = SwaggerExamplesConsts.PostViewResponseExample.Description, + Content = SwaggerExamplesConsts.PostViewResponseExample.Content, + ImageUrl = SwaggerExamplesConsts.PostViewResponseExample.ImageUrl, + AuthorId = Guid.NewGuid().ToString(), + CommentsCount = 10, + + Comments = new List + { + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "1", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "2", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + }, + + Tags = new List + { + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "1" + }, + + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "2" + }, + }, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostWithPagedCommentsResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostWithPagedCommentsResponseExample.cs new file mode 100644 index 00000000..668739ad --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/PostsResponses/PostWithPagedCommentsResponseExample.cs @@ -0,0 +1,80 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.PostsResponses; + +using System; +using System.Collections.Generic; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses; +using Blog.Contracts.V1.Responses.CommentsResponses; +using Blog.Contracts.V1.Responses.PostsResponses; +using Blog.Contracts.V1.Responses.TagsResponses; +using Blog.Core.Consts; + +/// +/// Post with paged comments response example. +/// +/// +public class PostWithPagedCommentsResponseExample : IExamplesProvider +{ + /// + public PostWithPagedCommentsResponse GetExamples() + { + return new PostWithPagedCommentsResponse + { + Post = new PostViewResponse + { + Id = 0, + Title = SwaggerExamplesConsts.PostViewResponseExample.Title, + Description = SwaggerExamplesConsts.PostViewResponseExample.Description, + Content = SwaggerExamplesConsts.PostViewResponseExample.Content, + ImageUrl = SwaggerExamplesConsts.PostViewResponseExample.ImageUrl, + AuthorId = Guid.NewGuid().ToString(), + CommentsCount = 10 + }, + Comments = new PagedCommentsResponse + { + Comments = new List + { + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "1", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + + new () + { + Id = 0, + PostId = 0, + CommentBody = SwaggerExamplesConsts.CommentResponseExample.CommentBody + "2", + CreatedAt = DateTime.Now, + UserId = Guid.NewGuid().ToString() + }, + }, + + PageInfo = new PageInfoResponse + { + PageNumber = 1, + PageSize = 10, + TotalItems = 100 + }, + }, + + Tags = new List + { + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "1" + }, + + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "2" + } + }, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/TagsResponses/PagedTagsResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/TagsResponses/PagedTagsResponseExample.cs new file mode 100644 index 00000000..832c1b68 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/TagsResponses/PagedTagsResponseExample.cs @@ -0,0 +1,49 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.TagsResponses; + +using System.Collections.Generic; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses; +using Blog.Contracts.V1.Responses.TagsResponses; +using Blog.Core.Consts; + +/// +/// Paged tags response example. +/// +/// +public class PagedTagsResponseExample : IExamplesProvider +{ + /// + public PagedTagsResponse GetExamples() + { + return new PagedTagsResponse + { + PageInfo = new PageInfoResponse + { + PageNumber = 1, + PageSize = 10, + TotalItems = 100, + }, + + Tags = new List + { + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "1" + }, + + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "2" + }, + + new () + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "3" + } + }, + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/TagsResponses/TagResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/TagsResponses/TagResponseExample.cs new file mode 100644 index 00000000..0bafcb42 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/TagsResponses/TagResponseExample.cs @@ -0,0 +1,22 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.TagsResponses; + +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses.TagsResponses; +using Blog.Core.Consts; + +/// +/// Tag response example. +/// +/// +public class TagResponseExample : IExamplesProvider +{ + /// + public TagResponse GetExamples() + { + return new TagResponse + { + Id = 0, + Title = SwaggerExamplesConsts.TagResponseExample.Title + "1" + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/AccountResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/AccountResponseExample.cs new file mode 100644 index 00000000..f267c6cd --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/AccountResponseExample.cs @@ -0,0 +1,36 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.UsersResponses; + +using System; +using System.Collections.Generic; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses.UsersResponses; +using Blog.Core.Consts; + +/// +/// Account response example. +/// +/// +public class AccountResponseExample : IExamplesProvider +{ + /// + public AccountResponse GetExamples() + { + return new AccountResponse + { + Id = Guid.NewGuid(), + FirstName = SwaggerExamplesConsts.AccountExample.FirstName, + LastName = SwaggerExamplesConsts.AccountExample.LastName, + UserName = SwaggerExamplesConsts.AccountExample.UserName, + Email = SwaggerExamplesConsts.AccountExample.Email, + PhoneNumber = SwaggerExamplesConsts.AccountExample.PhoneNumber, + Roles = new List + { + new () + { + Id = Guid.NewGuid().ToString(), + Name = Consts.Roles.User + }, + } + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/ApplicationUserResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/ApplicationUserResponseExample.cs new file mode 100644 index 00000000..bab5270a --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/ApplicationUserResponseExample.cs @@ -0,0 +1,34 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.UsersResponses; + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Identity; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses.UsersResponses; +using Blog.Core.Consts; + +/// +/// Application user response example. +/// +/// +public class ApplicationUserResponseExample : IExamplesProvider +{ + /// + public ApplicationUserResponse GetExamples() + { + return new ApplicationUserResponse + { + FirstName = SwaggerExamplesConsts.AccountExample.FirstName, + LastName = SwaggerExamplesConsts.AccountExample.LastName, + Email = SwaggerExamplesConsts.AccountExample.Email, + EmailConfirmed = true, + Roles = new List> + { + new () + { + RoleId = Guid.NewGuid().ToString() + } + } + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/ProfileResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/ProfileResponseExample.cs new file mode 100644 index 00000000..41e66518 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/ProfileResponseExample.cs @@ -0,0 +1,42 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.UsersResponses; + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Identity; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses.UsersResponses; +using Blog.Core.Consts; + +/// +/// Profile response example. +/// +/// +public class ProfileResponseExample : IExamplesProvider +{ + /// + public ProfileResponse GetExamples() + { + return new ProfileResponse + { + UserId = Guid.NewGuid().ToString(), + + User = new ApplicationUserResponse + { + FirstName = SwaggerExamplesConsts.AccountExample.FirstName, + LastName = SwaggerExamplesConsts.AccountExample.LastName, + Email = SwaggerExamplesConsts.AccountExample.Email, + EmailConfirmed = true, + Roles = new List> + { + new () + { + RoleId = Guid.NewGuid().ToString() + } + } + }, + + About = SwaggerExamplesConsts.ProfileResponseExample.About, + ProfileImg = SwaggerExamplesConsts.ProfileResponseExample.ProfileImg + }; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/RoleResponseExample.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/RoleResponseExample.cs new file mode 100644 index 00000000..fd3621d8 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/SwaggerExamples/Responses/UsersResponses/RoleResponseExample.cs @@ -0,0 +1,23 @@ +namespace BlogVerticalSliceCQRSMinimalApi.SwaggerExamples.Responses.UsersResponses; + +using System; +using Swashbuckle.AspNetCore.Filters; +using Blog.Contracts.V1.Responses.UsersResponses; +using Blog.Core.Consts; + +/// +/// Role response example. +/// +/// +public class RoleResponseExample : IExamplesProvider +{ + /// + public RoleResponse GetExamples() + { + return new RoleResponse + { + Id = Guid.NewGuid().ToString(), + Name = Consts.Roles.User + }; + } +} \ No newline at end of file From dd22890574c8c77cd5fd69448b4a8917fa1fd285 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 15 Mar 2026 01:11:07 +0200 Subject: [PATCH 11/59] Add view models --- .../EnableAuthenticatorViewModel.cs | 31 ++++++++ .../AspNetUser/ForgotPasswordViewModel.cs | 16 +++++ .../AspNetUser/LoginInfoViewModel.cs | 17 +++++ .../TwoFactorAuthenticationViewModel.cs | 22 ++++++ .../AspNetUser/UserCollectionViewModel.cs | 14 ++++ .../AspNetUser/UserItemViewModel.cs | 71 +++++++++++++++++++ .../AspNetUser/UserTableViewModel.cs | 19 +++++ .../Manage/AddPhoneNumberViewModel.cs | 17 +++++ .../Manage/ChangePasswordViewModel.cs | 26 +++++++ .../Manage/ConfigureTwoFactorViewModel.cs | 20 ++++++ .../VIewModels/Manage/FactorViewModel.cs | 12 ++++ .../VIewModels/Manage/IndexViewModel.cs | 35 +++++++++ .../Manage/ManageLoginsViewModel.cs | 21 ++++++ .../VIewModels/Manage/RemoveLoginViewModel.cs | 17 +++++ .../VIewModels/Manage/SetPasswordViewModel.cs | 26 +++++++ .../Manage/VerifyPhoneNumberViewModel.cs | 23 ++++++ .../VIewModels/Settings/SettingViewModel.cs | 25 +++++++ .../Settings/SettingsListViewModel.cs | 14 ++++ 18 files changed, 426 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/EnableAuthenticatorViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/ForgotPasswordViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/LoginInfoViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/TwoFactorAuthenticationViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserCollectionViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserItemViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserTableViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/AddPhoneNumberViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ChangePasswordViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ConfigureTwoFactorViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/FactorViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/IndexViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ManageLoginsViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/RemoveLoginViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/SetPasswordViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/VerifyPhoneNumberViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Settings/SettingViewModel.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Settings/SettingsListViewModel.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/EnableAuthenticatorViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/EnableAuthenticatorViewModel.cs new file mode 100644 index 00000000..a50c362c --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/EnableAuthenticatorViewModel.cs @@ -0,0 +1,31 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.AspNetUser; + +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +/// +/// Enable authenticator view model. +/// +public class EnableAuthenticatorViewModel +{ + /// + /// Gets or sets code. + /// + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Verification Code")] + public string Code { get; set; } + + /// + /// Gets or sets sharedKey. + /// + [BindNever] + public string SharedKey { get; set; } + + /// + /// Gets or sets authenticationUri. + /// + [BindNever] + public string AuthenticatorUri { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/ForgotPasswordViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/ForgotPasswordViewModel.cs new file mode 100644 index 00000000..d6c7d547 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/ForgotPasswordViewModel.cs @@ -0,0 +1,16 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.AspNetUser; + +using System.ComponentModel.DataAnnotations; + +/// +/// Forgot password view model +/// +public class ForgotPasswordViewModel +{ + /// + /// Gets or sets email. + /// + [Required] + [EmailAddress] + public string Email { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/LoginInfoViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/LoginInfoViewModel.cs new file mode 100644 index 00000000..74ec7f35 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/LoginInfoViewModel.cs @@ -0,0 +1,17 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.AspNetUser; + +/// +/// Login info view model. +/// +public class LoginInfoViewModel +{ + /// + /// Gets or sets jwt. + /// + public string Jwt { get; set; } + + /// + /// Gets or sets profileId. + /// + public int ProfileId { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/TwoFactorAuthenticationViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/TwoFactorAuthenticationViewModel.cs new file mode 100644 index 00000000..b72cbf43 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/TwoFactorAuthenticationViewModel.cs @@ -0,0 +1,22 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.AspNetUser; + +/// +/// Two-factor authentication view model +/// +public class TwoFactorAuthenticationViewModel +{ + /// + /// Gets or sets hasAuthenticator. + /// + public bool HasAuthenticator { get; set; } + + /// + /// Gets or sets recoveryCodesLeft. + /// + public int RecoveryCodesLeft { get; set; } + + /// + /// Gets or sets is2FaEnabled. + /// + public bool Is2FaEnabled { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserCollectionViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserCollectionViewModel.cs new file mode 100644 index 00000000..450e4210 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserCollectionViewModel.cs @@ -0,0 +1,14 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.AspNetUser; + +using System.Collections.Generic; + +/// +/// User collection view model +/// +public class UserCollectionViewModel +{ + /// + /// Gets or sets users. + /// + public IList Users { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserItemViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserItemViewModel.cs new file mode 100644 index 00000000..ebb57353 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserItemViewModel.cs @@ -0,0 +1,71 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.AspNetUser; + +using System.ComponentModel.DataAnnotations; +using Blog.Data.Models; + +/// +/// User item view model. +/// +public class UserItemViewModel +{ + /// + /// Initializes static members of the class. + /// + public UserItemViewModel() + { + } + + /// + /// Initializes static members of the class. + /// + /// user. + public UserItemViewModel(ApplicationUser user) + { + Id = user.Id; + UserName = user.UserName; + Email = user.Email; + PhoneNumber = user.PhoneNumber; + UserName = user.UserName; + } + + /// + /// Gets or sets id. + /// + public string Id { get; set; } + + /// + /// Gets or sets userName. + /// + [Required] + [StringLength(64, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", + MinimumLength = 2)] + public string UserName { get; set; } + + /// + /// Gets or sets email. + /// + [Required] + [EmailAddress] + public string Email { get; set; } + + /// + /// Gets or sets firstName. + /// + [Required] + [StringLength(64, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", + MinimumLength = 2)] + public string FirstName { get; set; } + + /// + /// Gets or sets lastName. + /// + [Required] + [StringLength(64, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", + MinimumLength = 2)] + public string LastName { get; set; } + + /// + /// Gets or sets phoneNumber. + /// + public string PhoneNumber { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserTableViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserTableViewModel.cs new file mode 100644 index 00000000..d461783d --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/AspNetUser/UserTableViewModel.cs @@ -0,0 +1,19 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.AspNetUser; + +using System.Collections.Generic; + +/// +/// User table view model. +/// +public class UserTableViewModel +{ + /// + /// Gets or sets users. + /// + public IList Users { get; set; } + + /// + /// Gets or sets recordsFiltered. + /// + public int RecordsFiltered { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/AddPhoneNumberViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/AddPhoneNumberViewModel.cs new file mode 100644 index 00000000..527c8de0 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/AddPhoneNumberViewModel.cs @@ -0,0 +1,17 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.Manage; + +using System.ComponentModel.DataAnnotations; + +/// +/// Add phone number view model +/// +public class AddPhoneNumberViewModel +{ + /// + /// Gets or sets phoneNumber. + /// + [Required] + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ChangePasswordViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ChangePasswordViewModel.cs new file mode 100644 index 00000000..140f27c7 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ChangePasswordViewModel.cs @@ -0,0 +1,26 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.Manage; + +using System.ComponentModel.DataAnnotations; + +/// +/// Change password view model. +/// +public class ChangePasswordViewModel +{ + /// + /// Gets or sets oldPassword. + /// + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + /// + /// Gets or sets newPassword. + /// + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ConfigureTwoFactorViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ConfigureTwoFactorViewModel.cs new file mode 100644 index 00000000..f704c5a8 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ConfigureTwoFactorViewModel.cs @@ -0,0 +1,20 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.Manage; + +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Rendering; + +/// +/// Configure two factor view model. +/// +public class ConfigureTwoFactorViewModel +{ + /// + /// Gets or sets selectedProvider. + /// + public string SelectedProvider { get; set; } + + /// + /// Gets or sets providers. + /// + public ICollection Providers { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/FactorViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/FactorViewModel.cs new file mode 100644 index 00000000..4299f952 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/FactorViewModel.cs @@ -0,0 +1,12 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.Manage; + +/// +/// Factor view model. +/// +public class FactorViewModel +{ + /// + /// Gets or sets purpose. + /// + public string Purpose { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/IndexViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/IndexViewModel.cs new file mode 100644 index 00000000..e1796862 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/IndexViewModel.cs @@ -0,0 +1,35 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.Manage; + +using System.Collections.Generic; +using Microsoft.AspNetCore.Identity; + +/// +/// Index view model. +/// +public class IndexViewModel +{ + /// + /// Gets or sets hasPassword. + /// + public bool HasPassword { get; set; } + + /// + /// Gets or sets logins. + /// + public IList Logins { get; set; } + + /// + /// Gets or sets pPhoneNumber. + /// + public string PhoneNumber { get; set; } + + /// + /// Gets or sets tTwoFactor. + /// + public bool TwoFactor { get; set; } + + /// + /// Gets or sets browserRemembered. + /// + public bool BrowserRemembered { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ManageLoginsViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ManageLoginsViewModel.cs new file mode 100644 index 00000000..d8e1d25c --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/ManageLoginsViewModel.cs @@ -0,0 +1,21 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.Manage; + +using System.Collections.Generic; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; + +/// +/// Manage logins view model. +/// +public class ManageLoginsViewModel +{ + /// + /// Gets or sets currentLogins. + /// + public IList CurrentLogins { get; set; } + + /// + /// Gets or sets otherLogins. + /// + public IList OtherLogins { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/RemoveLoginViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/RemoveLoginViewModel.cs new file mode 100644 index 00000000..f2eab444 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/RemoveLoginViewModel.cs @@ -0,0 +1,17 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.Manage; + +/// +/// Remove login view model. +/// +public class RemoveLoginViewModel +{ + /// + /// Gets or sets loginProvider. + /// + public string LoginProvider { get; set; } + + /// + /// Gets or sets providerKey. + /// + public string ProviderKey { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/SetPasswordViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/SetPasswordViewModel.cs new file mode 100644 index 00000000..d658f22e --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/SetPasswordViewModel.cs @@ -0,0 +1,26 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.Manage; + +using System.ComponentModel.DataAnnotations; + +/// +/// Set password view model. +/// +public class SetPasswordViewModel +{ + /// + /// Gets or sets newPassword. + /// + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + /// + /// Gets or sets confirmPassword. + /// + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/VerifyPhoneNumberViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/VerifyPhoneNumberViewModel.cs new file mode 100644 index 00000000..aba47ca3 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Manage/VerifyPhoneNumberViewModel.cs @@ -0,0 +1,23 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.Manage; + +using System.ComponentModel.DataAnnotations; + +/// +/// Verify phone number view model. +/// +public class VerifyPhoneNumberViewModel +{ + /// + /// Gets or sets code. + /// + [Required] + public string Code { get; set; } + + /// + /// Gets or sets phoneNumber. + /// + [Required] + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Settings/SettingViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Settings/SettingViewModel.cs new file mode 100644 index 00000000..84674e6c --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Settings/SettingViewModel.cs @@ -0,0 +1,25 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.Settings; + +using Blog.Core.Mapping.Interfaces; +using Blog.Data.Models; + +/// +/// Setting view model. +/// +public class SettingViewModel : IMapFrom +{ + /// + /// Gets or sets id.l + /// + public int Id { get; set; } + + /// + /// Gets or sets name. + /// + public string Name { get; set; } + + /// + /// Gets or sets value. + /// + public string Value { get; set; } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Settings/SettingsListViewModel.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Settings/SettingsListViewModel.cs new file mode 100644 index 00000000..54c26704 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/VIewModels/Settings/SettingsListViewModel.cs @@ -0,0 +1,14 @@ +namespace BlogVerticalSliceCQRSMinimalApi.ViewModels.Settings; + +using System.Collections.Generic; + +/// +/// Settings list view model. +/// +public class SettingsListViewModel +{ + /// + /// Gets or sets settings. + /// + public IEnumerable Settings { get; set; } +} \ No newline at end of file From a1d4761ec13639d433830e9a8b0839868cda38ec Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 15 Mar 2026 01:11:44 +0200 Subject: [PATCH 12/59] Add health check configuration --- .../HealthChecks/ReadinessPublisher.cs | 55 +++++++++++++++++++ .../HealthChecks/RedisHealthCheck.cs | 30 ++++++++++ .../HealthCheckInstaller.cs | 30 ++++++++++ 3 files changed, 115 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/HealthChecks/ReadinessPublisher.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/HealthChecks/RedisHealthCheck.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/HealthCheckInstaller.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/HealthChecks/ReadinessPublisher.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/HealthChecks/ReadinessPublisher.cs new file mode 100644 index 00000000..ced6826d --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/HealthChecks/ReadinessPublisher.cs @@ -0,0 +1,55 @@ +namespace BlogVerticalSliceCQRSMinimalApi.HealthChecks; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; + +/// +/// Readiness publisher. +/// +/// +public class ReadinessPublisher : IHealthCheckPublisher +{ + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + public ReadinessPublisher(ILogger logger) + { + _logger = logger; + } + + /// + /// Publishes the provided . + /// + /// The . The result of executing a set of health checks. + /// The . + /// + /// A which will complete when publishing is complete. + /// + public Task PublishAsync(HealthReport report, + CancellationToken cancellationToken) + { + if (report.Status == HealthStatus.Healthy) + { + _logger.LogInformation("{Timestamp} Readiness Probe Status: {Result}", + DateTime.UtcNow, report.Status); + } + else + { + _logger.LogError("{Timestamp} Readiness Probe Status: {Result}", + DateTime.UtcNow, report.Status); + } + + cancellationToken.ThrowIfCancellationRequested(); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/HealthChecks/RedisHealthCheck.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/HealthChecks/RedisHealthCheck.cs new file mode 100644 index 00000000..07bb55b4 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/HealthChecks/RedisHealthCheck.cs @@ -0,0 +1,30 @@ +namespace BlogVerticalSliceCQRSMinimalApi.HealthChecks +{ + /* + public class RedisHealthCheck : IHealthCheck + { + private readonly IConnectionMultiplexer _connectionMultiplexer; + + public RedisHealthCheck(IConnectionMultiplexer connectionMultiplexer) + { + _connectionMultiplexer = connectionMultiplexer; + } + + public Task CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = new CancellationToken()) + { + try + { + var database = _connectionMultiplexer.GetDatabase(); + database.StringGet("health"); + + return Task.FromResult(HealthCheckResult.Healthy()); + } + catch (Exception e) + { + return Task.FromResult(HealthCheckResult.Unhealthy(e.Message)); + } + } + } + */ +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/HealthCheckInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/HealthCheckInstaller.cs new file mode 100644 index 00000000..b4c9a6e6 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/HealthCheckInstaller.cs @@ -0,0 +1,30 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Blog.Data; +using HealthChecks; + +/// +/// Health check installer. +/// +/// +public class HealthCheckInstaller : IInstaller +{ + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + services.AddHealthChecks() + .AddDbContextCheck(); + // .AddCheck("Redis"); + + services.Configure(options => + { + options.Delay = TimeSpan.FromSeconds(2); + options.Predicate = (check) => check.Tags.Contains("ready"); + }); + + services.AddSingleton(); + } +} \ No newline at end of file From 0c0f94c03edbbd480f4e78824579d38a649b4e3d Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 16 Mar 2026 01:12:07 +0200 Subject: [PATCH 13/59] Add mappers --- .../VIewModelToEntityMappingRole.cs | 22 +++++++++++++ .../ViewModelToEntityMappingUser.cs | 33 +++++++++++++++++++ .../Mappers/DomainToResponseProfile.cs | 20 +++++++++++ .../Mappers/GeneralRequestsToDomains.cs | 24 ++++++++++++++ .../Posts/ViewModelToEntityMappingComment.cs | 24 ++++++++++++++ .../Posts/ViewModelToEntityMappingPosts.cs | 27 +++++++++++++++ .../Posts/ViewModelToEntityMappingTag.cs | 26 +++++++++++++++ .../Mappers/RequestToDomainProfile.cs | 19 +++++++++++ .../ViewModelToEntityMappingMessage.cs | 23 +++++++++++++ 9 files changed, 218 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/AspNetUser/VIewModelToEntityMappingRole.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/AspNetUser/ViewModelToEntityMappingUser.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/DomainToResponseProfile.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/GeneralRequestsToDomains.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingComment.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingPosts.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingTag.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/RequestToDomainProfile.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/ViewModelToEntityMappingMessage.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/AspNetUser/VIewModelToEntityMappingRole.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/AspNetUser/VIewModelToEntityMappingRole.cs new file mode 100644 index 00000000..d2e9f204 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/AspNetUser/VIewModelToEntityMappingRole.cs @@ -0,0 +1,22 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Mappers.AspNetUser; + +using Microsoft.AspNetCore.Identity; +using Profile = AutoMapper.Profile; +using Blog.Contracts.V1.Responses.UsersResponses; +using Blog.Data.Models; + +/// +/// VIew model to entity mapping role. +/// +/// +public class VIewModelToEntityMappingRole : Profile +{ + /// + /// Initializes a new instance of the class. + /// + public VIewModelToEntityMappingRole() + { + CreateMap, RoleResponse>(); + CreateMap(); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/AspNetUser/ViewModelToEntityMappingUser.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/AspNetUser/ViewModelToEntityMappingUser.cs new file mode 100644 index 00000000..99fce79d --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/AspNetUser/ViewModelToEntityMappingUser.cs @@ -0,0 +1,33 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Mappers.AspNetUser; + +using System.Linq; +using Blog.Contracts.V1.Requests.UsersRequests; +using Blog.Contracts.V1.Responses.UsersResponses; +using Blog.Data.Models; +using Blog.Services.Core.Dtos.User; +using BlogVerticalSliceCQRSMinimalApi.ViewModels.AspNetUser; + +/// +/// View model to entity mapping user. +/// +public class ViewModelToEntityMappingUser : AutoMapper.Profile +{ + /// + /// Initializes static members of the class. + /// + public ViewModelToEntityMappingUser() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap() + .ForMember(destinationMember => destinationMember.Roles, memberOptions + => memberOptions.Ignore()); + CreateMap(); + CreateMap(); + CreateMap() + .ForMember(destinationMember => destinationMember.Roles, memberOptions + => memberOptions.MapFrom(src => src.Roles.Select(x => new RoleResponse { Id = x.RoleId }))); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/DomainToResponseProfile.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/DomainToResponseProfile.cs new file mode 100644 index 00000000..6358cf0f --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/DomainToResponseProfile.cs @@ -0,0 +1,20 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Mappers; + +using Blog.Data.Models; +using Blog.Contracts.V1.Responses.UsersResponses; + +/// +/// Domain to response profile. +/// +/// +public class DomainToResponseProfile : AutoMapper.Profile +{ + /// + /// Initializes a new instance of the class. + /// + public DomainToResponseProfile() + { + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/GeneralRequestsToDomains.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/GeneralRequestsToDomains.cs new file mode 100644 index 00000000..80378af1 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/GeneralRequestsToDomains.cs @@ -0,0 +1,24 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Mappers; + +using AutoMapper; +using Blog.Services.Core.Dtos; +using Blog.Contracts.V1.Requests; +using Blog.Contracts.V1.Responses; +using Blog.Core.Helpers; + +/// +/// General requests to domains. +/// +/// +public class GeneralRequestsToDomains : Profile +{ + /// + /// Initializes a new instance of the class. + /// + public GeneralRequestsToDomains() + { + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingComment.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingComment.cs new file mode 100644 index 00000000..62008c3a --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingComment.cs @@ -0,0 +1,24 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Mappers.Posts; + +using Blog.Data.Models; +using Blog.Services.Core.Dtos.Posts; +using Blog.Contracts.V1.Requests.CommentsRequests; +using Blog.Contracts.V1.Responses.CommentsResponses; + +/// +/// View model to entity mapping comment. +/// +/// +public class ViewModelToEntityMappingComment : AutoMapper.Profile +{ + /// + /// Initializes a new instance of the class. + /// + public ViewModelToEntityMappingComment() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingPosts.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingPosts.cs new file mode 100644 index 00000000..523f45f1 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingPosts.cs @@ -0,0 +1,27 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Mappers.Posts; + +using Blog.Data.Models; +using Blog.Services.Core.Dtos.Posts; +using Blog.Contracts.V1.Requests.PostsRequests; +using Blog.Contracts.V1.Responses.PostsResponses; + +/// +/// View model to entity mapping posts. +/// +public class ViewModelToEntityMappingPosts : AutoMapper.Profile +{ + /// + /// Post maps. + /// + public ViewModelToEntityMappingPosts() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingTag.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingTag.cs new file mode 100644 index 00000000..313f768c --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/Posts/ViewModelToEntityMappingTag.cs @@ -0,0 +1,26 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Mappers.Posts; + +using Blog.Data.Models; +using Blog.Services.Core.Dtos.Posts; +using Blog.Contracts.V1.Requests.TagsRequests; +using Blog.Contracts.V1.Responses.TagsResponses; + +/// +/// View model to entity mapping tag. +/// +/// +public class ViewModelToEntityMappingTag : AutoMapper.Profile +{ + /// + /// Initializes a new instance of the class. + /// + public ViewModelToEntityMappingTag() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/RequestToDomainProfile.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/RequestToDomainProfile.cs new file mode 100644 index 00000000..ff138ad5 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/RequestToDomainProfile.cs @@ -0,0 +1,19 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Mappers; + +using Blog.Data.Models; +using Blog.Contracts.V1.Responses.UsersResponses; + +/// +/// Request to domain profile. +/// +/// +public class RequestToDomainProfile : AutoMapper.Profile +{ + /// + /// Initializes a new instance of the class. + /// + public RequestToDomainProfile() + { + CreateMap(); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/ViewModelToEntityMappingMessage.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/ViewModelToEntityMappingMessage.cs new file mode 100644 index 00000000..95b50760 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Mappers/ViewModelToEntityMappingMessage.cs @@ -0,0 +1,23 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Mappers; + +using Profile = AutoMapper.Profile; +using Blog.Data.Models; +using Blog.Contracts.V1.Requests.MessagesRequests; +using Blog.Contracts.V1.Responses; + +/// +/// View model to entity mapping message. +/// +/// +public class ViewModelToEntityMappingMessage : Profile +{ + /// + /// Initializes a new instance of the class. + /// + public ViewModelToEntityMappingMessage() + { + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file From a20a7962a57f5953b877e219af597245d6c023cc Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 16 Mar 2026 01:12:50 +0200 Subject: [PATCH 14/59] Add Api versioning installer --- .../ApiVersioningInstaller.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApiVersioningInstaller.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApiVersioningInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApiVersioningInstaller.cs new file mode 100644 index 00000000..ccea0e29 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApiVersioningInstaller.cs @@ -0,0 +1,43 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using Asp.Versioning; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +/// +/// Api versioning installer. +/// +/// +public class ApiVersioningInstaller : IInstaller +{ + /// + /// Installs the services. + /// + /// The services. + /// The configuration. + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + services.AddApiVersioning(options => + { + options.ReportApiVersions = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.AssumeDefaultVersionWhenUnspecified = true; + // Use whatever reader you want + options.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), + new HeaderApiVersionReader("x-api-version"), + new MediaTypeApiVersionReader("x-api-version")); + }) + .AddApiExplorer(options => + { + // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service + // note: the specified format code will format the version as "'v'major[.minor][-status]" + options.GroupNameFormat = "'v'VVV"; + + // note: this option is only necessary when versioning by url segment. the SubstitutionFormat + // can also be used to control the format of the API version in route templates + options.SubstituteApiVersionInUrl = true; + }); + + services.AddEndpointsApiExplorer(); + } +} \ No newline at end of file From d9e854f510a403326343d876cae900d91cc27030 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 16 Mar 2026 01:13:46 +0200 Subject: [PATCH 15/59] Add services Installer extentions --- .../IInstaller.cs | 17 ++++++++++ .../InstallerExtensions.cs | 33 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/IInstaller.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/InstallerExtensions.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/IInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/IInstaller.cs new file mode 100644 index 00000000..cfd7a8bc --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/IInstaller.cs @@ -0,0 +1,17 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +/// +/// Installer interface. +/// +public interface IInstaller +{ + /// + /// Installs the services. + /// + /// The services. + /// The configuration. + void InstallServices(IServiceCollection services, IConfiguration configuration); +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/InstallerExtensions.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/InstallerExtensions.cs new file mode 100644 index 00000000..e5dcdab3 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/InstallerExtensions.cs @@ -0,0 +1,33 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; + +/// +/// Installer extensions. +/// +public static class InstallerExtensions +{ + /// + /// Installs the services in assembly. + /// + /// The services. + /// The configuration. + public static void InstallServicesInAssembly(this IServiceCollection services, IConfiguration configuration) + { + var installers = typeof(Program).Assembly.ExportedTypes.Where(x => + typeof(IInstaller).IsAssignableFrom(x) + && x is { IsInterface: false, IsAbstract: false }) + .Select(Activator.CreateInstance) + .Cast().ToList(); + + installers.ForEach(installer => installer.InstallServices(services, configuration)); + + services.AddSingleton(configuration); + + services.Configure(options => { options.SuppressModelStateInvalidFilter = true; }); + } +} \ No newline at end of file From 003d94ada371951b501dfa8586b587cac70dbe90 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 16 Mar 2026 01:14:22 +0200 Subject: [PATCH 16/59] Add base services installers --- .../ApplicationBaseInstaller.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs new file mode 100644 index 00000000..b0595082 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs @@ -0,0 +1,27 @@ +using Carter; + +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using System.Text; + +/// +/// ApplicationBaseInstaller. +/// +/// +public class ApplicationBaseInstaller : IInstaller +{ + /// + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + // file upload dependency + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + services.AddAutoMapper(typeof(Program)); + services.AddOptions(); + services.AddLocalization(); + services.AddMemoryCache(); + services.AddResponseCaching(); + } +} \ No newline at end of file From 752728df852897d37a51b44fa72631e6a0e5bc49 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 17 Mar 2026 01:14:55 +0200 Subject: [PATCH 17/59] Add services installer --- .../ApplicationServicesInstaller.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs new file mode 100644 index 00000000..c5fa9458 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs @@ -0,0 +1,73 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using Blog.Services.Core.Caching; +using Blog.Services.Core.Caching.Interfaces; +using Blog.Services.Core.Email.Templates; +using Blog.Services.Core.Security; +using Blog.CommonServices.EmailServices; +using Blog.CommonServices.EmailServices.Interfaces; +using Blog.Core; +using Blog.Core.Configuration; +using Blog.Core.Infrastructure; +using Blog.Core.Interfaces; +using Blog.Data; +using Blog.Data.Models; +using Blog.Data.Repository; +using Blog.EntityServices.ControllerContext; +using Blog.EntityServices.EntityFrameworkServices; +using Blog.EntityServices.EntityFrameworkServices.Identity.Auth; +using Blog.EntityServices.EntityFrameworkServices.Identity.RefreshToken; +using Blog.EntityServices.EntityFrameworkServices.Identity.Registration; +using Blog.EntityServices.EntityFrameworkServices.Identity.User; +using Blog.EntityServices.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +/// +/// Application services installer. +/// +/// +public class ApplicationServicesInstaller : IInstaller +{ + /// + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + services.AddTransient(x => x.GetService>()?.Value); + services.AddTransient(); + + services.AddTransient(); + //services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddSingleton(); + services.AddHttpContextAccessor(); + + // Application services + services.AddTransient, ApplicationUserStore>(); + services.AddTransient, ApplicationRoleStore>(); + services.AddTransient, Repository>(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + //services.AddTransient(); + } +} \ No newline at end of file From 0dc68ecbe8f105b9457c076317f39d5a76446146 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 17 Mar 2026 01:15:35 +0200 Subject: [PATCH 18/59] Add authentication and authorization services installer --- ...AuthenticationAndAuthorizationInstaller.cs | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/AuthenticationAndAuthorizationInstaller.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/AuthenticationAndAuthorizationInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/AuthenticationAndAuthorizationInstaller.cs new file mode 100644 index 00000000..914cb1b4 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/AuthenticationAndAuthorizationInstaller.cs @@ -0,0 +1,119 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using System; +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using Blog.Services.Core.Identity.Auth; + +/// +/// Authentication and authorization installer. +/// +/// +public class AuthenticationAndAuthorizationInstaller : IInstaller +{ + /// + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + // TODO: Extract to external extension method .AddJWT() + + // Get options from app settings + var jwtAppSettingOptions = configuration.GetSection(nameof(JwtIssuerOptions)); + + // TODO: Get this from somewhere secure, possibly configuration !!! + // TODO: Implement some kind of key vault service to store Secret + const string secretKey = "iNivDmHLpUA223sqsfhqGbMRdRj1PVkH"; + var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)); + + // JWT wire up + services.AddTransient(); + + // Configure JwtIssuerOptions + services.Configure(options => + { + options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; + options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)]; + options.SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); + }); + + // Configure JWT auth + var tokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)], + + ValidateAudience = true, + ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)], + + ValidateIssuerSigningKey = true, + IssuerSigningKey = signingKey, + + RequireExpirationTime = false, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + + }).AddJwtBearer(configureOptions => + { + configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; + configureOptions.TokenValidationParameters = tokenValidationParameters; + configureOptions.SaveToken = true; + /* + configureOptions.Events = new JwtBearerEvents + { + + OnAuthenticationFailed = context => + { + if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) + { + context.Response.Headers.Add("Token-Expired", "true"); + } + return Task.CompletedTask; + }, + + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + + // If the request is for our hub... + var path = context.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && + (path.StartsWithSegments("/hubs/notification") || path.StartsWithSegments("/hubs/message"))) + { + // Read the token out of the query string + context.Token = accessToken; + } + return Task.CompletedTask; + } + };*/ + }); + + // api user claim policy + services.AddAuthorization(options => + { + options.AddPolicy("ApiUser", policy => policy.RequireClaim(JwtClaimTypes.Rol, JwtClaims.ApiAccess)); + }); + services.AddCors(options => + { + options.AddPolicy("AllowAll", builder => + { + builder.AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + }); + options.AddPolicy("AllowAllBlazor", builder => + { + builder.WithOrigins("https://localhost:44390").AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + }); + }); + } +} \ No newline at end of file From 11ff8b4ee15b699e430b4a0ffa3081b24c42b31b Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 17 Mar 2026 01:15:57 +0200 Subject: [PATCH 19/59] Add CORS installer --- .../CorsInstaller.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/CorsInstaller.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/CorsInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/CorsInstaller.cs new file mode 100644 index 00000000..d7e7db80 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/CorsInstaller.cs @@ -0,0 +1,45 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System.Linq; + +/// +/// Cors installer. +/// +/// +public class CorsInstaller : IInstaller +{ + /// + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + var origins = configuration.GetSection("Cors").GetSection("Origins").Value?.Split(','); + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", builder => + { + builder.AllowAnyHeader(); + builder.AllowAnyMethod(); + if (origins is not { Length: > 0 }) + { + return; + } + + if (origins.Contains("*")) + { + builder.AllowAnyHeader(); + builder.AllowAnyMethod(); + builder.SetIsOriginAllowed(_ => true); + builder.AllowCredentials(); + } + else + { + foreach (var origin in origins) + { + builder.WithOrigins(origin); + } + } + }); + }); + } +} \ No newline at end of file From c671c4d8280364a9b7b5d883f773b19a73eec865 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 17 Mar 2026 01:16:25 +0200 Subject: [PATCH 20/59] Add Data repositories installers --- .../DataRepositoriesInstaller.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs new file mode 100644 index 00000000..5e861b43 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs @@ -0,0 +1,30 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Blog.Data; +using Blog.Data.Models; +using Blog.Data.Repository; + +/// +/// Data repositories installer. +/// +/// +public class DataRepositoriesInstaller : IInstaller +{ + /// + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + // Data repositories + // services.AddScoped(typeof(IDeletableEntityRepository<>), typeof(EfDeletableEntityRepository<>)); + // services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); + services.AddTransient, Repository>(); + services.AddTransient, Repository>(); + services.AddTransient, Repository>(); + services.AddTransient, Repository>(); + services.AddTransient, Repository>(); + services.AddTransient, Repository>(); + services.AddTransient, Repository>(); + services.AddTransient, Repository>(); + } +} \ No newline at end of file From 846a3a25b235e3d51482b168188b40480e8bb9bf Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 18 Mar 2026 01:16:50 +0200 Subject: [PATCH 21/59] Add Database installer --- .../DbInstaller.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/DbInstaller.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/DbInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/DbInstaller.cs new file mode 100644 index 00000000..2c603013 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/DbInstaller.cs @@ -0,0 +1,22 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Blog.Data; +using Blog.Data.Extensions; + +/// +/// Database installer. +/// +/// +public class DbInstaller : IInstaller +{ + /// + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + services.AddDbContextPool( + options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); + services.AddUnitOfWork(); + } +} \ No newline at end of file From 6a671806a752b5d52b6f3957bfc3ad94d6199b09 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 18 Mar 2026 01:17:34 +0200 Subject: [PATCH 22/59] Add Email Installer --- .../EmailInstaller.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/EmailInstaller.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/EmailInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/EmailInstaller.cs new file mode 100644 index 00000000..5e83a097 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/EmailInstaller.cs @@ -0,0 +1,44 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Blog.CommonServices.EmailServices; +using Blog.CommonServices.EmailServices.Interfaces; +using Blog.Core.Emails; +using Blog.Services.Core.Email.Smtp; +using Blog.Services.Core.Utilities; + +/// +/// Email installer. +/// +/// +public class EmailInstaller : IInstaller +{ + /// + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + // TODO: Extract to external extension method .AddEmail() + services.AddSingleton(); + + // Configure SmtpOptions + var smtpOptions = configuration.GetSection(nameof(SmtpOptions)); + + // Configure SmtpOptions + services.Configure(options => + { + options.Host = smtpOptions[nameof(SmtpOptions.Host)]; + options.Port = smtpOptions[nameof(SmtpOptions.Port)].ToInt(); + options.UserName = smtpOptions[nameof(SmtpOptions.UserName)]; + options.Password = smtpOptions[nameof(SmtpOptions.Password)]; + options.EnableSsl = smtpOptions[nameof(SmtpOptions.EnableSsl)].ToBool(); + }); + + // Configure SystemEmailOptions + var systemEmailOptions = configuration.GetSection(nameof(EmailExtensionOptions)); + services.Configure(options => + { + options.BaseUrl = systemEmailOptions[nameof(EmailExtensionOptions.BaseUrl)]; + options.From = systemEmailOptions[nameof(EmailExtensionOptions.From)]; + }); + } +} \ No newline at end of file From 9e4c5f63cdd6e43ceb5d87939d4cb40f74c47dd5 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 18 Mar 2026 10:33:04 +0200 Subject: [PATCH 23/59] Configure autentication and authorization --- .../IdentityInstaller.cs | 54 +++++++++++++++++++ .../ConfigureAuthentication.cs | 19 +++++++ 2 files changed, 73 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/IdentityInstaller.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureAuthentication.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/IdentityInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/IdentityInstaller.cs new file mode 100644 index 00000000..cc711a76 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/IdentityInstaller.cs @@ -0,0 +1,54 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Blog.Data; +using Blog.Data.Models; + +/// +/// Identity installer. +/// +/// +public class IdentityInstaller : IInstaller +{ + /// + public void InstallServices(IServiceCollection services, IConfiguration configuration) + { + // Add Identity + // TODO: Extract to external extension method .AddIdentity() + services.AddScoped>(); + + var builder = services.AddIdentityCore(o => + { + // Configure Identity options + o.Password.RequireDigit = false; + o.Password.RequireLowercase = false; + o.Password.RequireUppercase = false; + o.Password.RequireNonAlphanumeric = false; + o.Password.RequiredLength = 6; + }); + + /* + services.AddIdentity( + options => + { + options.Password.RequireDigit = false; + options.Password.RequireLowercase = false; + options.Password.RequireUppercase = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredLength = 6; + }) + .AddEntityFrameworkStores() + .AddUserStore() + .AddRoleStore() + .AddDefaultTokenProviders(); + */ + + + builder = new IdentityBuilder(builder.UserType, typeof(ApplicationRole), builder.Services); + + builder.AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureAuthentication.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureAuthentication.cs new file mode 100644 index 00000000..d79d4658 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureAuthentication.cs @@ -0,0 +1,19 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigures; + +using Microsoft.AspNetCore.Builder; + +/// +/// Configure authentication. +/// +public static class ConfigureAuthentication +{ + /// + /// Configures the specified application. + /// + /// The application. + public static void Configure(IApplicationBuilder app) + { + app.UseCors("EnableCORS"); + app.UseCors("AllowAllBlazor"); + } +} \ No newline at end of file From 4f6525561275dd0c0c65991bf9e4749141904f7e Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 18 Mar 2026 10:34:24 +0200 Subject: [PATCH 24/59] Configure validations --- .../BlogVerticalSliceCQRSMinimalApi.csproj | 1 + .../MediatorInstaller.cs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/MediatorInstaller.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj index e307234f..625c2245 100644 --- a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj @@ -12,6 +12,7 @@ + diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/MediatorInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/MediatorInstaller.cs new file mode 100644 index 00000000..b2745bcb --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/MediatorInstaller.cs @@ -0,0 +1,17 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +using MediatR; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Blog.Core.Infrastructure.Mediator.Behaviors; + +/// +/// Mediator installer. +/// +/// +public class MediatorInstaller : IInstaller +{ + /// + public void InstallServices(IServiceCollection services, IConfiguration configuration) + => services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorBehavior<,>)); +} \ No newline at end of file From f969c422788a8d1cba1f9cfb476d915a64988602 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 19 Mar 2026 10:35:26 +0200 Subject: [PATCH 25/59] Add base configurations --- .../StartupConfigures/ConfigureBase.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureBase.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureBase.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureBase.cs new file mode 100644 index 00000000..e2d18b72 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureBase.cs @@ -0,0 +1,54 @@ +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigures; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Builder; +using Blog.Core.Infrastructure.Middlewares; + +/// +/// Configure base. +/// +public class ConfigureBase +{ + /// + /// Configures the specified application. + /// + /// The application. + /// The env. + public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // TODO: Add development configuration + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Error"); + app.UseHsts(); + } + + app.UseStatusCodePagesWithReExecute("/api/errors/{0}"); + + // Configures application to serve the index.html file from /wwwroot + // when you access the server from a browser + app.UseDefaultFiles(); + app.UseStaticFiles(new StaticFileOptions + { + OnPrepareResponse = ctx => + { + ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=600"); + } + }); + + app.UseResponseCaching(); + + app.UseETagger(); + + app.UseMiddleware(typeof(ErrorHandlingMiddleware)); + + app.UseHealthChecks("/api/health-check"); + app.UseHttpsRedirection(); + } +} \ No newline at end of file From 87c4142aa0a2e05c6bf70512272254d4f7a16b79 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 19 Mar 2026 10:36:10 +0200 Subject: [PATCH 26/59] Configure routes --- .../StartupConfigures/ConfigureRoutes.cs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureRoutes.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureRoutes.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureRoutes.cs new file mode 100644 index 00000000..3142367b --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureRoutes.cs @@ -0,0 +1,87 @@ +using Carter; + +namespace BlogVerticalSliceCQRSMinimalApi.StartupConfigures; + +using System.IO; +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; + +/// +/// Configure routes. +/// +public class ConfigureRoutes +{ + /// + /// Configures the specified application. + /// + /// The application. + public static void Configure(WebApplication app) + { + // TODO: Implement more advanced Error Handling + // TODO: Extract implementations to external files + app.UseExceptionHandler(builder => + { + builder.Run(async context => + { + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); + + var error = context.Features.Get(); + if (error != null) + { + //context.Response.AddApplicationError(error.Error.Message); + await context.Response.WriteAsync(error.Error.Message).ConfigureAwait(false); + } + }); + }); + + // Redirect any non-API calls to the Angular application + // so our application can handle the routing + // TODO: Extract to extension method .AddAngular() + app.Use(async (context, next) => + { + await next(); + + if (context.Response.StatusCode != 404 || Path.HasExtension(context.Request.Path.Value) || + context.Request.Path.Value.StartsWith("/api/")) + { + } + else + { + context.Request.Path = "/index.html"; + await next(); + } + }); + + // Configure application for usage as API + // with default route of '/api/[Controller]' + + app.UseCors("AllowAll"); + app.UseRouting(); + + app.UseAuthorization(); + app.UseAuthentication(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + endpoints.MapControllerRoute( + name: "areaRoute", + pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); + + //endpoints.MapHealthChecks("/health"); + }); + + app.MapDefaultEndpoints(); + + //TODO: move to StartupConfigures + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + } + } +} \ No newline at end of file From e822033182e827221774014798879baafef2b3b8 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 19 Mar 2026 10:37:27 +0200 Subject: [PATCH 27/59] Configure base command and command handler interfaces --- .../CQRSExtensions/ICommand.cs | 16 ++++++++++++++++ .../CQRSExtensions/ICommandHandler.cs | 14 ++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/ICommand.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/ICommandHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/ICommand.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/ICommand.cs new file mode 100644 index 00000000..fe2c9317 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/ICommand.cs @@ -0,0 +1,16 @@ +using MediatR; + +namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +/// +/// +/// +//will not return any response +public interface ICommand : ICommand; + +/// +/// +/// +/// +//will return TResponse +public interface ICommand : IRequest; diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/ICommandHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/ICommandHandler.cs new file mode 100644 index 00000000..37581075 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/ICommandHandler.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +//will not return any response +public interface ICommandHandler + : IRequestHandler + where TCommand : ICommand; + +//will return TResponse +public interface ICommandHandler + : IRequestHandler + where TCommand : ICommand + where TResponse : notnull; \ No newline at end of file From 9e15553d1664cb3a23e80f9f2d468487d0f87753 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 19 Mar 2026 10:38:18 +0200 Subject: [PATCH 28/59] Configure base query and query handler interfaces --- .../CQRSExtensions/IQuery.cs | 6 ++++++ .../CQRSExtensions/IQueryHandler.cs | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/IQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/IQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/IQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/IQuery.cs new file mode 100644 index 00000000..9108ed4b --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/IQuery.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +public interface IQuery : IRequest + where TResponse : notnull; diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/IQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/IQueryHandler.cs new file mode 100644 index 00000000..8257e714 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/IQueryHandler.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +//will return TResponse +public interface IQueryHandler + : IRequestHandler + where TQuery : IQuery + where TResponse : notnull; \ No newline at end of file From 488173b301d043de032530c1da144ac4f05a4403 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 20 Mar 2026 10:40:21 +0200 Subject: [PATCH 29/59] Add get all users query and query handler in accounts feature --- .../Accounts/GetAllUsers/GetAllUsersQuery.cs | 6 +++++ .../GetAllUsers/GetAllUsersQueryHandler.cs | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/GetAllUsers/GetAllUsersQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/GetAllUsers/GetAllUsersQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/GetAllUsers/GetAllUsersQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/GetAllUsers/GetAllUsersQuery.cs new file mode 100644 index 00000000..b1b14361 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/GetAllUsers/GetAllUsersQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses.UsersResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.GetAllUsers; + +public record GetAllUsersQuery : IQuery>; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/GetAllUsers/GetAllUsersQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/GetAllUsers/GetAllUsersQueryHandler.cs new file mode 100644 index 00000000..891e61f5 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/GetAllUsers/GetAllUsersQueryHandler.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses.UsersResponses; +using Blog.Data.Models; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.GetAllUsers; + +public class GetAllUsersQueryHandler( + UserManager userManager, + IMapper mapper) + : IQueryHandler> +{ + public async Task> Handle(GetAllUsersQuery query, CancellationToken cancellationToken) + { + var users = await userManager.Users + .Include(u => u.Roles) + .ToListAsync(cancellationToken: cancellationToken); + + return mapper.Map>(users); + } +} \ No newline at end of file From 7e01c7abfb8eb629ac225274f370fc5c921dde15 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 20 Mar 2026 10:41:00 +0200 Subject: [PATCH 30/59] Add initialize query and query handler in accounts feature --- .../Accounts/Initialize/InitializeQuery.cs | 6 ++++++ .../Initialize/InitializeQueryHandler.cs | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Initialize/InitializeQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Initialize/InitializeQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Initialize/InitializeQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Initialize/InitializeQuery.cs new file mode 100644 index 00000000..69b231c6 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Initialize/InitializeQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses.UsersResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Initialize; + +public record InitializeQuery(string UserId) : IQuery>; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Initialize/InitializeQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Initialize/InitializeQueryHandler.cs new file mode 100644 index 00000000..b76d3d39 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Initialize/InitializeQueryHandler.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses.UsersResponses; +using Blog.Data.Models; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Initialize; + +public class InitializeQueryHandler( + RoleManager roleManager, + IMapper mapper) + : IQueryHandler> +{ + public async Task> Handle(InitializeQuery query, CancellationToken cancellationToken) + { + var roles = await roleManager.Roles.ToListAsync(cancellationToken: cancellationToken); + + return mapper.Map>(roles); + } +} \ No newline at end of file From ba198a29ec6dc2a3da5b19c820ba8310610ff84b Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 20 Mar 2026 10:42:29 +0200 Subject: [PATCH 31/59] Add send confirmation email query and query handler in accounts feature --- .../SendConfirmationEmailQuery.cs | 6 ++++++ .../SendConfirmationEmailQueryHandler.cs | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/SendConfirmationEmail/SendConfirmationEmailQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/SendConfirmationEmail/SendConfirmationEmailQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/SendConfirmationEmail/SendConfirmationEmailQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/SendConfirmationEmail/SendConfirmationEmailQuery.cs new file mode 100644 index 00000000..7850f94b --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/SendConfirmationEmail/SendConfirmationEmailQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.SendConfirmationEmail; + +public record SendConfirmationEmailQuery(string Email) : IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/SendConfirmationEmail/SendConfirmationEmailQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/SendConfirmationEmail/SendConfirmationEmailQueryHandler.cs new file mode 100644 index 00000000..1e192be2 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/SendConfirmationEmail/SendConfirmationEmailQueryHandler.cs @@ -0,0 +1,20 @@ +using Blog.CommonServices.EmailServices.Interfaces; +using Blog.Contracts.V1.Responses; +using Blog.EntityServices.EntityFrameworkServices.Identity.User; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.SendConfirmationEmail; + +public class SendConfirmationEmailQueryHandler( + IUserService userService, + IEmailExtensionService emailService) + : IQueryHandler +{ + public async Task Handle(SendConfirmationEmailQuery query, CancellationToken cancellationToken) + { + var token = await userService.GetEmailVerificationTokenAsync(query.Email); + await emailService.SendVerificationEmailAsync(query.Email, token); + + return new BaseResponse(); + } +} \ No newline at end of file From 57f51e51b5601bce5f5af5b7289d0fbd7b6df09c Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 20 Mar 2026 10:43:13 +0200 Subject: [PATCH 32/59] Add users activity query and query handler in accounts feature --- .../Accounts/UsersActivity/UsersActivityQuery.cs | 6 ++++++ .../UsersActivity/UsersActivityQueryHandler.cs | 12 ++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/UsersActivity/UsersActivityQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/UsersActivity/UsersActivityQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/UsersActivity/UsersActivityQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/UsersActivity/UsersActivityQuery.cs new file mode 100644 index 00000000..19a1d090 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/UsersActivity/UsersActivityQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses.Chart; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.UsersActivity; + +public class UsersActivityQuery : IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/UsersActivity/UsersActivityQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/UsersActivity/UsersActivityQueryHandler.cs new file mode 100644 index 00000000..7bc48dec --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/UsersActivity/UsersActivityQueryHandler.cs @@ -0,0 +1,12 @@ +using Blog.Contracts.V1.Responses.Chart; +using Blog.EntityServices.EntityFrameworkServices.Identity.User; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.UsersActivity; + +public class UsersActivityQueryHandler(IUserService userService) + : IQueryHandler +{ + public async Task Handle(UsersActivityQuery request, CancellationToken cancellationToken) + => await userService.GetUsersActivity(); +} \ No newline at end of file From c2ba491e0cdea2cc6264dbd604132c0a5e652dfb Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 21 Mar 2026 10:44:37 +0200 Subject: [PATCH 33/59] Add login command and command handler in accounts feature --- .../Features/Accounts/Login/LoginCommand.cs | 6 ++++++ .../Features/Accounts/Login/LoginCommandHandler.cs | 11 +++++++++++ 2 files changed, 17 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginCommand.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginCommandHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginCommand.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginCommand.cs new file mode 100644 index 00000000..6ce240a5 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginCommand.cs @@ -0,0 +1,6 @@ +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Login; + +public record LoginCommand(string Email, string Password) + : ICommand; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginCommandHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginCommandHandler.cs new file mode 100644 index 00000000..2e213604 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginCommandHandler.cs @@ -0,0 +1,11 @@ +using Blog.EntityServices.EntityFrameworkServices.Identity.Auth; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Login; + +public class LoginCommandHandler(IAuthService authService) + : ICommandHandler +{ + public async Task Handle(LoginCommand command, CancellationToken cancellationToken) + => await authService.GetJwtAsync(command.Email, command.Password); +} \ No newline at end of file From ea2a12cad3908acd0b38752fd6dea074df521027 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 21 Mar 2026 10:45:01 +0200 Subject: [PATCH 34/59] Add login endpoint in accounts feature --- .../Features/Accounts/Login/LoginEndpoint.cs | 45 +++++++++++++++++++ .../Features/Accounts/Login/LoginRequest.cs | 3 ++ 2 files changed, 48 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginEndpoint.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginRequest.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginEndpoint.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginEndpoint.cs new file mode 100644 index 00000000..8ed6406d --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginEndpoint.cs @@ -0,0 +1,45 @@ +using Asp.Versioning; +using Blog.Contracts.V1; +using Carter; +using Mapster; +using MediatR; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Login; + +public class LoginEndpoint : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var versionSet = app.NewApiVersionSet() + .HasApiVersion(new ApiVersion(1, 0)) + .ReportApiVersions() + .Build(); + + var group = app.MapGroup(ApiRoutes.AccountsController.Accounts) + .WithApiVersionSet(versionSet) + .MapToApiVersion(1.0) + .WithTags("Accounts"); + + group.MapPost(ApiRoutes.AccountsController.Login, + async (LoginRequest request, ISender sender) => + { + if (request is null) + return Results.BadRequest(); + + var command = request.Adapt(); + + var result = await sender.Send(command); + + return result is null + ? Results.BadRequest("Invalid name or password") + : Results.Ok(result); + }) + //.RequireAuthorization() + .WithName("Login") + .Produces(200) + .Produces(400) + .WithSummary("Login") + .WithDescription("Login") + .HasApiVersion(1.0); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginRequest.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginRequest.cs new file mode 100644 index 00000000..ee33d20d --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Login/LoginRequest.cs @@ -0,0 +1,3 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Login; + +public record LoginRequest(string Email, string Password); \ No newline at end of file From 490a7e287b606c2027ea1da4bb7797c7168a8c16 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 21 Mar 2026 10:45:35 +0200 Subject: [PATCH 35/59] Add register command and command handler in accounts feature --- .../Accounts/Register/RegisterCommand.cs | 8 +++ .../Register/RegisterCommandHandler.cs | 49 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterCommand.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterCommandHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterCommand.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterCommand.cs new file mode 100644 index 00000000..cf84ed97 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterCommand.cs @@ -0,0 +1,8 @@ +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; +using Microsoft.AspNetCore.Identity; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Register; + +public record RegisterCommand(string Email, string Password, string ConfirmPassword, string FirstName, + string LastName, string UserName, string PhoneNumber,string ConcurrencyStamp, IEnumerable Roles) + : ICommand; diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterCommandHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterCommandHandler.cs new file mode 100644 index 00000000..940eea91 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterCommandHandler.cs @@ -0,0 +1,49 @@ +using AutoMapper; +using Blog.Contracts.V1.Requests.UsersRequests; +using Blog.Data.Models; +using Blog.EntityServices.EntityFrameworkServices.Identity.Registration; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; +using Microsoft.AspNetCore.Identity; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Register; + +public class RegisterCommandHandler(IMapper mapper, + IRegistrationService registrationService, + UserManager userManager) : ICommandHandler +{ + public async Task Handle(RegisterCommand command, CancellationToken cancellationToken) + { + var model = new RegistrationRequest + { + Email = command.Email, + Password = command.Password, + ConfirmPassword = command.ConfirmPassword, + FirstName = command.FirstName, + LastName = command.LastName, + UserName = command.Email, + PhoneNumber = command.PhoneNumber, + ConcurrencyStamp = command.ConcurrencyStamp, + Roles = command.Roles + }; + + var user = mapper.Map(model); + user.CreatedOn = DateTime.UtcNow; + user.SecurityStamp = Guid.NewGuid().ToString(); + + var result = await registrationService.RegisterAsync(user, model.Password); + + //if (!result.Succeeded) + //return Results.BadRequest(result); + + model.Roles ??= ["User"]; + + foreach (var role in model.Roles) + { + var roleResult = await userManager.AddToRoleAsync(user, role); + //if (!roleResult.Succeeded) + //return Results.BadRequest(roleResult); + } + + return result; + } +} From ef530daeff0f7637c032fbd48654f7c15065ae30 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 21 Mar 2026 10:46:14 +0200 Subject: [PATCH 36/59] Add register endpoint in accounts feature --- .../Accounts/Register/RegisterEndpoint.cs | 53 +++++++++++++++++++ .../Accounts/Register/RegisterRequest.cs | 4 ++ 2 files changed, 57 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterEndpoint.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterRequest.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterEndpoint.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterEndpoint.cs new file mode 100644 index 00000000..8c16a3bd --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterEndpoint.cs @@ -0,0 +1,53 @@ +using Asp.Versioning; +using Blog.Contracts.V1; +using Carter; +using Mapster; +using MediatR; +using Microsoft.AspNetCore.Identity; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Register; + +public class RegisterEndpoint : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var versionSet = app.NewApiVersionSet() + .HasApiVersion(new ApiVersion(1, 0)) + .ReportApiVersions() + .Build(); + + var group = app.MapGroup(ApiRoutes.AccountsController.Accounts) + .WithApiVersionSet(versionSet) + .MapToApiVersion(1.0) + .WithTags("Accounts"); + + group.MapPost(ApiRoutes.AccountsController.Register, + async (RegisterRequest request, ISender sender) => + { + var command = request.Adapt(); + + var result = await sender.Send(command); + + if (!result.Succeeded) + return Results.BadRequest(result); + + /*request.Roles ??= ["User"]; + + foreach (var role in model.Roles) + { + var roleResult = await userManager.AddToRoleAsync(user, role); + if (!roleResult.Succeeded) + return Results.BadRequest(roleResult); + }*/ + + return Results.NoContent(); + }) + //.RequireAuthorization() + .WithName("Register") + .Produces(204) + .Produces(400) + .WithSummary("Register") + .WithDescription("Register") + .HasApiVersion(1.0); + } +} \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterRequest.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterRequest.cs new file mode 100644 index 00000000..3fa85036 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Accounts/Register/RegisterRequest.cs @@ -0,0 +1,4 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Register; + +public class RegisterRequest(string Email, string Password, string ConfirmPassword, string FirstName, + string LastName, string UserName, string PhoneNumber, string ConcurrencyStamp, IEnumerable Roles); \ No newline at end of file From 456c72c1f2fde50e4dbd7db8e6c5cc48d5fd59dc Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 22 Mar 2026 10:47:22 +0200 Subject: [PATCH 37/59] Add comments activity query and query handler in accounts feature --- .../CommentsActivity/CommentsActivityQuery.cs | 6 ++++++ .../CommentsActivity/CommentsActivityQueryHandler.cs | 12 ++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CommentsActivity/CommentsActivityQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CommentsActivity/CommentsActivityQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CommentsActivity/CommentsActivityQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CommentsActivity/CommentsActivityQuery.cs new file mode 100644 index 00000000..99063deb --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CommentsActivity/CommentsActivityQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses.Chart; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.CommentsActivity; + +public record CommentsActivityQuery : IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CommentsActivity/CommentsActivityQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CommentsActivity/CommentsActivityQueryHandler.cs new file mode 100644 index 00000000..192f97a3 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CommentsActivity/CommentsActivityQueryHandler.cs @@ -0,0 +1,12 @@ +using Blog.Contracts.V1.Responses.Chart; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.CommentsActivity; + +public class CommentsActivityQueryHandler(ICommentsService service) + : IQueryHandler +{ + public async Task Handle(CommentsActivityQuery query, CancellationToken cancellationToken) + => await service.GetCommentsActivity(); +} \ No newline at end of file From c6237a882b0a132b14a9f28de5e3490beeadba1b Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 22 Mar 2026 10:48:31 +0200 Subject: [PATCH 38/59] Add create comment command and command handler in comments feature --- .../CreateComment/CreateCommentCommand.cs | 7 +++++ .../CreateCommentCommandHandler.cs | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentCommand.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentCommandHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentCommand.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentCommand.cs new file mode 100644 index 00000000..785e5df8 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentCommand.cs @@ -0,0 +1,7 @@ +using Blog.Contracts.V1.Responses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.CreateComment; + +public record CreateCommentCommand(int PostId, string CommentBody, string UserId, string Name, string Email) + : ICommand>; diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentCommandHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentCommandHandler.cs new file mode 100644 index 00000000..5413af90 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentCommandHandler.cs @@ -0,0 +1,30 @@ +using Blog.Contracts.V1.Responses; +using Blog.Data.Models; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.CreateComment; + +public class CreateCommentCommandHandler(ICommentsService service) + : ICommandHandler> +{ + public async Task> Handle(CreateCommentCommand command, CancellationToken cancellationToken) + { + var comment = new Comment + { + PostId = command.PostId, + CommentBody = command.CommentBody, + UserId = command.UserId, + Name = command.Name, + Email = command.Email, + CreatedAt = DateTime.UtcNow + }; + + await service.InsertAsync(comment); + + return new CreatedResponse + { + Id = comment.Id + }; + } +} From c08837c21dfe7119589905d89d4e7734baae2bf8 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 22 Mar 2026 10:49:23 +0200 Subject: [PATCH 39/59] Add create comment endpoint in comments feature --- .../CreateComment/CreateCommentEndpoint.cs | 50 +++++++++++++++++++ .../CreateComment/CreateCommentRequest.cs | 3 ++ 2 files changed, 53 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentEndpoint.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentRequest.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentEndpoint.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentEndpoint.cs new file mode 100644 index 00000000..d064d9f6 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentEndpoint.cs @@ -0,0 +1,50 @@ +using Asp.Versioning; +using Blog.Contracts.V1; +using Blog.Contracts.V1.Responses; +using Carter; +using Mapster; +using MediatR; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.CreateComment; + +public class CreateCommentEndpoint : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var versionSet = app.NewApiVersionSet() + .HasApiVersion(new ApiVersion(1, 0)) + .ReportApiVersions() + .Build(); + + var group = app.MapGroup(ApiRoutes.CommentsController.Comments) + .WithApiVersionSet(versionSet) + .MapToApiVersion(1.0) + .WithTags("Comments"); + + group.MapPost(ApiRoutes.CommentsController.CreateComment, + async (CreateCommentRequest request, + ISender sender, + HttpContext context) => + { + var command = request.Adapt(); + + var result = await sender.Send(command); + + var location = + $"{context.Request.Scheme}://{context.Request.Host}" + + $"{ApiRoutes.CommentsController.Comments}/{result.Id}"; + + return Results.Created(location, new CreatedResponse + { + Id = result.Id + }); + }) + //.RequireAuthorization() + .WithName("CreateComment") + .Produces>(201) + .Produces(400) + .WithSummary("Create comment") + .WithDescription("Create comment") + .HasApiVersion(1.0); + } +} diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentRequest.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentRequest.cs new file mode 100644 index 00000000..54b64f9d --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/CreateComment/CreateCommentRequest.cs @@ -0,0 +1,3 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.CreateComment; + +public record CreateCommentRequest(int PostId, string CommentBody, string UserId, string Name, string Email); From 18494f0512549bf920762c9ac0742e51c8070ee0 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 22 Mar 2026 10:50:08 +0200 Subject: [PATCH 40/59] Add get all query and query handler in comments feature --- .../Features/Comments/GetAll/GetAllQuery.cs | 6 ++++++ .../Comments/GetAll/GetAllQueryHandler.cs | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetAll/GetAllQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetAll/GetAllQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetAll/GetAllQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetAll/GetAllQuery.cs new file mode 100644 index 00000000..e5bb9b64 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetAll/GetAllQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses.CommentsResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.GetAll; + +public record GetAllQuery : IQuery>; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetAll/GetAllQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetAll/GetAllQueryHandler.cs new file mode 100644 index 00000000..1779e86f --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetAll/GetAllQueryHandler.cs @@ -0,0 +1,19 @@ +using Blog.Contracts.V1.Responses.CommentsResponses; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; +using MapsterMapper; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.GetAll; + +public class GetAllQueryHandler( + ICommentsService service, + IMapper mapper) + : IQueryHandler> +{ + public async Task> Handle(GetAllQuery query, CancellationToken cancellationToken) + { + var comments = await service.GetAllAsync(); + + return mapper.Map>(comments); + } +} \ No newline at end of file From 579a8416483ee28928ecf60267af39c06e89bd18 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 23 Mar 2026 10:50:51 +0200 Subject: [PATCH 41/59] Add get comment by id query and query handler in comments feature --- .../GetCommentById/GetCommentByIdQuery.cs | 6 ++++++ .../GetCommentByIdQueryHandler.cs | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentById/GetCommentByIdQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentById/GetCommentByIdQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentById/GetCommentByIdQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentById/GetCommentByIdQuery.cs new file mode 100644 index 00000000..be48341c --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentById/GetCommentByIdQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses.CommentsResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.GetCommentById; + +public record GetCommentByIdQuery(int Id) : IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentById/GetCommentByIdQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentById/GetCommentByIdQueryHandler.cs new file mode 100644 index 00000000..40952dbc --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentById/GetCommentByIdQueryHandler.cs @@ -0,0 +1,19 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses.CommentsResponses; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.GetCommentById; + +public class GetCommentByIdQueryHandler( + ICommentsService service, + IMapper mapper) + : IQueryHandler +{ + public async Task Handle(GetCommentByIdQuery query, CancellationToken cancellationToken) + { + var comment = await service.GetCommentAsync(query.Id); + + return mapper.Map(comment); + } +} \ No newline at end of file From ca5595d3c3d14b9fc9a16ca8599b9de8291aff94 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 23 Mar 2026 10:52:19 +0200 Subject: [PATCH 42/59] Add get comments by filter query and query handler in comments feature --- .../GetCommentsByFilterQuery.cs | 7 ++++++ .../GetCommentsByFilterQueryHandler.cs | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByFilter/GetCommentsByFilterQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByFilter/GetCommentsByFilterQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByFilter/GetCommentsByFilterQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByFilter/GetCommentsByFilterQuery.cs new file mode 100644 index 00000000..9728be0c --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByFilter/GetCommentsByFilterQuery.cs @@ -0,0 +1,7 @@ +using Blog.Contracts.V1.Responses.CommentsResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.GetCommentsByFilter; + +public record GetCommentsByFilterQuery(string OrderBy, string SortBy, int? CurrentPage, int? PageSize, string DisplayType) + : SortParametersQuery(OrderBy, SortBy, CurrentPage, PageSize, DisplayType), IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByFilter/GetCommentsByFilterQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByFilter/GetCommentsByFilterQueryHandler.cs new file mode 100644 index 00000000..6bdf48d3 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByFilter/GetCommentsByFilterQueryHandler.cs @@ -0,0 +1,24 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses.CommentsResponses; +using Blog.EntityServices.Interfaces; +using Blog.Services.Core.Dtos; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.GetCommentsByFilter; + +public class GetCommentsByFilterQueryHandler( + ICommentsService service, + IMapper mapper) + : IQueryHandler +{ + public async Task Handle(GetCommentsByFilterQuery query, CancellationToken cancellationToken) + { + query = query with { CurrentPage = query.CurrentPage ?? 1 }; + query = query with { PageSize = 10 }; + + var result = await service.GetPagedComments( + mapper.Map(query)); + + return mapper.Map(result); + } +} \ No newline at end of file From ade15ec3eb05900dd5c6ed5f8b06869ea690b15e Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 23 Mar 2026 10:53:01 +0200 Subject: [PATCH 43/59] Add get comments by post query and query handler in comments feature --- .../GetCommentsByPostQuery.cs | 7 ++++++ .../GetCommentsByPostQueryHandler.cs | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByPost/GetCommentsByPostQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByPost/GetCommentsByPostQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByPost/GetCommentsByPostQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByPost/GetCommentsByPostQuery.cs new file mode 100644 index 00000000..4a29247d --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByPost/GetCommentsByPostQuery.cs @@ -0,0 +1,7 @@ +using Blog.Contracts.V1.Responses.CommentsResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.GetCommentsByPost; + +public record GetCommentsByPostQuery(int PostId, string OrderBy, string SortBy, int? CurrentPage, int? PageSize, string DisplayType) + : SortParametersQuery(OrderBy, SortBy, CurrentPage, PageSize, DisplayType), IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByPost/GetCommentsByPostQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByPost/GetCommentsByPostQueryHandler.cs new file mode 100644 index 00000000..3ab0245b --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Comments/GetCommentsByPost/GetCommentsByPostQueryHandler.cs @@ -0,0 +1,25 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses.CommentsResponses; +using Blog.EntityServices.Interfaces; +using Blog.Services.Core.Dtos; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Comments.GetCommentsByPost; + +public class GetCommentsByPostQueryHandler( + ICommentsService service, + IMapper mapper) + : IQueryHandler +{ + public async Task Handle(GetCommentsByPostQuery query, CancellationToken cancellationToken) + { + query = query with { CurrentPage = query.CurrentPage ?? 1 }; + query = query with { PageSize = 10 }; + + var result = await service.GetPagedCommentsByPostId( + query.PostId, + mapper.Map(query)); + + return mapper.Map(result); + } +} \ No newline at end of file From 819b5c59c0cee65760c13a60a6a6e2c4c627158a Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 23 Mar 2026 10:53:48 +0200 Subject: [PATCH 44/59] Add initialize query and query handler in health check feature --- .../HealthCheck/Initialize/InitializeQuery.cs | 6 ++++++ .../Initialize/InitializeQueryHandler.cs | 13 +++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/HealthCheck/Initialize/InitializeQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/HealthCheck/Initialize/InitializeQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/HealthCheck/Initialize/InitializeQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/HealthCheck/Initialize/InitializeQuery.cs new file mode 100644 index 00000000..47787772 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/HealthCheck/Initialize/InitializeQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses.PostsResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.HealthCheck.Initialize; + +public class InitializeQuery : IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/HealthCheck/Initialize/InitializeQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/HealthCheck/Initialize/InitializeQueryHandler.cs new file mode 100644 index 00000000..ad1237eb --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/HealthCheck/Initialize/InitializeQueryHandler.cs @@ -0,0 +1,13 @@ +using Blog.Contracts.V1.Responses.PostsResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.HealthCheck.Initialize; + +public class InitializeQueryHandler + : IQueryHandler +{ + public Task Handle(InitializeQuery request, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} \ No newline at end of file From 4469e2ada87066d3f1ebaf606a3defee1f881964 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 24 Mar 2026 10:55:07 +0200 Subject: [PATCH 45/59] Add create message command and command handler in messages feature --- .../CreateMessage/CreateMessageCommand.cs | 8 +++++ .../CreateMessageCommandHandler.cs | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageCommand.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageCommandHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageCommand.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageCommand.cs new file mode 100644 index 00000000..5c4dc2d1 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageCommand.cs @@ -0,0 +1,8 @@ +using Blog.Contracts.V1.Responses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.CreateMessage; + +public record CreateMessageCommand(string SenderId, string RecipientId, string SenderEmail, string SenderName, + string Subject, string Body, int MessageType) + : ICommand>; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageCommandHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageCommandHandler.cs new file mode 100644 index 00000000..48734a60 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageCommandHandler.cs @@ -0,0 +1,32 @@ +using Blog.Contracts.V1.Responses; +using Blog.Core.Enums; +using Blog.Data.Models; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.CreateMessage; + +public class CreateMessageCommandHandler(IMessagesService service) + : ICommandHandler> +{ + public async Task> Handle(CreateMessageCommand command, CancellationToken cancellationToken) + { + var message = new Message + { + SenderId = command.SenderId, + RecipientId = command.RecipientId, + SenderEmail = command.SenderEmail, + SenderName = command.SenderName, + Subject = command.Subject, + Body = command.Body, + MessageType = (MessageType)command.MessageType + }; + + await service.InsertAsync(message); + + return new CreatedResponse + { + Id = message.Id + }; + } +} \ No newline at end of file From 243d3928a1ddbd0f7156bb55cf5be182d10a5c74 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 24 Mar 2026 10:55:48 +0200 Subject: [PATCH 46/59] Add create message endpoint in messages feature --- .../CreateMessage/CreateMessageEndpoint.cs | 50 +++++++++++++++++++ .../CreateMessage/CreateMessageRequest.cs | 4 ++ 2 files changed, 54 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageEndpoint.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageRequest.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageEndpoint.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageEndpoint.cs new file mode 100644 index 00000000..645fee19 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageEndpoint.cs @@ -0,0 +1,50 @@ +using Asp.Versioning; +using Blog.Contracts.V1; +using Blog.Contracts.V1.Responses; +using Carter; +using Mapster; +using MediatR; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.CreateMessage; + +public class CreateMessageEndpoint : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var versionSet = app.NewApiVersionSet() + .HasApiVersion(new ApiVersion(1, 0)) + .ReportApiVersions() + .Build(); + + var group = app.MapGroup(ApiRoutes.MessagesController.Messages) + .WithApiVersionSet(versionSet) + .MapToApiVersion(1.0) + .WithTags("Messages"); + + group.MapPost("/", + async (CreateMessageRequest request, + ISender sender, + HttpContext context) => + { + var command = request.Adapt(); + + var result = await sender.Send(command); + + var location = + $"{context.Request.Scheme}://{context.Request.Host}" + + $"/messages/{result.Id}"; + + return Results.Created(location, new CreatedResponse + { + Id = result.Id + }); + }) + //.RequireAuthorization() + .WithName("CreateMessage") + .Produces>(201) + .Produces(400) + .WithSummary("Create message") + .WithDescription("Create message") + .HasApiVersion(1.0); + } +} diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageRequest.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageRequest.cs new file mode 100644 index 00000000..48bf4309 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/CreateMessage/CreateMessageRequest.cs @@ -0,0 +1,4 @@ +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.CreateMessage; + +public class CreateMessageRequest(string SenderId, string RecipientId, string SenderEmail, string SenderName, + string Subject, string Body, int MessageType); From 189290fa8188ad412f7ee0445f1fec23cc0ccba2 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 24 Mar 2026 10:56:19 +0200 Subject: [PATCH 47/59] Add get all query and query handler in messages feature --- .../Features/Messages/GetAll/GetAllQuery.cs | 6 ++++++ .../Messages/GetAll/GetAllQueryHandler.cs | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetAll/GetAllQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetAll/GetAllQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetAll/GetAllQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetAll/GetAllQuery.cs new file mode 100644 index 00000000..266eb9c2 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetAll/GetAllQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.GetAll; + +public record GetAllQuery : IQuery>; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetAll/GetAllQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetAll/GetAllQueryHandler.cs new file mode 100644 index 00000000..642613d0 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetAll/GetAllQueryHandler.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.GetAll; + +public class GetAllQueryHandler(IMessagesService service, IMapper mapper) + : IQueryHandler> +{ + public async Task> Handle(GetAllQuery query, CancellationToken cancellationToken) + { + var messages = await service.GetAllAsync(); + + return mapper.Map>(messages); + } +} \ No newline at end of file From 996966d20982dc4468f0526bcab60cb19216fcc2 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 24 Mar 2026 10:57:04 +0200 Subject: [PATCH 48/59] Add get by id query and query handler in messages feature --- .../Features/Messages/GetById/GetByIdQuery.cs | 6 ++++++ .../Messages/GetById/GetByIdQueryHandler.cs | 13 +++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetById/GetByIdQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetById/GetByIdQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetById/GetByIdQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetById/GetByIdQuery.cs new file mode 100644 index 00000000..ea810fdf --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetById/GetByIdQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.GetById; + +public record GetByIdQuery(int Id) : IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetById/GetByIdQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetById/GetByIdQueryHandler.cs new file mode 100644 index 00000000..81ecffe8 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetById/GetByIdQueryHandler.cs @@ -0,0 +1,13 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.GetById; + +public class GetByIdQueryHandler(IMessagesService service, IMapper mapper) + : IQueryHandler +{ + public async Task Handle(GetByIdQuery query, CancellationToken cancellationToken) + => mapper.Map(await service.FindAsync(query.Id)); +} \ No newline at end of file From cf4240c370f277e73c2cfc2219b2a3be1d868d30 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 25 Mar 2026 10:57:47 +0200 Subject: [PATCH 49/59] Add get recipient messages query and query handler in messages feature --- .../GetRecipientMessagesQuery.cs | 6 +++++ .../GetRecipientMessagesQueryHandler.cs | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetRecipientMessages/GetRecipientMessagesQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetRecipientMessages/GetRecipientMessagesQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetRecipientMessages/GetRecipientMessagesQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetRecipientMessages/GetRecipientMessagesQuery.cs new file mode 100644 index 00000000..4e043764 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetRecipientMessages/GetRecipientMessagesQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.GetRecipientMessages; + +public record GetRecipientMessagesQuery(string RecipientId) : IQuery>; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetRecipientMessages/GetRecipientMessagesQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetRecipientMessages/GetRecipientMessagesQueryHandler.cs new file mode 100644 index 00000000..91e82369 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetRecipientMessages/GetRecipientMessagesQueryHandler.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses; +using Blog.Contracts.V1.Responses.Chart; +using Blog.Data.Specifications; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; +using BlogVerticalSliceCQRSMinimalApi.Features.Comments.CommentsActivity; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.GetRecipientMessages; + +public class GetRecipientMessagesQueryHandler(IMessagesService service, IMapper mapper) + : IQueryHandler> +{ + public async Task> Handle(GetRecipientMessagesQuery query, + CancellationToken cancellationToken) + { + var messages = await service.GetAllAsync( + new MessageSpecification(x => + x.RecipientId.Equals(query.RecipientId, StringComparison.CurrentCultureIgnoreCase))); + + return mapper.Map>(messages); + } +} \ No newline at end of file From 06827d3aa9e103c7a14ed9d6410e8618b0d4c86c Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 25 Mar 2026 10:58:13 +0200 Subject: [PATCH 50/59] Add get sender messages query and query handler in messages feature --- .../GetSenderMessagesQuery.cs | 6 ++++++ .../GetSenderMessagesQueryHandler.cs | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetSenderMessages/GetSenderMessagesQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetSenderMessages/GetSenderMessagesQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetSenderMessages/GetSenderMessagesQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetSenderMessages/GetSenderMessagesQuery.cs new file mode 100644 index 00000000..91d8c685 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetSenderMessages/GetSenderMessagesQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.GetSenderMessages; + +public record GetSenderMessagesQuery(string SenderEmail) : IQuery>; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetSenderMessages/GetSenderMessagesQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetSenderMessages/GetSenderMessagesQueryHandler.cs new file mode 100644 index 00000000..2f509691 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Messages/GetSenderMessages/GetSenderMessagesQueryHandler.cs @@ -0,0 +1,20 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses; +using Blog.Data.Specifications; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Messages.GetSenderMessages; + +public class GetSenderMessagesQueryHandler(IMessagesService service, IMapper mapper) + : IQueryHandler> +{ + public async Task> Handle(GetSenderMessagesQuery query, CancellationToken cancellationToken) + { + var messages = await service.GetAllAsync( + new MessageSpecification(x => + x.SenderEmail.Equals(query.SenderEmail, StringComparison.CurrentCultureIgnoreCase))); + + return mapper.Map>(messages); + } +} \ No newline at end of file From 08337323d4f065f226a52a851ba2fbfe1ffdb372 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 25 Mar 2026 10:59:20 +0200 Subject: [PATCH 51/59] Add create post command and command handler in posts feature --- .../Posts/CreatePost/CreatePostCommand.cs | 9 +++++ .../CreatePost/CreatePostCommandHandler.cs | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostCommand.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostCommandHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostCommand.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostCommand.cs new file mode 100644 index 00000000..c2a446d7 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostCommand.cs @@ -0,0 +1,9 @@ +using Blog.Contracts.V1.Requests.TagsRequests; +using Blog.Contracts.V1.Responses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.CreatePost; + +public record CreatePostCommand(string Title, string Description, string Content, string ImageUrl, + string AuthorId, DateTime CreatedAt, IList Tags) + : ICommand>; diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostCommandHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostCommandHandler.cs new file mode 100644 index 00000000..708f0198 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostCommandHandler.cs @@ -0,0 +1,33 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses; +using Blog.Data.Models; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.CreatePost; + +public class CreatePostCommandHandler(IPostsService service, IMapper mapper) + : ICommandHandler> +{ + public async Task> Handle(CreatePostCommand command, CancellationToken cancellationToken) + { + var post = new Post + { + Title = command.Title, + Description = command.Description, + Content = command.Content, + ImageUrl = command.ImageUrl, + AuthorId = command.AuthorId, + CreatedAt = command.CreatedAt + }; + + var tags = mapper.Map>(command.Tags.Distinct()); + + await service.InsertAsync(post, tags); + + return new CreatedResponse + { + Id = post.Id + }; + } +} From 90d79bb63db512d49074a2c1160b9626fc5a8cfa Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 25 Mar 2026 11:00:09 +0200 Subject: [PATCH 52/59] Add create post endpoint posts feature --- .../Posts/CreatePost/CreatePostEndpoint.cs | 50 +++++++++++++++++++ .../Posts/CreatePost/CreatePostRequest.cs | 6 +++ 2 files changed, 56 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostEndpoint.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostRequest.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostEndpoint.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostEndpoint.cs new file mode 100644 index 00000000..ffabab51 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostEndpoint.cs @@ -0,0 +1,50 @@ +using Asp.Versioning; +using Blog.Contracts.V1; +using Blog.Contracts.V1.Responses; +using Carter; +using Mapster; +using MediatR; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.CreatePost; + +public class CreatePostEndpoint : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + var versionSet = app.NewApiVersionSet() + .HasApiVersion(new ApiVersion(1, 0)) + .ReportApiVersions() + .Build(); + + var group = app.MapGroup(ApiRoutes.PostsController.Posts) + .WithApiVersionSet(versionSet) + .MapToApiVersion(1.0) + .WithTags("Posts"); + + group.MapPost("/", + async (CreatePostRequest request, + ISender sender, + HttpContext context) => + { + var command = request.Adapt(); + + var result = await sender.Send(command); + + var location = + $"{context.Request.Scheme}://{context.Request.Host}" + + $"/posts/{result.Id}"; + + return Results.Created(location, new CreatedResponse + { + Id = result.Id + }); + }) + //.RequireAuthorization() + .WithName("CreatePost") + .Produces>(201) + .Produces(400) + .WithSummary("Create post") + .WithDescription("Create post") + .HasApiVersion(1.0); + } +} diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostRequest.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostRequest.cs new file mode 100644 index 00000000..11922b88 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/CreatePost/CreatePostRequest.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Requests.TagsRequests; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.CreatePost; + +public class CreatePostRequest(string Title, string Description, string Content, string ImageUrl, + string AuthorId, DateTime CreatedAt, IList Tags); \ No newline at end of file From b3b95dd5cd9fab11fab518268ab98abdec2efc74 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 25 Mar 2026 11:03:18 +0200 Subject: [PATCH 53/59] Add Carter, Mapster and MediatR configurations for endpoints --- .../BlogVerticalSliceCQRSMinimalApi.csproj | 2 ++ .../ApplicationBaseInstaller.cs | 6 ++++++ .../StartupConfigures/ConfigureRoutes.cs | 2 ++ 3 files changed, 10 insertions(+) diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj index 625c2245..d2582fed 100644 --- a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/BlogVerticalSliceCQRSMinimalApi.csproj @@ -12,6 +12,8 @@ + + diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs index b0595082..6c2cf632 100644 --- a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs @@ -23,5 +23,11 @@ public void InstallServices(IServiceCollection services, IConfiguration configur services.AddLocalization(); services.AddMemoryCache(); services.AddResponseCaching(); + + services.AddCarter(); + services.AddMediatR(configuration => + { + configuration.RegisterServicesFromAssembly(typeof(Program).Assembly); + }); } } \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureRoutes.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureRoutes.cs index 3142367b..ce413081 100644 --- a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureRoutes.cs +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/StartupConfigures/ConfigureRoutes.cs @@ -76,6 +76,8 @@ public static void Configure(WebApplication app) //endpoints.MapHealthChecks("/health"); }); + app.MapCarter(); + app.MapDefaultEndpoints(); //TODO: move to StartupConfigures From 2a9d4e957c986b8d6ca95b69b08c5b1f607b903b Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 26 Mar 2026 11:04:05 +0200 Subject: [PATCH 54/59] Cofigure program --- .../BlogVerticalSliceCQRSMinimalApi/Program.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Program.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Program.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Program.cs new file mode 100644 index 00000000..6b258d6b --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Program.cs @@ -0,0 +1,18 @@ +using BlogVerticalSliceCQRSMinimalApi.StartupConfigures; +using BlogVerticalSliceCQRSMinimalApi.StartupConfigureServicesInstallers; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add services to the container. +builder.Services.InstallServicesInAssembly(builder.Configuration); + +var app = builder.Build(); + +ConfigureBase.Configure(app, builder.Environment); +ConfigureSwagger.Configure(app, builder.Configuration); +ConfigureAuthentication.Configure(app); +ConfigureRoutes.Configure(app); + +app.Run(); \ No newline at end of file From 3cfcab97111a082682296065ee38abe607bb2b90 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 26 Mar 2026 11:04:38 +0200 Subject: [PATCH 55/59] Add get all posts query and query handler in posts feature --- .../Posts/GetAllPosts/GetAllPostsQuery.cs | 6 ++++++ .../GetAllPosts/GetAllPostsQueryHandler.cs | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetAllPosts/GetAllPostsQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetAllPosts/GetAllPostsQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetAllPosts/GetAllPostsQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetAllPosts/GetAllPostsQuery.cs new file mode 100644 index 00000000..9ffb4964 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetAllPosts/GetAllPostsQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses.PostsResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.GetAllPosts; + +public record GetAllPostsQuery : IQuery>; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetAllPosts/GetAllPostsQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetAllPosts/GetAllPostsQueryHandler.cs new file mode 100644 index 00000000..1a91958f --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetAllPosts/GetAllPostsQueryHandler.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses.PostsResponses; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.GetAllPosts; + +public class GetAllPostsQueryHandler(IPostsService service, IMapper mapper) + : IQueryHandler> +{ + public async Task> Handle(GetAllPostsQuery query, CancellationToken cancellationToken) + { + var posts = await service.GetAllAsync(); + + return mapper.Map>(posts); + } +} \ No newline at end of file From fbe573f842dc94ea0dad9f5cc4c9d21a4a7c03b3 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 26 Mar 2026 11:05:20 +0200 Subject: [PATCH 56/59] Add get post by id query and query handler in posts feature --- .../Posts/GetPostById/GetPostByIdQuery.cs | 6 +++++ .../GetPostById/GetPostByIdQueryHandler.cs | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPostById/GetPostByIdQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPostById/GetPostByIdQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPostById/GetPostByIdQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPostById/GetPostByIdQuery.cs new file mode 100644 index 00000000..29fdeb3d --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPostById/GetPostByIdQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses.PostsResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.GetPostById; + +public record GetPostByIdQuery(int Id) : IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPostById/GetPostByIdQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPostById/GetPostByIdQueryHandler.cs new file mode 100644 index 00000000..f0bc0010 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPostById/GetPostByIdQueryHandler.cs @@ -0,0 +1,24 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses.PostsResponses; +using Blog.EntityServices.Interfaces; +using Blog.Services.Core.Dtos; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.GetPostById; + +public class GetPostByIdQueryHandler(IPostsService service, IMapper mapper) + : IQueryHandler +{ + public async Task Handle(GetPostByIdQuery query, CancellationToken cancellationToken) + { + var sort = new SortParametersDto + { + CurrentPage = 1, + PageSize = 10 + }; + + var post = await service.GetPost(query.Id, sort); + + return mapper.Map(post); + } +} \ No newline at end of file From be0f0868875cd766efa24575a3b6778e7541a7ca Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 26 Mar 2026 11:06:14 +0200 Subject: [PATCH 57/59] Add get posts query and query handler in posts feature --- .../CQRSExtensions/SearchParametersQuery.cs | 4 ++++ .../CQRSExtensions/SortParametersQuery.cs | 3 +++ .../Features/Posts/GetPosts/GetPostsQuery.cs | 7 ++++++ .../Posts/GetPosts/GetPostsQueryHandler.cs | 24 +++++++++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/SearchParametersQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/SortParametersQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPosts/GetPostsQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPosts/GetPostsQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/SearchParametersQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/SearchParametersQuery.cs new file mode 100644 index 00000000..67748aa3 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/SearchParametersQuery.cs @@ -0,0 +1,4 @@ +namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +public record SearchParametersQuery(string OrderBy, string SortBy, int? CurrentPage, int? PageSize, string DisplayType) + : SortParametersQuery(OrderBy, SortBy, CurrentPage, PageSize, DisplayType); \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/SortParametersQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/SortParametersQuery.cs new file mode 100644 index 00000000..7392f54a --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/CQRSExtensions/SortParametersQuery.cs @@ -0,0 +1,3 @@ +namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +public record SortParametersQuery(string OrderBy, string SortBy, int? CurrentPage, int? PageSize, string DisplayType); \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPosts/GetPostsQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPosts/GetPostsQuery.cs new file mode 100644 index 00000000..3b7ef6df --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPosts/GetPostsQuery.cs @@ -0,0 +1,7 @@ +using Blog.Contracts.V1.Responses.PostsResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.GetPosts; + +public record GetPostsQuery(string OrderBy, string SortBy, int? CurrentPage, int? PageSize, string DisplayType, string Tag) + : SearchParametersQuery(OrderBy, SortBy, CurrentPage, PageSize, DisplayType), IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPosts/GetPostsQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPosts/GetPostsQueryHandler.cs new file mode 100644 index 00000000..aba8a06f --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/GetPosts/GetPostsQueryHandler.cs @@ -0,0 +1,24 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses.PostsResponses; +using Blog.EntityServices.Interfaces; +using Blog.Services.Core.Dtos.Posts; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.GetPosts; + +public class GetPostsQueryHandler(IPostsService service, IMapper mapper) + : IQueryHandler +{ + public async Task Handle(GetPostsQuery query, CancellationToken cancellationToken) + { + query = query with { OrderBy = query.OrderBy ?? "asc" }; + query = query with { SortBy = query.SortBy ?? "Title" }; + query = query with { CurrentPage = query.CurrentPage ?? 1 }; + query = query with { PageSize = query.PageSize ?? 10 }; + + var posts = await service.GetPostsAsync( + mapper.Map(query)); + + return mapper.Map(posts); + } +} \ No newline at end of file From d7871b80617d5c4fc197587c72b7853561a4d5e4 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 27 Mar 2026 11:06:53 +0200 Subject: [PATCH 58/59] Add posts activity query and query handler in posts feature --- .../Posts/PostsActivity/PostsActivityQuery.cs | 6 ++++++ .../Posts/PostsActivity/PostsActivityQueryHandler.cs | 12 ++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/PostsActivity/PostsActivityQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/PostsActivity/PostsActivityQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/PostsActivity/PostsActivityQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/PostsActivity/PostsActivityQuery.cs new file mode 100644 index 00000000..6105f787 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/PostsActivity/PostsActivityQuery.cs @@ -0,0 +1,6 @@ +using Blog.Contracts.V1.Responses.Chart; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.PostsActivity; + +public record PostsActivityQuery : IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/PostsActivity/PostsActivityQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/PostsActivity/PostsActivityQueryHandler.cs new file mode 100644 index 00000000..2137f6da --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/PostsActivity/PostsActivityQueryHandler.cs @@ -0,0 +1,12 @@ +using Blog.Contracts.V1.Responses.Chart; +using Blog.EntityServices.Interfaces; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.PostsActivity; + +public class PostsActivityQueryHandler(IPostsService service) + : IQueryHandler +{ + public async Task Handle(PostsActivityQuery query, CancellationToken cancellationToken) + => await service.GetPostsActivity(); +} \ No newline at end of file From 977e837499700d0f0df7c721e1d8f6f5fe8d239c Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 27 Mar 2026 11:07:22 +0200 Subject: [PATCH 59/59] Add users posts query and query handler in posts feature --- .../Posts/UserPosts/UserPostsQuery.cs | 7 ++++++ .../Posts/UserPosts/UserPostsQueryHandler.cs | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/UserPosts/UserPostsQuery.cs create mode 100644 BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/UserPosts/UserPostsQueryHandler.cs diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/UserPosts/UserPostsQuery.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/UserPosts/UserPostsQuery.cs new file mode 100644 index 00000000..f00c5064 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/UserPosts/UserPostsQuery.cs @@ -0,0 +1,7 @@ +using Blog.Contracts.V1.Responses.PostsResponses; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.UserPosts; + +public record UserPostsQuery(string OrderBy, string SortBy, int? CurrentPage, int? PageSize, string DisplayType, string Tag, string Id) + : SearchParametersQuery(OrderBy, SortBy, CurrentPage, PageSize, DisplayType), IQuery; \ No newline at end of file diff --git a/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/UserPosts/UserPostsQueryHandler.cs b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/UserPosts/UserPostsQueryHandler.cs new file mode 100644 index 00000000..6ab993d5 --- /dev/null +++ b/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/Features/Posts/UserPosts/UserPostsQueryHandler.cs @@ -0,0 +1,25 @@ +using AutoMapper; +using Blog.Contracts.V1.Responses.PostsResponses; +using Blog.EntityServices.Interfaces; +using Blog.Services.Core.Dtos.Posts; +using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions; + +namespace BlogVerticalSliceCQRSMinimalApi.Features.Posts.UserPosts; + +public class UserPostsQueryHandler(IPostsService service, IMapper mapper) + : IQueryHandler +{ + public async Task Handle(UserPostsQuery query, CancellationToken cancellationToken) + { + query = query with { OrderBy = query.OrderBy ?? "asc" }; + query = query with { SortBy = query.SortBy ?? "Title" }; + query = query with { CurrentPage = query.CurrentPage ?? 1 }; + query = query with { PageSize = query.PageSize ?? 10 }; + + var posts = await service.GetUserPostsAsync( + query.Id, + mapper.Map(query)); + + return mapper.Map(posts); + } +} \ No newline at end of file