Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
refactor: apply code review improvements to Phase 1b
- Add XML documentation to all queries, commands, and requests for consistency
- Extract duplicate DTO mapping logic to DtoMappingExtensions class
- Use ToDto() and ToListDto() extension methods in all query handlers
- Improve migration logging: warn when DbContext not found, log test environment skips
- Treat Guid.Empty as validation error in GetServiceByIdQueryHandler (consistency with commands)
- Add explicit null check in CreateServiceCategoryEndpoint for safer handling
- Centralize mapping logic to avoid duplication and improve maintainability
  • Loading branch information
Filipe Frigini committed Nov 19, 2025
commit dc29e7cd6cb28dc7195068931834bc3797dc95d5
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ private static async Task<IResult> CreateAsync(
if (!result.IsSuccess)
return Handle(result);

return Handle(result, "GetServiceCategoryById", new { id = result.Value!.Id });
if (result.Value is null)
return Results.BadRequest("Unexpected null value in successful result.");

return Handle(result, "GetServiceCategoryById", new { id = result.Value.Id });
}
}
9 changes: 8 additions & 1 deletion src/Modules/ServiceCatalogs/API/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,19 @@ private static void EnsureDatabaseMigrations(WebApplication app)
try
{
using var scope = app.Services.CreateScope();
var logger = scope.ServiceProvider.GetService<ILogger<Infrastructure.Persistence.ServiceCatalogsDbContext>>();
var context = scope.ServiceProvider.GetService<Infrastructure.Persistence.ServiceCatalogsDbContext>();
if (context == null) return;

if (context == null)
{
logger?.LogWarning("ServiceCatalogsDbContext not found in DI container. Skipping migrations.");
return;
}

// Em ambiente de teste, pular migrações automáticas
if (app.Environment.IsEnvironment("Test") || app.Environment.IsEnvironment("Testing"))
{
logger?.LogInformation("Skipping ServiceCatalogs migrations in test environment: {Environment}", app.Environment.EnvironmentName);
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@

namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory;

/// <summary>
/// Command to activate a service category.
/// </summary>
public sealed record ActivateServiceCategoryCommand(Guid Id) : Command<Result>;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory;

/// <summary>
/// Command to update an existing service category's information.
/// </summary>
public sealed record UpdateServiceCategoryCommand(
Guid Id,
string Name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs.Requests.Service;

/// <summary>
/// Request to update an existing service's information.
/// </summary>
public sealed record UpdateServiceRequest : Request
{
public string Name { get; init; } = string.Empty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs;
using MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings;
using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service;
using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories;
using MeAjudaAi.Shared.Functional;
Expand All @@ -15,13 +16,7 @@ public async Task<Result<IReadOnlyList<ServiceListDto>>> HandleAsync(
{
var services = await repository.GetAllAsync(request.ActiveOnly, cancellationToken);

var dtos = services.Select(s => new ServiceListDto(
s.Id.Value,
s.CategoryId.Value,
s.Name,
s.Description,
s.IsActive
)).ToList();
var dtos = services.Select(s => s.ToListDto()).ToList();

return Result<IReadOnlyList<ServiceListDto>>.Success(dtos);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ public sealed class GetServiceByIdQueryHandler(IServiceRepository repository)
GetServiceByIdQuery request,
CancellationToken cancellationToken = default)
{
// Treat Guid.Empty as validation error for consistency with command handlers
if (request.Id == Guid.Empty)
return Result<ServiceDto?>.Success(null);
return Result<ServiceDto?>.Failure("Service ID cannot be empty.");

var serviceId = ServiceId.From(request.Id);
var service = await repository.GetByIdAsync(serviceId, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs;
using MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings;
using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service;
using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories;
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
Expand All @@ -20,13 +21,7 @@ public async Task<Result<IReadOnlyList<ServiceListDto>>> HandleAsync(
var categoryId = ServiceCategoryId.From(request.CategoryId);
var services = await repository.GetByCategoryAsync(categoryId, request.ActiveOnly, cancellationToken);

var dtos = services.Select(s => new ServiceListDto(
s.Id.Value,
s.CategoryId.Value,
s.Name,
s.Description,
s.IsActive
)).ToList();
var dtos = services.Select(s => s.ToListDto()).ToList();

return Result<IReadOnlyList<ServiceListDto>>.Success(dtos);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs;
using MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings;
using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory;
using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories;
using MeAjudaAi.Shared.Functional;
Expand All @@ -15,15 +16,7 @@ public async Task<Result<IReadOnlyList<ServiceCategoryDto>>> HandleAsync(
{
var categories = await repository.GetAllAsync(request.ActiveOnly, cancellationToken);

var dtos = categories.Select(c => new ServiceCategoryDto(
c.Id.Value,
c.Name,
c.Description,
c.IsActive,
c.DisplayOrder,
c.CreatedAt,
c.UpdatedAt
)).ToList();
var dtos = categories.Select(c => c.ToDto()).ToList();

return Result<IReadOnlyList<ServiceCategoryDto>>.Success(dtos);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs;
using MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings;
using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory;
using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories;
using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects;
Expand All @@ -23,15 +24,7 @@ public sealed class GetServiceCategoryByIdQueryHandler(IServiceCategoryRepositor
if (category is null)
return Result<ServiceCategoryDto?>.Success(null);

var dto = new ServiceCategoryDto(
category.Id.Value,
category.Name,
category.Description,
category.IsActive,
category.DisplayOrder,
category.CreatedAt,
category.UpdatedAt
);
var dto = category.ToDto();

return Result<ServiceCategoryDto?>.Success(dto);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs;
using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities;

namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Mappings;

/// <summary>
/// Extension methods for mapping domain entities to DTOs.
/// Centralizes mapping logic to avoid duplication across handlers.
/// </summary>
public static class DtoMappingExtensions
{
/// <summary>
/// Maps a Service entity to a ServiceListDto.
/// </summary>
public static ServiceListDto ToListDto(this Service service)
=> new(
service.Id.Value,
service.CategoryId.Value,
service.Name,
service.Description,
service.IsActive);

/// <summary>
/// Maps a ServiceCategory entity to a ServiceCategoryDto.
/// </summary>
public static ServiceCategoryDto ToDto(this ServiceCategory category)
=> new(
category.Id.Value,
category.Name,
category.Description,
category.IsActive,
category.DisplayOrder,
category.CreatedAt,
category.UpdatedAt);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@

namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service;

/// <summary>
/// Query to retrieve all services within a specific category.
/// </summary>
public sealed record GetServicesByCategoryQuery(Guid CategoryId, bool ActiveOnly = false)
: Query<Result<IReadOnlyList<ServiceListDto>>>;
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@

namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory;

/// <summary>
/// Query to retrieve service categories with their service counts.
/// </summary>
public sealed record GetServiceCategoriesWithCountQuery(bool ActiveOnly = false)
: Query<Result<IReadOnlyList<ServiceCategoryWithCountDto>>>;
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@

namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory;

/// <summary>
/// Query to retrieve a service category by its identifier.
/// </summary>
public sealed record GetServiceCategoryByIdQuery(Guid Id)
: Query<Result<ServiceCategoryDto?>>;
Loading