Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
09cc4a0
Add better documentation to Accounts Api Endpoints
lakatoshv Mar 12, 2026
bf51d0c
Add better documentation to Comments Api Endpoints
lakatoshv Mar 12, 2026
d22a5e9
Add Health check Api Endpoints
lakatoshv Mar 12, 2026
9b505fc
Add better documentation to Messages Api Endpoints
lakatoshv Mar 12, 2026
9af35e6
Add better documentation to Posts Api Endpoints
lakatoshv Mar 13, 2026
2fdb91d
Created Blog vertical slice CQRS Minimal Api project
lakatoshv Mar 13, 2026
290e6e6
Add Cache setups
lakatoshv Mar 13, 2026
cc503d8
Add Swagger configuration
lakatoshv Mar 13, 2026
52ac242
Add validation filter
lakatoshv Mar 14, 2026
c006ff2
Add Swagger extentions
lakatoshv Mar 14, 2026
dd22890
Add view models
lakatoshv Mar 14, 2026
a1d4761
Add health check configuration
lakatoshv Mar 14, 2026
0c0f94c
Add mappers
lakatoshv Mar 15, 2026
a20a796
Add Api versioning installer
lakatoshv Mar 15, 2026
d9e854f
Add services Installer extentions
lakatoshv Mar 15, 2026
003d94a
Add base services installers
lakatoshv Mar 15, 2026
752728d
Add services installer
lakatoshv Mar 16, 2026
0dc68ec
Add authentication and authorization services installer
lakatoshv Mar 16, 2026
11ff8b4
Add CORS installer
lakatoshv Mar 16, 2026
c671c4d
Add Data repositories installers
lakatoshv Mar 16, 2026
846a3a2
Add Database installer
lakatoshv Mar 17, 2026
6a67180
Add Email Installer
lakatoshv Mar 17, 2026
9e4c5f6
Configure autentication and authorization
lakatoshv Mar 18, 2026
4f65255
Configure validations
lakatoshv Mar 18, 2026
f969c42
Add base configurations
lakatoshv Mar 19, 2026
87c4142
Configure routes
lakatoshv Mar 19, 2026
e822033
Configure base command and command handler interfaces
lakatoshv Mar 19, 2026
9e15553
Configure base query and query handler interfaces
lakatoshv Mar 19, 2026
488173b
Add get all users query and query handler in accounts feature
lakatoshv Mar 20, 2026
7e01c7a
Add initialize query and query handler in accounts feature
lakatoshv Mar 20, 2026
ba198a2
Add send confirmation email query and query handler in accounts feature
lakatoshv Mar 20, 2026
57f51e5
Add users activity query and query handler in accounts feature
lakatoshv Mar 20, 2026
c2ba491
Add login command and command handler in accounts feature
lakatoshv Mar 21, 2026
ea2a12c
Add login endpoint in accounts feature
lakatoshv Mar 21, 2026
490a7e2
Add register command and command handler in accounts feature
lakatoshv Mar 21, 2026
ef530da
Add register endpoint in accounts feature
lakatoshv Mar 21, 2026
456c72c
Add comments activity query and query handler in accounts feature
lakatoshv Mar 22, 2026
c6237a8
Add create comment command and command handler in comments feature
lakatoshv Mar 22, 2026
c08837c
Add create comment endpoint in comments feature
lakatoshv Mar 22, 2026
18494f0
Add get all query and query handler in comments feature
lakatoshv Mar 22, 2026
579a841
Add get comment by id query and query handler in comments feature
lakatoshv Mar 23, 2026
ca5595d
Add get comments by filter query and query handler in comments feature
lakatoshv Mar 23, 2026
ade15ec
Add get comments by post query and query handler in comments feature
lakatoshv Mar 23, 2026
819b5c5
Add initialize query and query handler in health check feature
lakatoshv Mar 23, 2026
4469e2a
Add create message command and command handler in messages feature
lakatoshv Mar 24, 2026
243d392
Add create message endpoint in messages feature
lakatoshv Mar 24, 2026
189290f
Add get all query and query handler in messages feature
lakatoshv Mar 24, 2026
996966d
Add get by id query and query handler in messages feature
lakatoshv Mar 24, 2026
cf4240c
Add get recipient messages query and query handler in messages feature
lakatoshv Mar 25, 2026
06827d3
Add get sender messages query and query handler in messages feature
lakatoshv Mar 25, 2026
0833732
Add create post command and command handler in posts feature
lakatoshv Mar 25, 2026
90d79bb
Add create post endpoint posts feature
lakatoshv Mar 25, 2026
b3b95dd
Add Carter, Mapster and MediatR configurations for endpoints
lakatoshv Mar 25, 2026
2a9d4e9
Cofigure program
lakatoshv Mar 26, 2026
3cfcab9
Add get all posts query and query handler in posts feature
lakatoshv Mar 26, 2026
fbe573f
Add get post by id query and query handler in posts feature
lakatoshv Mar 26, 2026
be0f086
Add get posts query and query handler in posts feature
lakatoshv Mar 26, 2026
d7871b8
Add posts activity query and query handler in posts feature
lakatoshv Mar 27, 2026
977e837
Add users posts query and query handler in posts feature
lakatoshv Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,5 @@
/BlogWebApp/Aspire/Blog.Aspire/Blog.Aspire.AppHost/bin/Debug/net10.0
/BlogWebApp/BlogMinimalApi/bin/Debug/net10.0
/BlogWebApp/BlogMinimalApi/obj
/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/bin/Debug/net10.0
/BlogWebApp/BlogVerticalSliceCQRSMinimalApi/obj
14 changes: 11 additions & 3 deletions BlogWebApp/BlogMinimalApi/ApiEndpoints/AccountsApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,12 @@ public void InstallApiRoutes(WebApplication app)
? Results.BadRequest("Invalid name or password")
: Results.Ok(jwt);
})

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test comment

.Produces<string>()
.Produces<string>(400);
.WithName("Login")
.Produces(200)
.Produces<string>(400)
.WithSummary("Login")
.WithDescription("Login")
.HasApiVersion(1.0);


// Register
Expand Down Expand Up @@ -118,8 +122,12 @@ public void InstallApiRoutes(WebApplication app)

return Results.NoContent();
})
.WithName("Register")
.Produces(204)
.Produces<IdentityResult>(400);
.Produces<IdentityResult>(400)
.WithSummary("Register")
.WithDescription("Register")
.HasApiVersion(1.0);

// -------------------------
// PRIVATE ENDPOINTS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,12 @@ public void InstallApiRoutes(WebApplication app)
Id = comment.Id
});
})
.WithName("CreateComment")
.Produces<CreatedResponse<int>>(201)
.Produces(400);
.Produces(400)
.WithSummary("Create comment")
.WithDescription("Create comment")
.HasApiVersion(1.0);


// Edit Comment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void InstallApiRoutes(WebApplication app)
publicGroup.MapGet("/health", async (HealthCheckService healthCheckService) =>
{
var report = await healthCheckService.CheckHealthAsync();

var response = new
{
Status = report.Status.ToString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,12 @@ public void InstallApiRoutes(WebApplication app)

return Results.Created($"/messages/{message.Id}", null);
})
.Produces(201)
.Produces(400);
.WithName("CreateMessage")
.Produces<CreatedResponse<int>>(201)
.Produces(400)
.WithSummary("Create message")
.WithDescription("Create message")
.HasApiVersion(1.0);


// UPDATE
Expand Down
6 changes: 5 additions & 1 deletion BlogWebApp/BlogMinimalApi/ApiEndpoints/PostsApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,12 @@ public void InstallApiRoutes(WebApplication app)
$"{ApiRoutes.PostsController.Posts}/{post.Id}",
new CreatedResponse<int> { Id = post.Id });
})
.WithName("CreatePost")
.Produces<CreatedResponse<int>>(201)
.Produces(400);
.Produces(400)
.WithSummary("Create post")
.WithDescription("Create post")
.HasApiVersion(1.0);


// LIKE POST
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Carter" Version="10.0.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="MediatR" Version="14.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3" />
<PackageReference Include="Asp.Versioning.Http" Version="8.1.1" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.1" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.4" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Aspire\Blog.Aspire\Blog.Aspire.ServiceDefaults\Blog.Aspire.ServiceDefaults.csproj" />
<ProjectReference Include="..\Services\Blog.Services\Blog.EntityServices.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>http</ActiveDebugProfile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@BlogVerticalSliceCQRSMinimalApi_HostAddress = http://localhost:5037

GET {{BlogVerticalSliceCQRSMinimalApi_HostAddress}}/weatherforecast/
Accept: application/json

###
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using MediatR;

namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;

/// <summary>
///
/// </summary>
//will not return any response
public interface ICommand : ICommand<Unit>;

/// <summary>
///
/// </summary>
/// <typeparam name="TResponse"></typeparam>
//will return TResponse
public interface ICommand<out TResponse> : IRequest<TResponse>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using MediatR;

namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;

//will not return any response
public interface ICommandHandler<in TCommand>
: IRequestHandler<TCommand, Unit>
where TCommand : ICommand<Unit>;

//will return TResponse
public interface ICommandHandler<in TCommand, TResponse>
: IRequestHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
where TResponse : notnull;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using MediatR;

namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;

public interface IQuery<out TResponse> : IRequest<TResponse>
where TResponse : notnull;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using MediatR;

namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;

//will return TResponse
public interface IQueryHandler<in TQuery, TResponse>
: IRequestHandler<TQuery, TResponse>
where TQuery : IQuery<TResponse>
where TResponse : notnull;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;

public record SearchParametersQuery(string OrderBy, string SortBy, int? CurrentPage, int? PageSize, string DisplayType)
: SortParametersQuery(OrderBy, SortBy, CurrentPage, PageSize, DisplayType);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;

public record SortParametersQuery(string OrderBy, string SortBy, int? CurrentPage, int? PageSize, string DisplayType);
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
namespace BlogVerticalSliceCQRSMinimalApi.Cache;

using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Blog.CommonServices.Interfaces;
using Blog.Core.Configuration;

/// <summary>
/// Cached attribute.
/// </summary>
/// <seealso cref="Attribute" />
/// <seealso cref="IAsyncActionFilter" />
/// <remarks>
/// Initializes a new instance of the <see cref="CachedAttribute"/> class.
/// </remarks>
/// <param name="lifeTimeSeconds">The lifetime seconds.</param>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CachedAttribute(int lifeTimeSeconds)
: Attribute, IAsyncActionFilter
{
/// <summary>
/// The lifetime seconds.
/// </summary>
private readonly int _lifeTimeSeconds = lifeTimeSeconds;

/// <summary>
/// Called asynchronously before the action, after model binding is complete.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext" />.</param>
/// <param name="next">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutionDelegate" />. Invoked to execute the next action filter or the action itself.</param>
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var cacheSettings = context.HttpContext.RequestServices.GetRequiredService<RedisCacheConfiguration>();
if (!cacheSettings.Enabled)
{
await next();
return;
}

var cachedService = context.HttpContext.RequestServices.GetRequiredService<IResponseCacheService>();
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));
}
}

/// <summary>
/// Generates the cache key from request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns></returns>
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Blog.Contracts.V1.Responses.UsersResponses;
using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;

namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.GetAllUsers;

public record GetAllUsersQuery : IQuery<List<AccountResponse>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using AutoMapper;
using Blog.Contracts.V1.Responses.UsersResponses;
using Blog.Data.Models;
using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.GetAllUsers;

public class GetAllUsersQueryHandler(
UserManager<ApplicationUser> userManager,
IMapper mapper)
: IQueryHandler<GetAllUsersQuery, List<AccountResponse>>
{
public async Task<List<AccountResponse>> Handle(GetAllUsersQuery query, CancellationToken cancellationToken)
{
var users = await userManager.Users
.Include(u => u.Roles)
.ToListAsync(cancellationToken: cancellationToken);

return mapper.Map<List<AccountResponse>>(users);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Blog.Contracts.V1.Responses.UsersResponses;
using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;

namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Initialize;

public record InitializeQuery(string UserId) : IQuery<List<RoleResponse>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using AutoMapper;
using Blog.Contracts.V1.Responses.UsersResponses;
using Blog.Data.Models;
using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Initialize;

public class InitializeQueryHandler(
RoleManager<ApplicationRole> roleManager,
IMapper mapper)
: IQueryHandler<InitializeQuery, List<RoleResponse>>
{
public async Task<List<RoleResponse>> Handle(InitializeQuery query, CancellationToken cancellationToken)
{
var roles = await roleManager.Roles.ToListAsync(cancellationToken: cancellationToken);

return mapper.Map<List<RoleResponse>>(roles);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;

namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Login;

public record LoginCommand(string Email, string Password)
: ICommand<string>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Blog.EntityServices.EntityFrameworkServices.Identity.Auth;
using BlogVerticalSliceCQRSMinimalApi.CQRSExtensions;

namespace BlogVerticalSliceCQRSMinimalApi.Features.Accounts.Login;

public class LoginCommandHandler(IAuthService authService)
: ICommandHandler<LoginCommand, string>
{
public async Task<string> Handle(LoginCommand command, CancellationToken cancellationToken)
=> await authService.GetJwtAsync(command.Email, command.Password);
}
Loading
Loading