From 6564b108dd689ff511edc17c9ebe1b1d5b85272e Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 27 Feb 2026 00:18:04 +0200 Subject: [PATCH 01/33] Created BlogMinimalApi project --- .gitignore | 2 + .../BlogMinimalApi/BlogMinimalApi.csproj | 33 +++++++++++++++ .../BlogMinimalApi/BlogMinimalApi.csproj.user | 9 ++++ BlogWebApp/BlogMinimalApi/BlogMinimalApi.http | 6 +++ BlogWebApp/BlogMinimalApi/Program.cs | 7 ++++ .../appsettings.Development.json | 8 ++++ BlogWebApp/BlogMinimalApi/appsettings.json | 42 +++++++++++++++++++ 7 files changed, 107 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/BlogMinimalApi.csproj create mode 100644 BlogWebApp/BlogMinimalApi/BlogMinimalApi.csproj.user create mode 100644 BlogWebApp/BlogMinimalApi/BlogMinimalApi.http create mode 100644 BlogWebApp/BlogMinimalApi/Program.cs create mode 100644 BlogWebApp/BlogMinimalApi/appsettings.Development.json create mode 100644 BlogWebApp/BlogMinimalApi/appsettings.json diff --git a/.gitignore b/.gitignore index 8b5408e2..4f5f2068 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,5 @@ /BlogWebApp/Tests/Blog.UnitTests/bin/Debug/net10.0 /Sdk/Blog.Sdk.ExampleUsage/bin/Debug/net10.0 /BlogWebApp/Aspire/Blog.Aspire/Blog.Aspire.AppHost/bin/Debug/net10.0 +/BlogWebApp/BlogMinimalApi/bin/Debug/net10.0 +/BlogWebApp/BlogMinimalApi/obj diff --git a/BlogWebApp/BlogMinimalApi/BlogMinimalApi.csproj b/BlogWebApp/BlogMinimalApi/BlogMinimalApi.csproj new file mode 100644 index 00000000..528ebc34 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/BlogMinimalApi.csproj @@ -0,0 +1,33 @@ + + + + net10.0 + enable + enable + + + + true + $(NoWarn);1591 + + + + + + + + + + + + + + + + + + + + + + diff --git a/BlogWebApp/BlogMinimalApi/BlogMinimalApi.csproj.user b/BlogWebApp/BlogMinimalApi/BlogMinimalApi.csproj.user new file mode 100644 index 00000000..983ecfc0 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/BlogMinimalApi.csproj.user @@ -0,0 +1,9 @@ + + + + http + + + ProjectDebugger + + \ No newline at end of file diff --git a/BlogWebApp/BlogMinimalApi/BlogMinimalApi.http b/BlogWebApp/BlogMinimalApi/BlogMinimalApi.http new file mode 100644 index 00000000..819be2fd --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/BlogMinimalApi.http @@ -0,0 +1,6 @@ +@BlogMinimalApi_HostAddress = http://localhost:5182 + +GET {{BlogMinimalApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/BlogWebApp/BlogMinimalApi/Program.cs b/BlogWebApp/BlogMinimalApi/Program.cs new file mode 100644 index 00000000..a4984275 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Program.cs @@ -0,0 +1,7 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +var app = builder.Build(); + +app.Run(); \ No newline at end of file diff --git a/BlogWebApp/BlogMinimalApi/appsettings.Development.json b/BlogWebApp/BlogMinimalApi/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/BlogWebApp/BlogMinimalApi/appsettings.json b/BlogWebApp/BlogMinimalApi/appsettings.json new file mode 100644 index 00000000..51fd74aa --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/appsettings.json @@ -0,0 +1,42 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=.;Database=BlogAspNetCoreNew;Integrated Security=False;Persist Security Info=False;User ID=n;Password=123123;Encrypt=false;TrustServerCertificate=True" + }, + + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + }, + + "JwtIssuerOptions": { + "Issuer": "BlogWebApi", + "Audience": "http://localhost:5182", + "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 ab764a02b9fd8d3461fbac00a21d6ac81b3f0a37 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 27 Feb 2026 00:19:28 +0200 Subject: [PATCH 02/33] Add Startup configure services installers --- .../IInstaller.cs | 17 ++++++++++ .../InstallerExtensions.cs | 33 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/IInstaller.cs create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/InstallerExtensions.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/IInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/IInstaller.cs new file mode 100644 index 00000000..e0142ac3 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/IInstaller.cs @@ -0,0 +1,17 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/StartupConfigureServicesInstallers/InstallerExtensions.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/InstallerExtensions.cs new file mode 100644 index 00000000..c7ba658c --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/InstallerExtensions.cs @@ -0,0 +1,33 @@ +namespace BlogMinimalApi.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 fee110d04c79d6265b041a3b1ac84436f4edb77f Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 27 Feb 2026 00:20:24 +0200 Subject: [PATCH 03/33] Add Api versioning installer --- .../ApiVersioningInstaller.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApiVersioningInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApiVersioningInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApiVersioningInstaller.cs new file mode 100644 index 00000000..b0e55675 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApiVersioningInstaller.cs @@ -0,0 +1,43 @@ +namespace BlogMinimalApi.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 8f04486080fa854f698b436cf1cc5f065f4368cd Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 27 Feb 2026 00:20:46 +0200 Subject: [PATCH 04/33] Add Application base installer --- .../ApplicationBaseInstaller.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs new file mode 100644 index 00000000..26b4fee3 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApplicationBaseInstaller.cs @@ -0,0 +1,25 @@ +namespace BlogMinimalApi.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 d7c45fbefe151ff92605d7c3f7c21661be2f4781 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 28 Feb 2026 00:21:14 +0200 Subject: [PATCH 05/33] Add Application services installer --- .../ApplicationServicesInstaller.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs new file mode 100644 index 00000000..5c045ea1 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/ApplicationServicesInstaller.cs @@ -0,0 +1,75 @@ +namespace BlogMinimalApi.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; +using Blog.CommonServices.EmailServices; +using Blog.CommonServices.EmailServices.Interfaces; +using Blog.CommonServices.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 2336bedc35e5c2d0e71d748cc083dacc12a0300e Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 28 Feb 2026 00:21:44 +0200 Subject: [PATCH 06/33] Add Authentication and authorization installer --- ...AuthenticationAndAuthorizationInstaller.cs | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/AuthenticationAndAuthorizationInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/AuthenticationAndAuthorizationInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/AuthenticationAndAuthorizationInstaller.cs new file mode 100644 index 00000000..f07fdeaa --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/AuthenticationAndAuthorizationInstaller.cs @@ -0,0 +1,119 @@ +namespace BlogMinimalApi.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 3cdf743a8c3067aaebd17a9724bcb04f79b4eb2b Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 28 Feb 2026 00:22:04 +0200 Subject: [PATCH 07/33] Add Cache installer --- .../CacheInstaller.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/CacheInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/CacheInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/CacheInstaller.cs new file mode 100644 index 00000000..77548c85 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/CacheInstaller.cs @@ -0,0 +1,32 @@ +namespace BlogMinimalApi.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 dd2af3eaff95cdc8ef150692ee786e9b2f08b165 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 28 Feb 2026 00:22:21 +0200 Subject: [PATCH 08/33] Add Cors installer --- .../CorsInstaller.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/CorsInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/CorsInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/CorsInstaller.cs new file mode 100644 index 00000000..281981ff --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/CorsInstaller.cs @@ -0,0 +1,45 @@ +namespace BlogMinimalApi.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 16731ad6e60fdb760f1dbc73cd3a57375f233e8c Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 1 Mar 2026 00:22:44 +0200 Subject: [PATCH 09/33] Add Data repositories installer --- .../DataRepositoriesInstaller.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs new file mode 100644 index 00000000..e90ce116 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/DataRepositoriesInstaller.cs @@ -0,0 +1,30 @@ +namespace BlogMinimalApi.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 287dbeade8315336f78f20945754bde6120a91f6 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 1 Mar 2026 00:23:02 +0200 Subject: [PATCH 10/33] Add Db installer --- .../DbInstaller.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/DbInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/DbInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/DbInstaller.cs new file mode 100644 index 00000000..f8a6f25a --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/DbInstaller.cs @@ -0,0 +1,22 @@ +namespace BlogMinimalApi.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 f035faaab60647e205825dd88d08c6672094fb53 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 1 Mar 2026 08:52:35 +0200 Subject: [PATCH 11/33] Add Email installer --- .../EmailInstaller.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/EmailInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/EmailInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/EmailInstaller.cs new file mode 100644 index 00000000..d9c753ad --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/EmailInstaller.cs @@ -0,0 +1,44 @@ +namespace BlogMinimalApi.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 0d67ed92eae0c58d4820fc84758ae9a01dff8bd9 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sun, 1 Mar 2026 08:53:14 +0200 Subject: [PATCH 12/33] Add Health Check configuration --- .../HealthChecks/ReadinessPublisher.cs | 55 +++++++++++++++++++ .../HealthChecks/RedisHealthCheck.cs | 30 ++++++++++ 2 files changed, 85 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/HealthChecks/ReadinessPublisher.cs create mode 100644 BlogWebApp/BlogMinimalApi/HealthChecks/RedisHealthCheck.cs diff --git a/BlogWebApp/BlogMinimalApi/HealthChecks/ReadinessPublisher.cs b/BlogWebApp/BlogMinimalApi/HealthChecks/ReadinessPublisher.cs new file mode 100644 index 00000000..21043b42 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/HealthChecks/ReadinessPublisher.cs @@ -0,0 +1,55 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/HealthChecks/RedisHealthCheck.cs b/BlogWebApp/BlogMinimalApi/HealthChecks/RedisHealthCheck.cs new file mode 100644 index 00000000..8d86e530 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/HealthChecks/RedisHealthCheck.cs @@ -0,0 +1,30 @@ +namespace BlogMinimalApi.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 From 2b4fef14bb8366e5a6f61d5e89d53039bb4b111b Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 2 Mar 2026 08:54:16 +0200 Subject: [PATCH 13/33] Add Health check installer --- .../Properties/launchSettings.json | 23 ++++++++++++++ .../HealthCheckInstaller.cs | 30 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/Properties/launchSettings.json create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/HealthCheckInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/Properties/launchSettings.json b/BlogWebApp/BlogMinimalApi/Properties/launchSettings.json new file mode 100644 index 00000000..d4fc46c0 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/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:5182", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7060;http://localhost:5182", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/HealthCheckInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/HealthCheckInstaller.cs new file mode 100644 index 00000000..2cb110fe --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/HealthCheckInstaller.cs @@ -0,0 +1,30 @@ +namespace BlogMinimalApi.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 f7007c76e90265e2af53de034d98ef855e295bcd Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 2 Mar 2026 08:55:50 +0200 Subject: [PATCH 14/33] Add Identity installer --- .../IdentityInstaller.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/IdentityInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/IdentityInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/IdentityInstaller.cs new file mode 100644 index 00000000..6899bb62 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/IdentityInstaller.cs @@ -0,0 +1,54 @@ +namespace BlogMinimalApi.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 From b420e01ed4edf8fe1334c9698c697e6e625026b3 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 2 Mar 2026 08:56:17 +0200 Subject: [PATCH 15/33] Add Mediator installer --- .../MediatorInstaller.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/MediatorInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/MediatorInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/MediatorInstaller.cs new file mode 100644 index 00000000..e8f848f3 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/MediatorInstaller.cs @@ -0,0 +1,17 @@ +namespace BlogMinimalApi.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 8dd9b6b76a3ddb27012399ffdac51acd9d06d0f8 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Mon, 2 Mar 2026 08:57:04 +0200 Subject: [PATCH 16/33] Add Swagger installer --- .../SwaggerInstaller.cs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs new file mode 100644 index 00000000..efda7fef --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs @@ -0,0 +1,103 @@ +namespace BlogMinimalApi.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.Collections.Generic; + 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(); + }); + } + } +} \ No newline at end of file From becb1517ff7c2244a58e72f03ab171c468ad1449 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 3 Mar 2026 09:31:12 +0200 Subject: [PATCH 17/33] Add cached attribute --- .../BlogMinimalApi/Cache/CachedAttribute.cs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/Cache/CachedAttribute.cs diff --git a/BlogWebApp/BlogMinimalApi/Cache/CachedAttribute.cs b/BlogWebApp/BlogMinimalApi/Cache/CachedAttribute.cs new file mode 100644 index 00000000..d8228953 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Cache/CachedAttribute.cs @@ -0,0 +1,88 @@ +namespace BlogMinimalApi.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 From b68c1abab52a723bac8466863f29ac62da4a310f Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 3 Mar 2026 09:31:54 +0200 Subject: [PATCH 18/33] Add Swagger filter --- .../ApplySummariesOperationFilter.cs | 30 +++++++++++++++++ .../SwaggerFilters/LowercaseDocumentFilter.cs | 28 ++++++++++++++++ .../SwaggerGroupOperationFilter.cs | 32 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/Filters/SwaggerFilters/ApplySummariesOperationFilter.cs create mode 100644 BlogWebApp/BlogMinimalApi/Filters/SwaggerFilters/LowercaseDocumentFilter.cs create mode 100644 BlogWebApp/BlogMinimalApi/Filters/SwaggerFilters/SwaggerGroupOperationFilter.cs diff --git a/BlogWebApp/BlogMinimalApi/Filters/SwaggerFilters/ApplySummariesOperationFilter.cs b/BlogWebApp/BlogMinimalApi/Filters/SwaggerFilters/ApplySummariesOperationFilter.cs new file mode 100644 index 00000000..71e6d534 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Filters/SwaggerFilters/ApplySummariesOperationFilter.cs @@ -0,0 +1,30 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/Filters/SwaggerFilters/LowercaseDocumentFilter.cs b/BlogWebApp/BlogMinimalApi/Filters/SwaggerFilters/LowercaseDocumentFilter.cs new file mode 100644 index 00000000..a9ce1df3 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Filters/SwaggerFilters/LowercaseDocumentFilter.cs @@ -0,0 +1,28 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/Filters/SwaggerFilters/SwaggerGroupOperationFilter.cs b/BlogWebApp/BlogMinimalApi/Filters/SwaggerFilters/SwaggerGroupOperationFilter.cs new file mode 100644 index 00000000..676cfa99 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Filters/SwaggerFilters/SwaggerGroupOperationFilter.cs @@ -0,0 +1,32 @@ +namespace BlogMinimalApi.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 From 66f5e0eed5b6f65219cd830b907e8838bf7d5bf0 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 3 Mar 2026 09:32:10 +0200 Subject: [PATCH 19/33] Add validation filter --- .../Filters/ValidationFilter.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/Filters/ValidationFilter.cs diff --git a/BlogWebApp/BlogMinimalApi/Filters/ValidationFilter.cs b/BlogWebApp/BlogMinimalApi/Filters/ValidationFilter.cs new file mode 100644 index 00000000..09b62c9b --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Filters/ValidationFilter.cs @@ -0,0 +1,54 @@ +namespace BlogMinimalApi.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 a407354dd4142cd2b98da2ae0fdcb8a973f2a66f Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Tue, 3 Mar 2026 09:33:32 +0200 Subject: [PATCH 20/33] Add swagger requests and responses examples --- .../SwaggerInstaller.cs | 2 + .../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 +++ 32 files changed, 1208 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/CommentsRequests/CreateCommentRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/CommentsRequests/UpdateCommentRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/MessagesRequests/CreateMessageRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/MessagesRequests/UpdateMessageRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/PostsRequests/CreatePostRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/PostsRequests/UpdatePostRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/SearchParametersRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/SortParametersRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/CreateTagRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/TagRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/UpdateTagRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/ChangePasswordRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/LoginRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/RegistrationRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/UpdateProfileRequestExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/CommentsResponses/CommentResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/CommentsResponses/PagedCommentsResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/CreatedResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/MessageResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PageInfoResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PagedPostsResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostTagRelationsResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostViewResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostWithPagedCommentsResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/TagsResponses/PagedTagsResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/TagsResponses/TagResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/AccountResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/ApplicationUserResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/ProfileResponseExample.cs create mode 100644 BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/RoleResponseExample.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs index efda7fef..80cb3c42 100644 --- a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs +++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs @@ -98,6 +98,8 @@ public void InstallServices(IServiceCollection services, IConfiguration configur c.OperationFilter(); }); + + services.AddSwaggerExamplesFromAssemblyOf(); } } } \ No newline at end of file diff --git a/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/CommentsRequests/CreateCommentRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/CommentsRequests/CreateCommentRequestExample.cs new file mode 100644 index 00000000..670e250d --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/CommentsRequests/CreateCommentRequestExample.cs @@ -0,0 +1,24 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/CommentsRequests/UpdateCommentRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/CommentsRequests/UpdateCommentRequestExample.cs new file mode 100644 index 00000000..93207a4f --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/CommentsRequests/UpdateCommentRequestExample.cs @@ -0,0 +1,27 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/MessagesRequests/CreateMessageRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/MessagesRequests/CreateMessageRequestExample.cs new file mode 100644 index 00000000..b6b3082a --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/MessagesRequests/CreateMessageRequestExample.cs @@ -0,0 +1,24 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/MessagesRequests/UpdateMessageRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/MessagesRequests/UpdateMessageRequestExample.cs new file mode 100644 index 00000000..536477f1 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/MessagesRequests/UpdateMessageRequestExample.cs @@ -0,0 +1,26 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/PostsRequests/CreatePostRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/PostsRequests/CreatePostRequestExample.cs new file mode 100644 index 00000000..b3029740 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/PostsRequests/CreatePostRequestExample.cs @@ -0,0 +1,34 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/PostsRequests/UpdatePostRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/PostsRequests/UpdatePostRequestExample.cs new file mode 100644 index 00000000..86254223 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/PostsRequests/UpdatePostRequestExample.cs @@ -0,0 +1,38 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/SearchParametersRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/SearchParametersRequestExample.cs new file mode 100644 index 00000000..3985db7a --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/SearchParametersRequestExample.cs @@ -0,0 +1,28 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/SortParametersRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/SortParametersRequestExample.cs new file mode 100644 index 00000000..b1f03361 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/SortParametersRequestExample.cs @@ -0,0 +1,24 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/CreateTagRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/CreateTagRequestExample.cs new file mode 100644 index 00000000..39b00863 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/CreateTagRequestExample.cs @@ -0,0 +1,21 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/TagRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/TagRequestExample.cs new file mode 100644 index 00000000..7f008ff3 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/TagRequestExample.cs @@ -0,0 +1,21 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/UpdateTagRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/UpdateTagRequestExample.cs new file mode 100644 index 00000000..fd9ede90 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/TagsRequests/UpdateTagRequestExample.cs @@ -0,0 +1,21 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/ChangePasswordRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/ChangePasswordRequestExample.cs new file mode 100644 index 00000000..f84abe73 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/ChangePasswordRequestExample.cs @@ -0,0 +1,23 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/LoginRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/LoginRequestExample.cs new file mode 100644 index 00000000..d347ff79 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/LoginRequestExample.cs @@ -0,0 +1,22 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/RegistrationRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/RegistrationRequestExample.cs new file mode 100644 index 00000000..7ef1ee8b --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/RegistrationRequestExample.cs @@ -0,0 +1,32 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/UpdateProfileRequestExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/UpdateProfileRequestExample.cs new file mode 100644 index 00000000..cd790414 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Requests/UsersRequests/UpdateProfileRequestExample.cs @@ -0,0 +1,26 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/CommentsResponses/CommentResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/CommentsResponses/CommentResponseExample.cs new file mode 100644 index 00000000..8b4ece3a --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/CommentsResponses/CommentResponseExample.cs @@ -0,0 +1,26 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/CommentsResponses/PagedCommentsResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/CommentsResponses/PagedCommentsResponseExample.cs new file mode 100644 index 00000000..c05767f6 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/CommentsResponses/PagedCommentsResponseExample.cs @@ -0,0 +1,49 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/CreatedResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/CreatedResponseExample.cs new file mode 100644 index 00000000..3cd33c9b --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/CreatedResponseExample.cs @@ -0,0 +1,22 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/MessageResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/MessageResponseExample.cs new file mode 100644 index 00000000..315ea3cb --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/MessageResponseExample.cs @@ -0,0 +1,60 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/PageInfoResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PageInfoResponseExample.cs new file mode 100644 index 00000000..d0a4dc72 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PageInfoResponseExample.cs @@ -0,0 +1,22 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PagedPostsResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PagedPostsResponseExample.cs new file mode 100644 index 00000000..991d5aed --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PagedPostsResponseExample.cs @@ -0,0 +1,175 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostResponseExample.cs new file mode 100644 index 00000000..b1341d1d --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostResponseExample.cs @@ -0,0 +1,71 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostTagRelationsResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostTagRelationsResponseExample.cs new file mode 100644 index 00000000..531a677a --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostTagRelationsResponseExample.cs @@ -0,0 +1,37 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostViewResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostViewResponseExample.cs new file mode 100644 index 00000000..2cbdab2b --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostViewResponseExample.cs @@ -0,0 +1,67 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostWithPagedCommentsResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostWithPagedCommentsResponseExample.cs new file mode 100644 index 00000000..051f0155 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/PostsResponses/PostWithPagedCommentsResponseExample.cs @@ -0,0 +1,80 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/TagsResponses/PagedTagsResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/TagsResponses/PagedTagsResponseExample.cs new file mode 100644 index 00000000..fe8e7591 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/TagsResponses/PagedTagsResponseExample.cs @@ -0,0 +1,49 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/TagsResponses/TagResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/TagsResponses/TagResponseExample.cs new file mode 100644 index 00000000..ed53ea6b --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/TagsResponses/TagResponseExample.cs @@ -0,0 +1,22 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/AccountResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/AccountResponseExample.cs new file mode 100644 index 00000000..607330b7 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/AccountResponseExample.cs @@ -0,0 +1,36 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/ApplicationUserResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/ApplicationUserResponseExample.cs new file mode 100644 index 00000000..4ba14a2b --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/ApplicationUserResponseExample.cs @@ -0,0 +1,34 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/ProfileResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/ProfileResponseExample.cs new file mode 100644 index 00000000..55977fff --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/ProfileResponseExample.cs @@ -0,0 +1,42 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/RoleResponseExample.cs b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/RoleResponseExample.cs new file mode 100644 index 00000000..4f1c4ee4 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/SwaggerExamples/Responses/UsersResponses/RoleResponseExample.cs @@ -0,0 +1,23 @@ +namespace BlogMinimalApi.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 71aac4393ec241d4f9c2414940274d1c16ccbee7 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 4 Mar 2026 09:35:24 +0200 Subject: [PATCH 21/33] 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/BlogMinimalApi/VIewModels/AspNetUser/EnableAuthenticatorViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/ForgotPasswordViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/LoginInfoViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/TwoFactorAuthenticationViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/UserCollectionViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/UserItemViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/UserTableViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/Manage/AddPhoneNumberViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/Manage/ChangePasswordViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/Manage/ConfigureTwoFactorViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/Manage/FactorViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/Manage/IndexViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/Manage/ManageLoginsViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/Manage/RemoveLoginViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/Manage/SetPasswordViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/Manage/VerifyPhoneNumberViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/Settings/SettingViewModel.cs create mode 100644 BlogWebApp/BlogMinimalApi/VIewModels/Settings/SettingsListViewModel.cs diff --git a/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/EnableAuthenticatorViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/EnableAuthenticatorViewModel.cs new file mode 100644 index 00000000..2620e81f --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/EnableAuthenticatorViewModel.cs @@ -0,0 +1,31 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/AspNetUser/ForgotPasswordViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/ForgotPasswordViewModel.cs new file mode 100644 index 00000000..023ae27d --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/ForgotPasswordViewModel.cs @@ -0,0 +1,16 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/AspNetUser/LoginInfoViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/LoginInfoViewModel.cs new file mode 100644 index 00000000..22a9ab79 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/LoginInfoViewModel.cs @@ -0,0 +1,17 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/AspNetUser/TwoFactorAuthenticationViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/TwoFactorAuthenticationViewModel.cs new file mode 100644 index 00000000..6b300ade --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/TwoFactorAuthenticationViewModel.cs @@ -0,0 +1,22 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/AspNetUser/UserCollectionViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/UserCollectionViewModel.cs new file mode 100644 index 00000000..26260ee9 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/UserCollectionViewModel.cs @@ -0,0 +1,14 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/AspNetUser/UserItemViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/UserItemViewModel.cs new file mode 100644 index 00000000..4ba5126e --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/UserItemViewModel.cs @@ -0,0 +1,71 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/AspNetUser/UserTableViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/UserTableViewModel.cs new file mode 100644 index 00000000..03c8dc93 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/AspNetUser/UserTableViewModel.cs @@ -0,0 +1,19 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/Manage/AddPhoneNumberViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/AddPhoneNumberViewModel.cs new file mode 100644 index 00000000..92f0648d --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/AddPhoneNumberViewModel.cs @@ -0,0 +1,17 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/Manage/ChangePasswordViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/ChangePasswordViewModel.cs new file mode 100644 index 00000000..ed1a0be7 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/ChangePasswordViewModel.cs @@ -0,0 +1,26 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/Manage/ConfigureTwoFactorViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/ConfigureTwoFactorViewModel.cs new file mode 100644 index 00000000..7b298b9a --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/ConfigureTwoFactorViewModel.cs @@ -0,0 +1,20 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/Manage/FactorViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/FactorViewModel.cs new file mode 100644 index 00000000..fd0447f5 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/FactorViewModel.cs @@ -0,0 +1,12 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/Manage/IndexViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/IndexViewModel.cs new file mode 100644 index 00000000..51140afd --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/IndexViewModel.cs @@ -0,0 +1,35 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/Manage/ManageLoginsViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/ManageLoginsViewModel.cs new file mode 100644 index 00000000..38bfb56c --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/ManageLoginsViewModel.cs @@ -0,0 +1,21 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/Manage/RemoveLoginViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/RemoveLoginViewModel.cs new file mode 100644 index 00000000..171fb666 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/RemoveLoginViewModel.cs @@ -0,0 +1,17 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/Manage/SetPasswordViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/SetPasswordViewModel.cs new file mode 100644 index 00000000..80bbccd1 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/SetPasswordViewModel.cs @@ -0,0 +1,26 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/Manage/VerifyPhoneNumberViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/VerifyPhoneNumberViewModel.cs new file mode 100644 index 00000000..253b74d9 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/Manage/VerifyPhoneNumberViewModel.cs @@ -0,0 +1,23 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/Settings/SettingViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/Settings/SettingViewModel.cs new file mode 100644 index 00000000..f31ed4b1 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/Settings/SettingViewModel.cs @@ -0,0 +1,25 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/VIewModels/Settings/SettingsListViewModel.cs b/BlogWebApp/BlogMinimalApi/VIewModels/Settings/SettingsListViewModel.cs new file mode 100644 index 00000000..6515df02 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/VIewModels/Settings/SettingsListViewModel.cs @@ -0,0 +1,14 @@ +namespace BlogMinimalApi.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 3541028d45fe66bcec797a16e0ca577222f49e5e Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 4 Mar 2026 09:37:03 +0200 Subject: [PATCH 22/33] 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/BlogMinimalApi/Mappers/AspNetUser/VIewModelToEntityMappingRole.cs create mode 100644 BlogWebApp/BlogMinimalApi/Mappers/AspNetUser/ViewModelToEntityMappingUser.cs create mode 100644 BlogWebApp/BlogMinimalApi/Mappers/DomainToResponseProfile.cs create mode 100644 BlogWebApp/BlogMinimalApi/Mappers/GeneralRequestsToDomains.cs create mode 100644 BlogWebApp/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingComment.cs create mode 100644 BlogWebApp/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingPosts.cs create mode 100644 BlogWebApp/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingTag.cs create mode 100644 BlogWebApp/BlogMinimalApi/Mappers/RequestToDomainProfile.cs create mode 100644 BlogWebApp/BlogMinimalApi/Mappers/ViewModelToEntityMappingMessage.cs diff --git a/BlogWebApp/BlogMinimalApi/Mappers/AspNetUser/VIewModelToEntityMappingRole.cs b/BlogWebApp/BlogMinimalApi/Mappers/AspNetUser/VIewModelToEntityMappingRole.cs new file mode 100644 index 00000000..793e5aa5 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Mappers/AspNetUser/VIewModelToEntityMappingRole.cs @@ -0,0 +1,22 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/Mappers/AspNetUser/ViewModelToEntityMappingUser.cs b/BlogWebApp/BlogMinimalApi/Mappers/AspNetUser/ViewModelToEntityMappingUser.cs new file mode 100644 index 00000000..51d8a23e --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Mappers/AspNetUser/ViewModelToEntityMappingUser.cs @@ -0,0 +1,33 @@ +namespace BlogMinimalApi.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 BlogMinimalApi.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/BlogMinimalApi/Mappers/DomainToResponseProfile.cs b/BlogWebApp/BlogMinimalApi/Mappers/DomainToResponseProfile.cs new file mode 100644 index 00000000..34259bf0 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Mappers/DomainToResponseProfile.cs @@ -0,0 +1,20 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/Mappers/GeneralRequestsToDomains.cs b/BlogWebApp/BlogMinimalApi/Mappers/GeneralRequestsToDomains.cs new file mode 100644 index 00000000..80cd39a9 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Mappers/GeneralRequestsToDomains.cs @@ -0,0 +1,24 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingComment.cs b/BlogWebApp/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingComment.cs new file mode 100644 index 00000000..d5acea16 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingComment.cs @@ -0,0 +1,24 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingPosts.cs b/BlogWebApp/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingPosts.cs new file mode 100644 index 00000000..7f82b941 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingPosts.cs @@ -0,0 +1,27 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingTag.cs b/BlogWebApp/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingTag.cs new file mode 100644 index 00000000..f05b1b1f --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Mappers/Posts/ViewModelToEntityMappingTag.cs @@ -0,0 +1,26 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/Mappers/RequestToDomainProfile.cs b/BlogWebApp/BlogMinimalApi/Mappers/RequestToDomainProfile.cs new file mode 100644 index 00000000..cfd828a7 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Mappers/RequestToDomainProfile.cs @@ -0,0 +1,19 @@ +namespace BlogMinimalApi.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/BlogMinimalApi/Mappers/ViewModelToEntityMappingMessage.cs b/BlogWebApp/BlogMinimalApi/Mappers/ViewModelToEntityMappingMessage.cs new file mode 100644 index 00000000..860c2a01 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/Mappers/ViewModelToEntityMappingMessage.cs @@ -0,0 +1,23 @@ +namespace BlogMinimalApi.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 9ec7138b90af8b60df3c9fc9a2dd6dfb97e8af34 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 4 Mar 2026 10:36:19 +0200 Subject: [PATCH 23/33] Configure authentication --- .../ConfigureAuthentication.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureAuthentication.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureAuthentication.cs b/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureAuthentication.cs new file mode 100644 index 00000000..9e1fdbf8 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureAuthentication.cs @@ -0,0 +1,19 @@ +namespace BlogMinimalApi.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 2b56149486abafef6ebdf950d19cf9f00a02ea2d Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Wed, 4 Mar 2026 10:36:43 +0200 Subject: [PATCH 24/33] Add base configuration --- .../StartupConfigures/ConfigureBase.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureBase.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureBase.cs b/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureBase.cs new file mode 100644 index 00000000..9b2d30c3 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureBase.cs @@ -0,0 +1,54 @@ +namespace BlogMinimalApi.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 d4ed07be2b54961dbbb0f36d6d88fe2d3629aeda Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 5 Mar 2026 10:37:05 +0200 Subject: [PATCH 25/33] Configure routes --- .../StartupConfigures/ConfigureRoutes.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureRoutes.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureRoutes.cs b/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureRoutes.cs new file mode 100644 index 00000000..ad3d2cee --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureRoutes.cs @@ -0,0 +1,85 @@ +namespace BlogMinimalApi.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 f238b257a6f550c8617249eb6816b611c009d605 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 5 Mar 2026 10:37:26 +0200 Subject: [PATCH 26/33] Configure swagger --- .../StartupConfigures/ConfigureSwagger.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureSwagger.cs diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureSwagger.cs b/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureSwagger.cs new file mode 100644 index 00000000..8fd90251 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/StartupConfigures/ConfigureSwagger.cs @@ -0,0 +1,34 @@ +namespace BlogMinimalApi.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 b280967f27ef494353ff370eb16125b0b43a9dc0 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 5 Mar 2026 10:37:55 +0200 Subject: [PATCH 27/33] Configure Program file --- BlogWebApp/BlogMinimalApi/Program.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/BlogWebApp/BlogMinimalApi/Program.cs b/BlogWebApp/BlogMinimalApi/Program.cs index a4984275..11fc7e83 100644 --- a/BlogWebApp/BlogMinimalApi/Program.cs +++ b/BlogWebApp/BlogMinimalApi/Program.cs @@ -1,7 +1,22 @@ +using BlogMinimalApi.ApiEndpoints; +using BlogMinimalApi.StartupConfigures; +using BlogMinimalApi.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); + +//Api endpoints +app.InstallApiRoutesInAssembly(); + app.Run(); \ No newline at end of file From cd8b18de22fccc92441cc12581879ab7408540f0 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Thu, 5 Mar 2026 10:38:46 +0200 Subject: [PATCH 28/33] Set up Accounts Api endpoints --- .../ApiEndpoints/AccountsApiEndpoints.cs | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/ApiEndpoints/AccountsApiEndpoints.cs diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/AccountsApiEndpoints.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/AccountsApiEndpoints.cs new file mode 100644 index 00000000..6f36859f --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/AccountsApiEndpoints.cs @@ -0,0 +1,187 @@ +using Asp.Versioning; +using AutoMapper; +using Blog.CommonServices.EmailServices.Interfaces; +using Blog.Contracts.V1; +using Blog.Contracts.V1.Requests.UsersRequests; +using Blog.Contracts.V1.Responses.Chart; +using Blog.Contracts.V1.Responses.UsersResponses; +using Blog.Data.Models; +using Blog.EntityServices.EntityFrameworkServices.Identity.Auth; +using Blog.EntityServices.EntityFrameworkServices.Identity.Registration; +using Blog.EntityServices.EntityFrameworkServices.Identity.User; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace BlogMinimalApi.ApiEndpoints; + +/// +/// Accounts Api endpoints. +/// +public class AccountsApiEndpoints : IRoutesInstaller +{ + public void InstallApiRoutes(WebApplication 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"); + + // ------------------------- + // PUBLIC ENDPOINTS + // ------------------------- + + var publicGroup = group.AllowAnonymous(); + + // Initialize (roles list) + publicGroup.MapGet(ApiRoutes.AccountsController.Initialize, + async (string userId, + RoleManager roleManager, + IMapper mapper) => + { + if (string.IsNullOrEmpty(userId)) + return Results.BadRequest("Something went wrong"); + + var roles = await roleManager.Roles.ToListAsync(); + var response = mapper.Map>(roles); + + return Results.Ok(response); + }) + .Produces>() + .Produces(400); + + + // Get All Users + publicGroup.MapGet(ApiRoutes.AccountsController.GetAllUsers, + async (UserManager userManager, + IMapper mapper) => + { + var users = await userManager.Users.Include(u => u.Roles).ToListAsync(); + var response = mapper.Map>(users); + + return Results.Ok(response); + }) + .Produces>(); + + + // Login + publicGroup.MapPost(ApiRoutes.AccountsController.Login, + async (LoginRequest credentials, + IAuthService authService) => + { + if (credentials is null) + return Results.BadRequest(); + + var jwt = await authService.GetJwtAsync(credentials.Email, credentials.Password); + + return jwt is null + ? Results.BadRequest("Invalid name or password") + : Results.Ok(jwt); + }) + .Produces() + .Produces(400); + + + // Register + publicGroup.MapPost(ApiRoutes.AccountsController.Register, + async (RegistrationRequest model, + IMapper mapper, + IRegistrationService registrationService, + UserManager userManager) => + { + if (model is null) + return Results.BadRequest(); + + model.UserName = model.Email; + + 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 Results.NoContent(); + }) + .Produces(204) + .Produces(400); + + // ------------------------- + // PRIVATE ENDPOINTS + // ------------------------- + + var privateGroup = group.RequireAuthorization(); + + // Send confirmation email + privateGroup.MapGet(ApiRoutes.AccountsController.SendConfirmationEmail, + async (IUserService userService, + IEmailExtensionService emailService, + HttpContext context) => + { + var email = context.User.Identity?.Name; + + if (string.IsNullOrEmpty(email)) + return Results.BadRequest(); + + var token = await userService.GetEmailVerificationTokenAsync(email); + await emailService.SendVerificationEmailAsync(email, token); + + return Results.NoContent(); + }) + .Produces(204); + + // Users activity + privateGroup.MapGet(ApiRoutes.AccountsController.UsersActivity, + async (IUserService userService) => + { + var activity = await userService.GetUsersActivity(); + + return activity is null + ? Results.NotFound() + : Results.Ok(activity); + }) + .Produces() + .Produces(404); + + // Change password + privateGroup.MapPut(ApiRoutes.AccountsController.ChangePassword, + async (ChangePasswordRequest model, + IUserService userService, + HttpContext context) => + { + if (model is null) + return Results.BadRequest(); + + var username = context.User.Identity?.Name; + + if (string.IsNullOrEmpty(username)) + return Results.BadRequest(); + + var result = await userService.ChangePasswordAsync( + username, + model.OldPassword, + model.NewPassword); + + return result.Succeeded + ? Results.NoContent() + : Results.BadRequest(result.Errors); + }) + .Produces(204) + .Produces(400); + } +} \ No newline at end of file From c0ab7f9a9b75af775f70f185c07743ddff160254 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 6 Mar 2026 10:39:23 +0200 Subject: [PATCH 29/33] Set up Comments api endpoints --- .../ApiEndpoints/CommentsApiEndpoints.cs | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/ApiEndpoints/CommentsApiEndpoints.cs diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/CommentsApiEndpoints.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/CommentsApiEndpoints.cs new file mode 100644 index 00000000..f4c88d5c --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/CommentsApiEndpoints.cs @@ -0,0 +1,198 @@ +using Asp.Versioning; +using AutoMapper; +using Blog.Contracts.V1; +using Blog.Contracts.V1.Requests; +using Blog.Contracts.V1.Requests.CommentsRequests; +using Blog.Contracts.V1.Responses; +using Blog.Contracts.V1.Responses.Chart; +using Blog.Contracts.V1.Responses.CommentsResponses; +using Blog.Data.Models; +using Blog.EntityServices.Interfaces; +using Blog.Services.Core.Dtos; + +namespace BlogMinimalApi.ApiEndpoints; + +/// +/// Comments Api endpoints. +/// +public class CommentsApiEndpoints : IRoutesInstaller +{ + public void InstallApiRoutes(WebApplication 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"); + + // ------------------------- + // PUBLIC + // ------------------------- + + var publicGroup = group.AllowAnonymous(); + + // Get All Comments + publicGroup.MapGet("/", + async (ICommentsService service, IMapper mapper) => + { + var comments = await service.GetAllAsync(); + var response = mapper.Map>(comments); + + return Results.Ok(response); + }) + .Produces>(); + + + // Comments Activity + publicGroup.MapGet(ApiRoutes.CommentsController.CommentsActivity, + async (ICommentsService service) => + { + var activity = await service.GetCommentsActivity(); + + return activity is null + ? Results.NotFound() + : Results.Ok(activity); + }) + .Produces() + .Produces(404); + + + // Get Paged Comments + publicGroup.MapPost(ApiRoutes.CommentsController.GetCommentsByFilter, + async (SortParametersRequest? sortParameters, + ICommentsService service, + IMapper mapper) => + { + sortParameters ??= new SortParametersRequest(); + sortParameters.CurrentPage ??= 1; + sortParameters.PageSize = 10; + + var result = await service.GetPagedComments( + mapper.Map(sortParameters)); + + return result is null + ? Results.NotFound() + : Results.Ok(mapper.Map(result)); + }) + .Produces() + .Produces(404); + + + // Get Comments By Post + publicGroup.MapPost(ApiRoutes.CommentsController.GetCommentsByPost, + async (int id, + SortParametersRequest? sortParameters, + ICommentsService service, + IMapper mapper) => + { + sortParameters ??= new SortParametersRequest(); + sortParameters.CurrentPage ??= 1; + sortParameters.PageSize = 10; + + var result = await service.GetPagedCommentsByPostId( + id, + mapper.Map(sortParameters)); + + return result is null + ? Results.NotFound() + : Results.Ok(mapper.Map(result)); + }) + .Produces() + .Produces(404); + + + // Get Comment By ID + publicGroup.MapGet("/{id:int}", + async (int id, + ICommentsService service, + IMapper mapper) => + { + var comment = await service.GetCommentAsync(id); + + return comment is null + ? Results.NotFound() + : Results.Ok(mapper.Map(comment)); + }) + .WithName(ApiRoutes.CommentsController.GetComment) + .Produces() + .Produces(404); + + // ------------------------- + // PRIVATE (Authorized) + // ------------------------- + + var privateGroup = group.RequireAuthorization(); + + // Create Comment + privateGroup.MapPost(ApiRoutes.CommentsController.CreateComment, + async (CreateCommentRequest request, + ICommentsService service, + IMapper mapper, + HttpContext context) => + { + var comment = mapper.Map(request); + comment.CreatedAt = DateTime.UtcNow; + + await service.InsertAsync(comment); + + var location = + $"{context.Request.Scheme}://{context.Request.Host}" + + $"{ApiRoutes.CommentsController.Comments}/{comment.Id}"; + + return Results.Created(location, new CreatedResponse + { + Id = comment.Id + }); + }) + .Produces>(201) + .Produces(400); + + + // Edit Comment + privateGroup.MapPut(ApiRoutes.CommentsController.EditComment, + async (int id, + UpdateCommentRequest request, + ICommentsService service, + IMapper mapper) => + { + var origin = await service.GetCommentAsync(id); + + if (origin is null || !origin.UserId.Equals(request.UserId)) + return Results.NotFound(); + + request.CreatedAt = DateTime.UtcNow; + + var updated = mapper.Map(request, origin); + + await service.UpdateAsync(updated); + + var refreshed = await service.GetCommentAsync(id); + + return Results.Ok(mapper.Map(refreshed)); + }) + .Produces() + .Produces(404); + + + // Delete Comment + privateGroup.MapDelete(ApiRoutes.CommentsController.DeleteComment, + async (int id, + ICommentsService service) => + { + var comment = await service.GetCommentAsync(id); + + if (comment is null) + return Results.NotFound(); + + await service.DeleteAsync(comment); + + return Results.Ok(new CreatedResponse { Id = id }); + }) + .Produces>() + .Produces(404); + } +} \ No newline at end of file From 492812515895aaef4e52a6102b0a6b7567788e73 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 6 Mar 2026 10:40:00 +0200 Subject: [PATCH 30/33] Set up Health Check Api endpoints --- .../ApiEndpoints/HealthCheckApiEndpoints.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/ApiEndpoints/HealthCheckApiEndpoints.cs diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/HealthCheckApiEndpoints.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/HealthCheckApiEndpoints.cs new file mode 100644 index 00000000..7da6b835 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/HealthCheckApiEndpoints.cs @@ -0,0 +1,56 @@ +using Asp.Versioning; +using Blog.Contracts.V1; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace BlogMinimalApi.ApiEndpoints; + +/// +/// Accounts Api endpoints. +/// +public class HealthCheckApiEndpoints : IRoutesInstaller +{ + public void InstallApiRoutes(WebApplication app) + { + var versionSet = app.NewApiVersionSet() + .HasApiVersion(new ApiVersion(1, 0)) + .ReportApiVersions() + .Build(); + + var group = app.MapGroup(ApiRoutes.HealthController.Health) + .WithApiVersionSet(versionSet) + .MapToApiVersion(1.0) + .WithTags("Accounts"); + + // ------------------------- + // PUBLIC ENDPOINTS + // ------------------------- + + var publicGroup = group.AllowAnonymous(); + + // Initialize (roles list) + publicGroup.MapGet("/health", async (HealthCheckService healthCheckService) => + { + var report = await healthCheckService.CheckHealthAsync(); + + var response = new + { + Status = report.Status.ToString(), + Checks = report.Entries.Select(x => new + { + Component = x.Key, + Status = x.Value.Status.ToString(), + Description = x.Value.Description + }), + Duration = report.TotalDuration + }; + + return report.Status == HealthStatus.Healthy + ? Results.Ok(response) + : Results.Problem( + detail: "Service is unhealthy", + statusCode: StatusCodes.Status503ServiceUnavailable); + }) + .WithName("Health") + .AllowAnonymous(); + } +} \ No newline at end of file From ff9478224f73d5351ad3e91fdad55115afbaa496 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 6 Mar 2026 10:40:57 +0200 Subject: [PATCH 31/33] Set up Messages Api endpoints --- .../ApiEndpoints/MessagesApiEndpoints.cs | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/ApiEndpoints/MessagesApiEndpoints.cs diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/MessagesApiEndpoints.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/MessagesApiEndpoints.cs new file mode 100644 index 00000000..f85d66b4 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/MessagesApiEndpoints.cs @@ -0,0 +1,204 @@ +using Asp.Versioning; +using AutoMapper; +using Blog.Contracts.V1; +using Blog.Contracts.V1.Requests.MessagesRequests; +using Blog.Contracts.V1.Responses; +using Blog.Data.Models; +using Blog.Data.Specifications; +using Blog.EntityServices.Interfaces; + +namespace BlogMinimalApi.ApiEndpoints; + +/// +/// Messages Api endpoints. +/// +public class MessagesApiEndpoints : IRoutesInstaller +{ + public void InstallApiRoutes(WebApplication 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"); + + // ------------------------- + // PUBLIC ENDPOINTS + // ------------------------- + + var publicGroup = group.AllowAnonymous(); + + // GET ALL + publicGroup.MapGet("", + async (IMessagesService messagesService, + IMapper mapper) => + { + var messages = await messagesService.GetAllAsync(); + + if (messages is null || messages.Count == 0) + return Results.NotFound(); + + var response = mapper.Map>(messages); + + return Results.Ok(response); + }) + .Produces>() + .Produces(404); + + + // GET BY RECIPIENT + publicGroup.MapGet(ApiRoutes.MessagesController.GetRecipientMessages, + async (string recipientId, + IMessagesService messagesService, + IMapper mapper) => + { + var messages = await messagesService.GetAllAsync( + new MessageSpecification(x => + x.RecipientId.Equals(recipientId, StringComparison.CurrentCultureIgnoreCase))); + + if (messages is null || messages.Count == 0) + return Results.NotFound(); + + return Results.Ok(mapper.Map>(messages)); + }) + .Produces>() + .Produces(404); + + + // GET BY SENDER + publicGroup.MapGet(ApiRoutes.MessagesController.GetSenderMessages, + async (string senderEmail, + IMessagesService messagesService, + IMapper mapper) => + { + var messages = await messagesService.GetAllAsync( + new MessageSpecification(x => + x.SenderEmail.Equals(senderEmail, StringComparison.CurrentCultureIgnoreCase))); + + if (messages is null || messages.Count == 0) + return Results.NotFound(); + + return Results.Ok(mapper.Map>(messages)); + }) + .Produces>() + .Produces(404); + + + // GET BY ID + publicGroup.MapGet(ApiRoutes.MessagesController.Show, + async (int id, + IMessagesService messagesService, + IMapper mapper) => + { + var message = await messagesService.FindAsync(id); + + return message is null + ? Results.NotFound() + : Results.Ok(mapper.Map(message)); + }) + .Produces() + .Produces(404); + + + // ------------------------- + // PRIVATE ENDPOINTS + // ------------------------- + + var privateGroup = group.RequireAuthorization(); + + // CREATE + privateGroup.MapPost("", + async (CreateMessageRequest request, + IMessagesService messagesService, + IMapper mapper, + HttpContext context) => + { + var userId = context.User.FindFirst("sub")?.Value; + + if (string.IsNullOrEmpty(userId)) + return Results.BadRequest("Unauthorized"); + + request.SenderId = userId; + + var message = mapper.Map(request); + + await messagesService.InsertAsync(message); + + return Results.Created($"/messages/{message.Id}", null); + }) + .Produces(201) + .Produces(400); + + + // UPDATE + privateGroup.MapPut("{id:int}", + async (int id, + UpdateMessageRequest request, + IMessagesService messagesService, + IMapper mapper, + HttpContext context) => + { + var userId = context.User.FindFirst("sub")?.Value; + + if (string.IsNullOrEmpty(userId)) + return Results.BadRequest("Unauthorized"); + + var message = await messagesService.FindAsync(id); + + if (message is null) + return Results.NotFound(); + + if (message.SenderId != userId && + message.RecipientId != userId) + { + return Results.BadRequest( + new { ErrorMessage = "You are not author or recipient." }); + } + + mapper.Map(request, message); + + await messagesService.UpdateAsync(message); + + return Results.NoContent(); + }) + .Produces(204) + .Produces(400) + .Produces(404); + + + // DELETE + privateGroup.MapDelete("{id:int}", + async (int id, + IMessagesService messagesService, + HttpContext context) => + { + var userId = context.User.FindFirst("sub")?.Value; + + if (string.IsNullOrEmpty(userId)) + return Results.BadRequest("Unauthorized"); + + var message = await messagesService.FindAsync(id); + + if (message is null) + return Results.NotFound(); + + if (message.SenderId != userId && + message.RecipientId != userId) + { + return Results.BadRequest( + new { ErrorMessage = "You are not allowed." }); + } + + await messagesService.DeleteAsync(message); + + return Results.Ok(new CreatedResponse { Id = id }); + }) + .Produces>() + .Produces(404) + .Produces(400); + } +} \ No newline at end of file From aab857505a8962ffaf7d152ffeae8fed4f4926d7 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Fri, 6 Mar 2026 10:41:36 +0200 Subject: [PATCH 32/33] Set up Posts Api endpoints --- .../ApiEndpoints/PostsApiEndpoints.cs | 305 ++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/ApiEndpoints/PostsApiEndpoints.cs diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/PostsApiEndpoints.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/PostsApiEndpoints.cs new file mode 100644 index 00000000..4d0533fe --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/PostsApiEndpoints.cs @@ -0,0 +1,305 @@ +using Asp.Versioning; +using AutoMapper; +using Blog.Contracts.V1; +using Blog.Contracts.V1.Requests.PostsRequests; +using Blog.Contracts.V1.Responses; +using Blog.Contracts.V1.Responses.Chart; +using Blog.Contracts.V1.Responses.PostsResponses; +using Blog.Contracts.V1.Responses.UsersResponses; +using Blog.Data.Models; +using Blog.EntityServices.Interfaces; +using Blog.Services.Core.Dtos; +using Blog.Services.Core.Dtos.Posts; + +namespace BlogMinimalApi.ApiEndpoints; + +/// +/// Posts Api endpoints. +/// +public class PostsApiEndpoints : IRoutesInstaller +{ + public void InstallApiRoutes(WebApplication 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"); + + // ------------------------- + // PUBLIC ENDPOINTS + // ------------------------- + + var publicGroup = group.AllowAnonymous(); + + // GET ALL POSTS + publicGroup.MapGet("", + async (IPostsService postsService, + IMapper mapper) => + { + var posts = await postsService.GetAllAsync(); + + if (posts is null || !posts.Any()) + return Results.NotFound(); + + return Results.Ok( + mapper.Map>(posts)); + }) + .Produces>() + .Produces(404); + + + // POSTS ACTIVITY + publicGroup.MapGet(ApiRoutes.PostsController.PostsActivity, + async (IPostsService postsService) => + { + var activity = await postsService.GetPostsActivity(); + + return activity is null + ? Results.NotFound() + : Results.Ok(activity); + }) + .Produces() + .Produces(404); + + + // FILTERED POSTS + publicGroup.MapPost(ApiRoutes.PostsController.GetPosts, + async (PostsSearchParametersRequest request, + IPostsService postsService, + IMapper mapper) => + { + request.SortParameters ??= new(); + request.SortParameters.OrderBy ??= "asc"; + request.SortParameters.SortBy ??= "Title"; + request.SortParameters.CurrentPage ??= 1; + request.SortParameters.PageSize ??= 10; + + var posts = await postsService.GetPostsAsync( + mapper.Map(request)); + + return posts is null + ? Results.NotFound() + : Results.Ok(mapper.Map(posts)); + }) + .Produces() + .Produces(404); + + + // USER POSTS + publicGroup.MapPost(ApiRoutes.PostsController.UserPosts, + async (string id, + PostsSearchParametersRequest request, + IPostsService postsService, + IMapper mapper) => + { + request.SortParameters ??= new(); + request.SortParameters.OrderBy ??= "asc"; + request.SortParameters.SortBy ??= "Title"; + request.SortParameters.CurrentPage ??= 1; + request.SortParameters.PageSize = 10; + + var posts = await postsService.GetUserPostsAsync( + id, + mapper.Map(request)); + + return posts is null + ? Results.NotFound() + : Results.Ok(mapper.Map(posts)); + }) + .Produces() + .Produces(404); + + + // GET POST BY ID + publicGroup.MapGet(ApiRoutes.PostsController.Show, + async (int id, + IPostsService postsService, + IMapper mapper) => + { + var sort = new SortParametersDto + { + CurrentPage = 1, + PageSize = 10 + }; + + var post = await postsService.GetPost(id, sort); + + return post is null + ? Results.NotFound() + : Results.Ok( + mapper.Map(post)); + }) + .Produces() + .Produces(404); + + + // ------------------------- + // PRIVATE ENDPOINTS + // ------------------------- + + var privateGroup = group.RequireAuthorization(); + + // CREATE POST + privateGroup.MapPost("", + async (CreatePostRequest model, + IPostsService postsService, + IMapper mapper, + HttpContext context) => + { + var userId = context.User.FindFirst("sub")?.Value; + + if (string.IsNullOrEmpty(userId)) + return Results.BadRequest("Unauthorized"); + + model.AuthorId = userId; + + var post = mapper.Map(model); + var tags = mapper.Map>(model.Tags.Distinct()); + + await postsService.InsertAsync(post, tags); + + return Results.Created( + $"{ApiRoutes.PostsController.Posts}/{post.Id}", + new CreatedResponse { Id = post.Id }); + }) + .Produces>(201) + .Produces(400); + + + // LIKE POST + privateGroup.MapPut(ApiRoutes.PostsController.LikePost, + async (int id, + IPostsService postsService, + IMapper mapper, + HttpContext context) => + { + var userId = context.User.FindFirst("sub")?.Value; + if (string.IsNullOrEmpty(userId)) + return Results.BadRequest("Unauthorized"); + + var post = await postsService.GetPostAsync(id); + if (post is null) + return Results.NotFound(); + + post.Likes++; + await postsService.UpdateAsync(post); + + var updated = await postsService.GetPostAsync(id); + + var response = mapper.Map(updated); + response.Author = + mapper.Map(updated.Author); + + return Results.Ok(response); + }) + .Produces() + .Produces(404) + .Produces(400); + + + // DISLIKE POST + privateGroup.MapPut(ApiRoutes.PostsController.DislikePost, + async (int id, + IPostsService postsService, + IMapper mapper, + HttpContext context) => + { + var userId = context.User.FindFirst("sub")?.Value; + if (string.IsNullOrEmpty(userId)) + return Results.BadRequest("Unauthorized"); + + var post = await postsService.GetPostAsync(id); + if (post is null) + return Results.NotFound(); + + post.Dislikes++; + await postsService.UpdateAsync(post); + + var updated = await postsService.GetPostAsync(id); + + var response = mapper.Map(updated); + response.Author = + mapper.Map(updated.Author); + + return Results.Ok(response); + }) + .Produces() + .Produces(404) + .Produces(400); + + + // EDIT POST + privateGroup.MapPut("{id:int}", + async (int id, + UpdatePostRequest model, + IPostsService postsService, + IPostsTagsRelationsService tagsService, + IMapper mapper, + HttpContext context) => + { + var userId = context.User.FindFirst("sub")?.Value; + if (string.IsNullOrEmpty(userId)) + return Results.BadRequest("Unauthorized"); + + var post = await postsService.GetPostAsync(id); + if (post is null) + return Results.NotFound(); + + if (post.AuthorId != userId) + return Results.BadRequest( + new { ErrorMessage = "You are not the author." }); + + mapper.Map(model, post); + + await postsService.UpdateAsync(post); + + var tags = mapper.Map>(model.Tags); + + await tagsService.AddTagsToExistingPost( + post.Id, + post.PostsTagsRelations.ToList(), + tags); + + var updated = await postsService.GetPostAsync(id); + + return Results.Ok( + mapper.Map(updated)); + }) + .Produces() + .Produces(404) + .Produces(400); + + + // DELETE POST + privateGroup.MapDelete("{id:int}", + async (int id, + IPostsService postsService, + HttpContext context) => + { + var userId = context.User.FindFirst("sub")?.Value; + if (string.IsNullOrEmpty(userId)) + return Results.BadRequest("Unauthorized"); + + var post = await postsService.GetPostAsync(id); + if (post is null) + return Results.NotFound(); + + if (post.AuthorId != userId) + return Results.BadRequest( + new { ErrorMessage = "You are not the author." }); + + await postsService.DeleteAsync(post); + + return Results.Ok( + new CreatedResponse { Id = id }); + }) + .Produces>() + .Produces(404) + .Produces(400); + } +} \ No newline at end of file From 4ab07b74bf2d2562cb13626c1ef925af6dbb0806 Mon Sep 17 00:00:00 2001 From: Vitalii Lakatosh Date: Sat, 7 Mar 2026 10:41:58 +0200 Subject: [PATCH 33/33] Configure Routes --- .../ApiEndpoints/IRoutesInstaller.cs | 13 ++++++++++++ .../ApiEndpoints/RouterInstallerExtensions.cs | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 BlogWebApp/BlogMinimalApi/ApiEndpoints/IRoutesInstaller.cs create mode 100644 BlogWebApp/BlogMinimalApi/ApiEndpoints/RouterInstallerExtensions.cs diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/IRoutesInstaller.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/IRoutesInstaller.cs new file mode 100644 index 00000000..2ddfa339 --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/IRoutesInstaller.cs @@ -0,0 +1,13 @@ +namespace BlogMinimalApi.ApiEndpoints; + +/// +/// Installer interface. +/// +public interface IRoutesInstaller +{ + /// + /// Installs the Api routes. + /// + /// The app. + void InstallApiRoutes(WebApplication app); +} \ No newline at end of file diff --git a/BlogWebApp/BlogMinimalApi/ApiEndpoints/RouterInstallerExtensions.cs b/BlogWebApp/BlogMinimalApi/ApiEndpoints/RouterInstallerExtensions.cs new file mode 100644 index 00000000..a36c066c --- /dev/null +++ b/BlogWebApp/BlogMinimalApi/ApiEndpoints/RouterInstallerExtensions.cs @@ -0,0 +1,21 @@ +namespace BlogMinimalApi.ApiEndpoints; + +/// +/// Installer extensions. +/// +public static class RouterInstallerExtensions +{ + /// + /// Installs the Api routes in assembly. + /// + public static void InstallApiRoutesInAssembly(this WebApplication app) + { + var installers = typeof(Program).Assembly.ExportedTypes.Where(x => + typeof(IRoutesInstaller).IsAssignableFrom(x) + && x is { IsInterface: false, IsAbstract: false }) + .Select(Activator.CreateInstance) + .Cast().ToList(); + + installers.ForEach(installer => installer.InstallApiRoutes(app)); + } +} \ No newline at end of file