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/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
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
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
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/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
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
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
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/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
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
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
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
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
diff --git a/BlogWebApp/BlogMinimalApi/Program.cs b/BlogWebApp/BlogMinimalApi/Program.cs
new file mode 100644
index 00000000..11fc7e83
--- /dev/null
+++ b/BlogWebApp/BlogMinimalApi/Program.cs
@@ -0,0 +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
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/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
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
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
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
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
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
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
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
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
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
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/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
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
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
diff --git a/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs
new file mode 100644
index 00000000..80cb3c42
--- /dev/null
+++ b/BlogWebApp/BlogMinimalApi/StartupConfigureServicesInstallers/SwaggerInstaller.cs
@@ -0,0 +1,105 @@
+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();
+ });
+
+ services.AddSwaggerExamplesFromAssemblyOf();
+ }
+ }
+}
\ No newline at end of file
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
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
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
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
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
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
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"
+ }
+}