Arquitetura e Padrões de Desenvolvimento - MeAjudaAi¶
Este documento detalha a arquitetura, padrões de design e diretrizes de desenvolvimento do projeto MeAjudaAi.
🏗️ Visão Geral da Arquitetura¶
Clean Architecture + DDD¶
O MeAjudaAi implementa Clean Architecture combinada com Domain-Driven Design (DDD) para máxima testabilidade e manutenibilidade.
graph TB
subgraph "🌐 Presentation Layer"
API[API Controllers]
MW[Middlewares]
FIL[Filtros]
end
subgraph "📋 Application Layer"
CMD[Commands]
QRY[Queries]
HDL[Handlers]
VAL[Validators]
end
subgraph "🏛️ Domain Layer"
ENT[Entities]
VO[Value Objects]
DOM[Domain Services]
EVT[Domain Events]
end
subgraph "🔧 Infrastructure Layer"
REPO[Repositories]
EXT[External Services]
CACHE[Caching]
MSG[Messaging]
end
API --> HDL
HDL --> DOM
HDL --> REPO
REPO --> ENT
DOM --> ENT
ENT --> VO
Modular Monolith¶
Estrutura modular que facilita futuras extrações para microserviços.
src/
├── Modules/ # Módulos de domínio
│ ├── Users/ # Gestão de usuários
│ ├── Providers/ # Prestadores de serviços
│ ├── Services/ # Catálogo de serviços (futuro)
│ ├── Bookings/ # Agendamentos (futuro)
│ └── Payments/ # Pagamentos (futuro)
├── Shared/ # Componentes compartilhados
│ └── MeAjudaAi.Shared/ # Primitivos e abstrações
├── Bootstrapper/ # Configuração e startup
│ └── MeAjudaAi.ApiService/ # API principal
└── Aspire/ # Orquestração de desenvolvimento
├── MeAjudaAi.AppHost/ # Host Aspire
└── MeAjudaAi.ServiceDefaults/ # Configurações padrão
🎨 Design Patterns Implementados¶
Este projeto implementa diversos padrões de design consolidados para garantir manutenibilidade, testabilidade e escalabilidade.
1. Repository Pattern¶
Propósito: Abstrair acesso a dados, permitindo testes unitários e troca de implementação.
Implementação Real:
// Interface do repositório (Domain Layer)
public interface IAllowedCityRepository
{
Task<AllowedCity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<AllowedCity?> GetByCityAndStateAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default);
Task<bool> IsCityAllowedAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default);
Task AddAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default);
Task UpdateAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default);
Task DeleteAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default);
}
// Implementação EF Core (Infrastructure Layer)
internal sealed class AllowedCityRepository(LocationsDbContext context) : IAllowedCityRepository
{
public async Task<AllowedCity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await context.AllowedCities
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
}
public async Task<bool> IsCityAllowedAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default)
{
var normalizedCity = cityName?.Trim() ?? string.Empty;
var normalizedState = stateSigla?.Trim().ToUpperInvariant() ?? string.Empty;
return await context.AllowedCities
.AnyAsync(x =>
EF.Functions.ILike(x.CityName, normalizedCity) &&
x.StateSigla == normalizedState &&
x.IsActive,
cancellationToken);
}
}
Benefícios: - ✅ Testes unitários sem banco de dados (mocks) - ✅ Encapsulamento de queries complexas - ✅ Possibilidade de cache transparente
2. CQRS (Command Query Responsibility Segregation)¶
Propósito: Separar operações de leitura (queries) das de escrita (commands).
Implementação Real - Command:
// Command (Application Layer)
public sealed record CreateAllowedCityCommand(
string CityName,
string StateSigla,
string? IbgeCode = null
) : ICommand<Result<Guid>>;
// Handler (Application Layer)
internal sealed class CreateAllowedCityCommandHandler(
IAllowedCityRepository repository,
IUnitOfWork unitOfWork,
ILogger<CreateAllowedCityCommandHandler> logger)
: ICommandHandler<CreateAllowedCityCommand, Result<Guid>>
{
public async Task<Result<Guid>> HandleAsync(
CreateAllowedCityCommand command,
CancellationToken cancellationToken)
{
// 1. Validar duplicação
if (await repository.ExistsAsync(command.CityName, command.StateSigla, cancellationToken))
{
return Result<Guid>.Failure(LocationsErrors.CityAlreadyExists(command.CityName, command.StateSigla));
}
// 2. Criar entidade de domínio
var allowedCity = AllowedCity.Create(
command.CityName,
command.StateSigla,
command.IbgeCode
);
// 3. Persistir
await repository.AddAsync(allowedCity, cancellationToken);
await unitOfWork.CommitAsync(cancellationToken);
logger.LogInformation("Cidade permitida criada: {CityName}/{State}", command.CityName, command.StateSigla);
return Result<Guid>.Success(allowedCity.Id);
}
}
Implementação Real - Query:
// Query (Application Layer)
public sealed record GetServiceCategoryByIdQuery(Guid CategoryId) : IQuery<Result<ServiceCategoryDto?>>;
// Handler (Application Layer)
internal sealed class GetServiceCategoryByIdQueryHandler(
IServiceCategoryRepository repository)
: IQueryHandler<GetServiceCategoryByIdQuery, Result<ServiceCategoryDto?>>
{
public async Task<Result<ServiceCategoryDto?>> HandleAsync(
GetServiceCategoryByIdQuery query,
CancellationToken cancellationToken)
{
var category = await repository.GetByIdAsync(query.CategoryId, cancellationToken);
if (category is null)
{
return Result<ServiceCategoryDto?>.Success(null);
}
var dto = ServiceCategoryMapper.ToDto(category);
return Result<ServiceCategoryDto?>.Success(dto);
}
}
Benefícios: - ✅ Separação clara de responsabilidades - ✅ Otimização independente de leitura vs escrita - ✅ Testabilidade individual de cada operação - ✅ Escalabilidade (queries podem usar read replicas)
3. Domain Events¶
Propósito: Comunicação desacoplada entre agregados e módulos.
Implementação Real:
// Evento de Domínio
public sealed record ProviderRegisteredDomainEvent(
Guid ProviderId,
Guid UserId,
string Name,
EProviderType Type
) : IDomainEvent
{
public DateTime OccurredAt { get; init; } = DateTime.UtcNow;
}
// Handler do Evento (Infrastructure Layer)
internal sealed class ProviderRegisteredDomainEventHandler(
IMessageBus messageBus,
ILogger<ProviderRegisteredDomainEventHandler> logger)
: IDomainEventHandler<ProviderRegisteredDomainEvent>
{
public async Task Handle(ProviderRegisteredDomainEvent notification, CancellationToken cancellationToken)
{
try
{
// Publicar evento de integração para outros módulos
var integrationEvent = new ProviderRegisteredIntegrationEvent(
notification.ProviderId,
notification.UserId,
notification.Name,
notification.Type.ToString()
);
await messageBus.PublishAsync(integrationEvent, cancellationToken);
logger.LogInformation(
"Evento de integração publicado para Provider {ProviderId}",
notification.ProviderId);
}
catch (Exception ex)
{
logger.LogError(ex, "Erro ao processar evento ProviderRegisteredDomainEvent");
throw;
}
}
}
// Uso no Agregado
public class Provider : AggregateRoot<Guid>
{
public static Provider Create(Guid userId, string name, EProviderType type, /* ... */)
{
var provider = new Provider
{
Id = UuidGenerator.NewId(),
UserId = userId,
Name = name,
Type = type,
// ...
};
// Adicionar evento de domínio
provider.AddDomainEvent(new ProviderRegisteredDomainEvent(
provider.Id,
userId,
name,
type
));
return provider;
}
}
Benefícios: - ✅ Desacoplamento entre agregados - ✅ Auditoria automática de mudanças - ✅ Integração assíncrona entre módulos - ✅ Extensibilidade (novos handlers sem alterar código existente)
4. Unit of Work Pattern¶
Propósito: Coordenar mudanças em múltiplos repositórios com transações.
Implementação Real:
// Interface (Shared Layer)
public interface IUnitOfWork
{
Task<int> CommitAsync(CancellationToken cancellationToken = default);
Task RollbackAsync(CancellationToken cancellationToken = default);
}
// Implementação EF Core (Infrastructure Layer)
internal sealed class UnitOfWork(DbContext context) : IUnitOfWork
{
public async Task<int> CommitAsync(CancellationToken cancellationToken = default)
{
// EF Core já gerencia transação implicitamente
return await context.SaveChangesAsync(cancellationToken);
}
public async Task RollbackAsync(CancellationToken cancellationToken = default)
{
await context.Database.RollbackTransactionAsync(cancellationToken);
}
}
// Uso em Handler
internal sealed class UpdateProviderProfileCommandHandler(
IProviderRepository providerRepository,
IDocumentsModuleApi documentsApi,
IUnitOfWork unitOfWork)
{
public async Task<Result> HandleAsync(UpdateProviderProfileCommand command, CancellationToken ct)
{
// 1. Buscar provider
var provider = await providerRepository.GetByIdAsync(command.ProviderId, ct);
// 2. Atualizar aggregate
provider.UpdateProfile(/* ... */);
// 3. Atualizar no repositório
await providerRepository.UpdateAsync(provider, ct);
// 4. Commit atômico (transação)
await unitOfWork.CommitAsync(ct);
return Result.Success();
}
}
Benefícios: - ✅ Transações atômicas - ✅ Coordenação de múltiplas mudanças - ✅ Rollback automático em caso de erro
5. Factory Pattern¶
Propósito: Encapsular lógica de criação de objetos complexos.
Implementação Real:
// UuidGenerator Factory (Shared/Time)
public static class UuidGenerator
{
public static Guid NewId()
{
return Guid.CreateVersion7(); // UUID v7 com timestamp ordenável
}
}
// SerilogConfigurator Factory (Shared/Logging)
public static class SerilogConfigurator
{
public static ILogger CreateLogger(IConfiguration configuration, string environmentName)
{
var loggerConfig = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.WithProperty("Application", "MeAjudaAi")
.Enrich.WithProperty("Environment", environmentName)
.Enrich.WithMachineName()
.Enrich.WithThreadId();
if (environmentName == "Development")
{
loggerConfig.WriteTo.Console();
}
loggerConfig.WriteTo.File(
"logs/app-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7
);
return loggerConfig.CreateLogger();
}
}
Benefícios: - ✅ Encapsulamento de lógica de criação - ✅ Configuração centralizada - ✅ Fácil substituição de implementação
6. Strategy Pattern¶
Propósito: Selecionar algoritmo/implementação em runtime.
Implementação Real (MessageBus):
// Interface comum (Shared/Messaging)
public interface IMessageBus
{
Task PublishAsync<T>(T message, CancellationToken cancellationToken = default);
Task SubscribeAsync<T>(Func<T, CancellationToken, Task> handler, CancellationToken cancellationToken = default);
}
// Estratégia 1: RabbitMQ
public class RabbitMqMessageBus : IMessageBus
{
public async Task PublishAsync<T>(T message, CancellationToken ct)
{
// Implementação RabbitMQ
}
}
// Estratégia 2: Azure Service Bus
public class ServiceBusMessageBus : IMessageBus
{
public async Task PublishAsync<T>(T message, CancellationToken ct)
{
// Implementação Azure Service Bus
}
}
// Seleção em runtime (Program.cs)
var messageBusProvider = builder.Configuration["MessageBus:Provider"];
if (messageBusProvider == "ServiceBus")
{
builder.Services.AddSingleton<IMessageBus, ServiceBusMessageBus>();
}
else
{
builder.Services.AddSingleton<IMessageBus, RabbitMqMessageBus>();
}
Benefícios: - ✅ Troca de implementação sem alterar código cliente - ✅ Suporte a múltiplos providers (RabbitMQ, Azure, Kafka) - ✅ Testabilidade (mocks)
7. Decorator Pattern (via Pipeline Behaviors)¶
Propósito: Adicionar comportamentos cross-cutting (logging, validação, cache) transparentemente.
Implementação Real:
// Behavior para Caching (Shared/Behaviors)
public class CachingBehavior<TRequest, TResponse>(
ICacheService cacheService,
ILogger<CachingBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
// Só aplica cache se query implementa ICacheableQuery
if (request is not ICacheableQuery cacheableQuery)
{
return await next();
}
var cacheKey = cacheableQuery.GetCacheKey();
var cacheExpiration = cacheableQuery.GetCacheExpiration();
// Tentar buscar no cache
var (cachedResult, isCached) = await cacheService.GetAsync<TResponse>(cacheKey, cancellationToken);
if (isCached)
{
logger.LogDebug("Cache hit for key: {CacheKey}", cacheKey);
return cachedResult;
}
// Executar query e cachear resultado
var result = await next();
if (result is not null)
{
await cacheService.SetAsync(cacheKey, result, cacheExpiration, cancellationToken);
}
return result;
}
}
// Registro (Application Layer Extensions)
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
Benefícios: - ✅ Concerns cross-cutting sem poluir handlers - ✅ Ordem de execução configurável - ✅ Adição/remoção de behaviors sem alterar código
8. Options Pattern¶
Propósito: Configuração fortemente tipada via injeção de dependência.
Implementação Real:
// Opções fortemente tipadas (Shared/Messaging)
public sealed class MessageBusOptions
{
public const string SectionName = "MessageBus";
public string Provider { get; set; } = "RabbitMQ"; // ou "ServiceBus"
public string ConnectionString { get; set; } = string.Empty;
public int RetryCount { get; set; } = 3;
public int RetryDelaySeconds { get; set; } = 5;
}
// Registro no Program.cs
builder.Services.Configure<MessageBusOptions>(
builder.Configuration.GetSection(MessageBusOptions.SectionName));
// Uso via injeção
public class RabbitMqMessageBus(
IOptions<MessageBusOptions> options,
ILogger<RabbitMqMessageBus> logger)
{
private readonly MessageBusOptions _options = options.Value;
public async Task PublishAsync<T>(T message, CancellationToken ct)
{
// Usa _options.ConnectionString, _options.RetryCount, etc.
}
}
Benefícios: - ✅ Configuração fortemente tipada (compile-time safety) - ✅ Validação via Data Annotations - ✅ Hot reload de configurações (IOptionsSnapshot)
9. Middleware Pipeline Pattern¶
Propósito: Processar requisições HTTP em cadeia com responsabilidades isoladas.
Implementação Real:
// Middleware customizado (ApiService/Middlewares)
public class GeographicRestrictionMiddleware(
RequestDelegate next,
ILocationsModuleApi locationsApi,
ILogger<GeographicRestrictionMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
// 1. Verificar se endpoint requer restrição geográfica
var endpoint = context.GetEndpoint();
var restrictionAttribute = endpoint?.Metadata
.GetMetadata<RequireGeographicRestrictionAttribute>();
if (restrictionAttribute is null)
{
await next(context);
return;
}
// 2. Extrair cidade/estado da requisição
var city = context.Request.Headers["X-City"].ToString();
var state = context.Request.Headers["X-State"].ToString();
if (string.IsNullOrEmpty(city) || string.IsNullOrEmpty(state))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new { error = "City and State required" });
return;
}
// 3. Validar via LocationsModuleApi
var isAllowed = await locationsApi.IsCityAllowedAsync(city, state);
if (!isAllowed.IsSuccess || !isAllowed.Value)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsJsonAsync(new { error = "City not allowed" });
return;
}
// 4. Continuar pipeline
await next(context);
}
}
// Registro no pipeline (Program.cs)
app.UseMiddleware<GeographicRestrictionMiddleware>();
Benefícios: - ✅ Separação de concerns (logging, auth, validação) - ✅ Ordem de execução clara - ✅ Reutilização entre endpoints
🚫 Anti-Patterns Evitados¶
❌ Anemic Domain Model¶
Evitado: Entidades ricas com comportamento encapsulado.
// ❌ ANTI-PATTERN: Anemic Domain
public class Provider
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Status { get; set; } // string sem validação
}
// ✅ PATTERN CORRETO: Rich Domain Model
public class Provider : AggregateRoot<Guid>
{
public string Name { get; private set; }
public EProviderStatus Status { get; private set; }
public void Activate(string adminEmail)
{
if (Status != EProviderStatus.PendingApproval)
throw new InvalidOperationException("Provider must be pending approval");
Status = EProviderStatus.Active;
AddDomainEvent(new ProviderActivatedDomainEvent(Id, adminEmail));
}
}
❌ Repository Anti-Patterns¶
Evitado: Repositórios genéricos com métodos desnecessários.
// ❌ ANTI-PATTERN: Generic Repository com métodos inutilizados
public interface IRepository<T>
{
Task<T> GetByIdAsync(Guid id);
Task<IEnumerable<T>> GetAllAsync(); // Perigoso: pode retornar milhões de registros
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
// ✅ PATTERN CORRETO: Repositórios específicos por agregado
public interface IProviderRepository
{
Task<Provider?> GetByIdAsync(Guid id, CancellationToken ct);
Task<Provider?> GetByUserIdAsync(Guid userId, CancellationToken ct);
Task<IReadOnlyList<Provider>> GetByCityAsync(string city, int pageSize, int page, CancellationToken ct);
// Apenas métodos realmente necessários
}
❌ Service Locator¶
Evitado: Dependency Injection explícita via construtor.
// ❌ ANTI-PATTERN: Service Locator
public class ProviderService
{
public void RegisterProvider(RegisterProviderDto dto)
{
var repository = ServiceLocator.GetService<IProviderRepository>();
var logger = ServiceLocator.GetService<ILogger>();
// Dependências ocultas, difícil de testar
}
}
// ✅ PATTERN CORRETO: Constructor Injection
public class RegisterProviderCommandHandler(
IProviderRepository repository,
IUnitOfWork unitOfWork,
ILogger<RegisterProviderCommandHandler> logger)
{
// Dependências explícitas e testáveis
}
📚 Referências e Boas Práticas¶
- Clean Architecture: Uncle Bob (Robert C. Martin)
- Domain-Driven Design: Eric Evans, Vaughn Vernon
- CQRS: Greg Young, Udi Dahan
- Modular Monolith: Milan Jovanovic, Kamil Grzybek
- Repository Pattern: Martin Fowler
- .NET Design Patterns: Microsoft Docs
🎯 Domain-Driven Design (DDD)¶
Bounded Contexts¶
1. Users Context¶
Responsabilidade: Gestão completa de identidade e perfis de usuário
namespace MeAjudaAi.Modules.Users.Domain;
/// <summary>
/// Contexto delimitado para gestão de usuários e identidade
/// </summary>
public class UsersContext
{
// Entidades principais
public DbSet<User> Users { get; set; }
// Agregados relacionados
public DbSet<UserProfile> UserProfiles { get; set; }
public DbSet<UserPreferences> UserPreferences { get; set; }
}
```bash
**Conceitos do Domínio**:
- **User**: Agregado raiz para dados básicos de identidade
- **UserProfile**: Perfil detalhado (experiência, habilidades, localização)
- **UserPreferences**: Preferências e configurações personalizadas
#### 2. **Providers Context**
**Responsabilidade**: Gestão completa de prestadores de serviços
```csharp
namespace MeAjudaAi.Modules.Providers.Domain;
/// <summary>
/// Contexto delimitado para gestão de prestadores de serviços
/// </summary>
public class ProvidersContext
{
// Entidades principais
public DbSet<Provider> Providers { get; set; }
}
Conceitos do Domínio: - Provider: Agregado raiz para prestadores de serviços com perfil empresarial - BusinessProfile: Perfil empresarial detalhado (razão social, contato, endereço) - Document: Documentos de verificação (CPF, CNPJ, certificações) - Qualification: Qualificações e habilitações profissionais - VerificationStatus: Status de verificação (Pending, Verified, Rejected, etc.)
3. ServiceCatalogs Context (Implementado)¶
Responsabilidade: Catálogo administrativo de categorias e serviços
Conceitos Implementados: - ServiceCategory: Categorias hierárquicas de serviços (aggregate root) - Service: Serviços oferecidos vinculados a categorias (aggregate root) - DisplayOrder: Ordenação customizada para apresentação - Activation/Deactivation: Controle de visibilidade no catálogo
Schema: service_catalogs (isolado no PostgreSQL)
4. Location Context (Implementado)¶
Responsabilidade: Geolocalização e lookup de CEP brasileiro
Conceitos Implementados: - Cep: Value object para CEP validado - Coordinates: Latitude/Longitude para geolocalização - Address: Endereço completo com dados estruturados - CepLookupService: Integração com ViaCEP, BrasilAPI, OpenCEP (fallback)
Observação: Módulo stateless (sem schema próprio), fornece serviços via Module API
5. Bookings Context (Futuro)¶
Responsabilidade: Agendamento e execução de serviços
Conceitos Planejados: - Booking: Agregado raiz para agendamentos - Schedule: Disponibilidade de prestadores - ServiceExecution: Execução e acompanhamento do serviço
Agregados e Entidades¶
Agregado User¶
/// <summary>
/// Agregado raiz para gestão de usuários do sistema
/// Responsável por manter a consistência dos dados do usuário
/// </summary>
public class User : AggregateRoot<UserId>
{
/// <summary>Identificador único externo (Keycloak)</summary>
public ExternalUserId ExternalId { get; private set; }
/// <summary>Email do usuário (único)</summary>
public Email Email { get; private set; }
/// <summary>Nome completo do usuário</summary>
public FullName FullName { get; private set; }
/// <summary>Tipo do usuário no sistema</summary>
public UserType UserType { get; private set; }
/// <summary>Status atual do usuário</summary>
public UserStatus Status { get; private set; }
/// <summary>Perfil detalhado do usuário</summary>
public UserProfile Profile { get; private set; }
/// <summary>Preferências do usuário</summary>
public UserPreferences Preferences { get; private set; }
}
Agregado Provider¶
/// <summary>
/// Agregado raiz para gestão de prestadores de serviços
/// Responsável por manter a consistência dos dados do prestador
/// </summary>
public class Provider : AggregateRoot<ProviderId>
{
/// <summary>Identificador do usuário associado</summary>
public Guid UserId { get; private set; }
/// <summary>Nome do prestador</summary>
public string Name { get; private set; }
/// <summary>Tipo do prestador (Individual ou Company)</summary>
public EProviderType Type { get; private set; }
/// <summary>Perfil empresarial completo</summary>
public BusinessProfile BusinessProfile { get; private set; }
/// <summary>Status de verificação atual</summary>
public EVerificationStatus VerificationStatus { get; private set; }
/// <summary>Documentos de verificação</summary>
public IReadOnlyCollection<Document> Documents { get; }
/// <summary>Qualificações profissionais</summary>
public IReadOnlyCollection<Qualification> Qualifications { get; }
}
Value Objects¶
/// <summary>
/// Value Object para identificador de usuário
/// Garante type safety e validação de identificadores
/// </summary>
public sealed record UserId(Guid Value) : EntityId(Value)
{
public static UserId New() => new(Guid.NewGuid());
public static UserId From(Guid value) => new(value);
public static UserId From(string value) => new(Guid.Parse(value));
}
/// <summary>
/// Value Object para email com validação
/// </summary>
public sealed record Email
{
private static readonly EmailAddressAttribute EmailValidator = new();
public string Value { get; }
public Email(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Email não pode ser vazio");
if (!EmailValidator.IsValid(value))
throw new ArgumentException($"Email inválido: {value}");
Value = value.ToLowerInvariant();
}
public static implicit operator string(Email email) => email.Value;
public static implicit operator Email(string email) => new(email);
}
Value Objects do Módulo Providers¶
/// <summary>
/// Value Object para identificador de prestador
/// </summary>
public sealed record ProviderId(Guid Value) : EntityId(Value)
{
public static ProviderId New() => new(Guid.NewGuid());
public static ProviderId From(Guid value) => new(value);
}
/// <summary>
/// Value Object para perfil empresarial
/// </summary>
public class BusinessProfile : ValueObject
{
public string LegalName { get; private set; }
public string? FantasyName { get; private set; }
public string? Description { get; private set; }
public ContactInfo ContactInfo { get; private set; }
public Address PrimaryAddress { get; private set; }
public BusinessProfile(
string legalName,
ContactInfo contactInfo,
Address primaryAddress,
string? fantasyName = null,
string? description = null)
{
// Validações e inicialização
}
}
/// <summary>
/// Value Object para documentos
/// </summary>
public class Document : ValueObject
{
public string Number { get; private set; }
public EDocumentType DocumentType { get; private set; }
public Document(string number, EDocumentType documentType)
{
// Validações e inicialização
}
}
Domain Events¶
/// <summary>
/// Evento disparado quando um novo usuário é registrado
/// </summary>
public sealed record UserRegisteredDomainEvent(
UserId UserId,
Email Email,
UserType UserType,
DateTime OccurredAt
) : DomainEvent(OccurredAt);
/// <summary>
/// Evento disparado quando perfil do usuário é atualizado
/// </summary>
public sealed record UserProfileUpdatedDomainEvent(
UserId UserId,
UserProfile UpdatedProfile,
DateTime OccurredAt
) : DomainEvent(OccurredAt);
Domain Events do Módulo Providers¶
/// <summary>
/// Evento disparado quando um novo prestador é registrado
/// </summary>
public sealed record ProviderRegisteredDomainEvent(
Guid AggregateId,
int Version,
Guid UserId,
string Name,
EProviderType Type,
string Email
) : DomainEvent(AggregateId, Version);
/// <summary>
/// Evento disparado quando um documento é adicionado
/// </summary>
public sealed record ProviderDocumentAddedDomainEvent(
Guid AggregateId,
int Version,
string DocumentNumber,
EDocumentType DocumentType
) : DomainEvent(AggregateId, Version);
/// <summary>
/// Evento disparado quando o status de verificação é atualizado
/// </summary>
public sealed record ProviderVerificationStatusUpdatedDomainEvent(
Guid AggregateId,
int Version,
EVerificationStatus OldStatus,
EVerificationStatus NewStatus,
string? UpdatedBy
) : DomainEvent(AggregateId, Version);
/// <summary>
/// Evento disparado quando um prestador é excluído
/// </summary>
public sealed record ProviderDeletedDomainEvent(
Guid AggregateId,
int Version,
string Reason
) : DomainEvent(AggregateId, Version);
⚡ CQRS (Command Query Responsibility Segregation)¶
Estrutura de Commands¶
/// <summary>
/// Command para registro de novo usuário
/// </summary>
public sealed record RegisterUserCommand(
string ExternalId,
string Email,
string FirstName,
string LastName,
UserType UserType
) : ICommand<RegisterUserResult>;
/// <summary>
/// Handler para processamento do command RegisterUser
/// </summary>
public sealed class RegisterUserCommandHandler
: ICommandHandler<RegisterUserCommand, RegisterUserResult>
{
private readonly IUsersRepository _usersRepository;
private readonly IUserProfileService _profileService;
private readonly IEventBus _eventBus;
public async Task<RegisterUserResult> Handle(
RegisterUserCommand command,
CancellationToken cancellationToken)
{
// 1. Validar se usuário já existe
var existingUser = await _usersRepository
.GetByExternalIdAsync(command.ExternalId, cancellationToken);
if (existingUser is not null)
return RegisterUserResult.UserAlreadyExists(command.ExternalId);
// 2. Criar agregado User
var user = User.Create(
ExternalUserId.From(command.ExternalId),
new Email(command.Email),
new FullName(command.FirstName, command.LastName),
command.UserType
);
// 3. Criar perfil inicial
await _profileService.CreateInitialProfileAsync(user.Id, cancellationToken);
// 4. Persistir
await _usersRepository.AddAsync(user, cancellationToken);
// 5. Publicar eventos de domínio
await _eventBus.PublishAsync(user.DomainEvents, cancellationToken);
return RegisterUserResult.Success(user.Id);
}
}
Estrutura de Queries¶
/// <summary>
/// Query para buscar usuário por ID
/// </summary>
public sealed record GetUserByIdQuery(UserId UserId) : IQuery<UserDto?>;
/// <summary>
/// Handler para query GetUserById
/// </summary>
public sealed class GetUserByIdQueryHandler
: IQueryHandler<GetUserByIdQuery, UserDto?>
{
private readonly IUsersReadRepository _repository;
public async Task<UserDto?> Handle(
GetUserByIdQuery query,
CancellationToken cancellationToken)
{
return await _repository.GetUserByIdAsync(query.UserId, cancellationToken);
}
}
DTOs e Mapeamento¶
/// <summary>
/// DTO para transferência de dados de usuário
/// </summary>
public sealed record UserDto(
string Id,
string ExternalId,
string Email,
string FirstName,
string LastName,
string UserType,
string Status,
UserProfileDto? Profile,
DateTime CreatedAt,
DateTime? UpdatedAt
);
/// <summary>
/// Mapper para conversão entre entidades e DTOs
/// </summary>
public static class UserMapper
{
public static UserDto ToDto(User user)
{
return new UserDto(
Id: user.Id.Value.ToString(),
ExternalId: user.ExternalId.Value,
Email: user.Email.Value,
FirstName: user.FullName.FirstName,
LastName: user.FullName.LastName,
UserType: user.UserType.ToString(),
Status: user.Status.ToString(),
Profile: user.Profile?.ToDto(),
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt
);
}
}
🔌 Dependency Injection e Modularização¶
Registro de Serviços por Módulo¶
/// <summary>
/// Extensão para registro dos serviços do módulo Users
/// </summary>
public static class UsersModuleServiceCollectionExtensions
{
public static IServiceCollection AddUsersModule(
this IServiceCollection services,
IConfiguration configuration)
{
// Contexto de banco
services.AddDbContext<UsersDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString("Users")));
// Repositórios
services.AddScoped<IUsersRepository, UsersRepository>();
services.AddScoped<IUsersReadRepository, UsersReadRepository>();
// Serviços de domínio
services.AddScoped<IUserProfileService, UserProfileService>();
services.AddScoped<IUserValidationService, UserValidationService>();
// Handlers CQRS
services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(RegisterUserCommandHandler).Assembly));
// Validators
services.AddValidatorsFromAssembly(typeof(RegisterUserCommandValidator).Assembly);
// Event Handlers
services.AddScoped<INotificationHandler<UserRegisteredDomainEvent>,
SendWelcomeEmailHandler>();
return services;
}
}
`csharp
### **Configuração no Program.cs**
```csharp
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Service Defaults (Aspire)
builder.AddServiceDefaults();
// Módulos de domínio
builder.Services.AddUsersModule(builder.Configuration);
// builder.Services.AddServicesModule(builder.Configuration); // Futuro
// builder.Services.AddBookingsModule(builder.Configuration); // Futuro
// Shared services
builder.Services.AddSharedServices(builder.Configuration);
// Infrastructure
builder.Services.AddInfrastructure(builder.Configuration);
var app = builder.Build();
// Middleware pipeline
app.UseSharedMiddleware();
app.MapUsersEndpoints();
app.MapDefaultEndpoints();
app.Run();
}
}
`yaml
## 📡 Event-Driven Architecture
### **Domain Events**
```csharp
/// <summary>
/// Classe base para eventos de domínio
/// </summary>
public abstract record DomainEvent(DateTime OccurredAt) : IDomainEvent;
/// <summary>
/// Interface para eventos de domínio
/// </summary>
public interface IDomainEvent : INotification
{
DateTime OccurredAt { get; }
}
/// <summary>
/// Agregado base com suporte a eventos de domínio
/// </summary>
public abstract class AggregateRoot<TId> : Entity<TId> where TId : EntityId
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void RaiseDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
}
`csharp
### **Implementação do Event Bus**
```csharp
/// <summary>
/// Event Bus para publicação de eventos
/// </summary>
public interface IEventBus
{
Task PublishAsync<T>(T @event, CancellationToken cancellationToken = default)
where T : IDomainEvent;
Task PublishAsync(IEnumerable<IDomainEvent> events, CancellationToken cancellationToken = default);
}
/// <summary>
/// Implementação do Event Bus usando MediatR
/// </summary>
public sealed class MediatREventBus : IEventBus
{
private readonly IMediator _mediator;
public MediatREventBus(IMediator mediator)
{
_mediator = mediator;
}
public async Task PublishAsync<T>(T @event, CancellationToken cancellationToken = default)
where T : IDomainEvent
{
await _mediator.Publish(@event, cancellationToken);
}
public async Task PublishAsync(IEnumerable<IDomainEvent> events, CancellationToken cancellationToken = default)
{
foreach (var @event in events)
{
await _mediator.Publish(@event, cancellationToken);
}
}
}
`sql
### **Event Handlers**
```csharp
/// <summary>
/// Handler para evento de usuário registrado
/// </summary>
public sealed class SendWelcomeEmailHandler
: INotificationHandler<UserRegisteredDomainEvent>
{
private readonly IEmailService _emailService;
private readonly ILogger<SendWelcomeEmailHandler> _logger;
public async Task Handle(
UserRegisteredDomainEvent notification,
CancellationToken cancellationToken)
{
try
{
var welcomeEmail = new WelcomeEmail(
To: notification.Email,
UserType: notification.UserType
);
await _emailService.SendAsync(welcomeEmail, cancellationToken);
_logger.LogInformation(
"Email de boas-vindas enviado para {Email} (UserId: {UserId})",
notification.Email, notification.UserId);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Erro ao enviar email de boas-vindas para {Email} (UserId: {UserId})",
notification.Email, notification.UserId);
}
}
}
`csharp
## 🛡️ Padrões de Segurança
### **Autenticação e Autorização**
```csharp
/// <summary>
/// Serviço de autenticação integrado com Keycloak
/// </summary>
public interface IAuthenticationService
{
Task<AuthenticationResult> AuthenticateAsync(string token, CancellationToken cancellationToken = default);
Task<UserContext> GetCurrentUserAsync(CancellationToken cancellationToken = default);
Task<bool> HasPermissionAsync(string permission, CancellationToken cancellationToken = default);
}
/// <summary>
/// Contexto do usuário atual autenticado
/// </summary>
public sealed record UserContext(
string ExternalId,
string Email,
IReadOnlyList<string> Roles,
IReadOnlyList<string> Permissions
);
/// <summary>
/// Filtro de autorização customizado
/// </summary>
public sealed class RequirePermissionAttribute : AuthorizeAttribute, IAuthorizationRequirement
{
public string Permission { get; }
public RequirePermissionAttribute(string permission)
{
Permission = permission;
Policy = $"RequirePermission:{permission}";
}
}
` ext
### **Validation Pattern**
```csharp
/// <summary>
/// Validator para command de registro de usuário
/// </summary>
public sealed class RegisterUserCommandValidator : AbstractValidator<RegisterUserCommand>
{
public RegisterUserCommandValidator()
{
RuleFor(x => x.ExternalId)
.NotEmpty()
.WithMessage("ExternalId é obrigatório");
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress()
.WithMessage("Email deve ser válido");
RuleFor(x => x.FirstName)
.NotEmpty()
.MaximumLength(100)
.WithMessage("Nome deve ter entre 1 e 100 caracteres");
RuleFor(x => x.LastName)
.NotEmpty()
.MaximumLength(100)
.WithMessage("Sobrenome deve ter entre 1 e 100 caracteres");
RuleFor(x => x.UserType)
.IsInEnum()
.WithMessage("Tipo de usuário inválido");
}
}
`csharp
## 🔄 Padrões de Resilência
### **Retry Pattern**
```csharp
/// <summary>
/// Política de retry para operações críticas
/// </summary>
public static class RetryPolicies
{
public static readonly RetryPolicy DatabaseRetryPolicy = Policy
.Handle<PostgresException>()
.Or<TimeoutException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
var logger = context.GetLogger();
logger?.LogWarning(
"Tentativa {RetryCount} falhou. Tentando novamente em {Delay}ms",
retryCount, timespan.TotalMilliseconds);
});
public static readonly RetryPolicy ExternalServiceRetryPolicy = Policy
.Handle<HttpRequestException>()
.Or<TaskCanceledException>()
.WaitAndRetryAsync(
retryCount: 2,
sleepDurationProvider: _ => TimeSpan.FromMilliseconds(500));
}
`yaml
### **Circuit Breaker Pattern**
```csharp
/// <summary>
/// Circuit Breaker para serviços externos
/// </summary>
public static class CircuitBreakerPolicies
{
public static readonly CircuitBreakerPolicy ExternalServiceCircuitBreaker = Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (exception, duration) =>
{
// Log circuit breaker opened
},
onReset: () =>
{
// Log circuit breaker closed
});
}
`csharp
## 📊 Observabilidade e Monitoramento
### **Logging Structure**
```csharp
/// <summary>
/// Logger estruturado para operações de usuário
/// </summary>
public static partial class UserLogMessages
{
[LoggerMessage(
EventId = 1001,
Level = LogLevel.Information,
Message = "Usuário {UserId} registrado com sucesso (Email: {Email}, Type: {UserType})")]
public static partial void UserRegistered(
this ILogger logger, string userId, string email, string userType);
[LoggerMessage(
EventId = 1002,
Level = LogLevel.Warning,
Message = "Tentativa de registro de usuário duplicado (ExternalId: {ExternalId})")]
public static partial void DuplicateUserRegistration(
this ILogger logger, string externalId);
[LoggerMessage(
EventId = 1003,
Level = LogLevel.Error,
Message = "Erro ao registrar usuário (ExternalId: {ExternalId})")]
public static partial void UserRegistrationFailed(
this ILogger logger, string externalId, Exception exception);
}
` ext
### **Métricas Personalizadas**
```csharp
/// <summary>
/// Métricas customizadas para o módulo Users
/// </summary>
public sealed class UserMetrics
{
private readonly Counter<int> _userRegistrationsCounter;
private readonly Histogram<double> _registrationDuration;
private readonly ObservableGauge<int> _activeUsersGauge;
public UserMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("MeAjudaAi.Users");
_userRegistrationsCounter = meter.CreateCounter<int>(
"user_registrations_total",
description: "Total de registros de usuários");
_registrationDuration = meter.CreateHistogram<double>(
"user_registration_duration_ms",
description: "Duração do processo de registro de usuário");
_activeUsersGauge = meter.CreateObservableGauge<int>(
"active_users_total",
description: "Número atual de usuários ativos");
}
public void RecordUserRegistration(UserType userType, double durationMs)
{
_userRegistrationsCounter.Add(1,
new KeyValuePair<string, object?>("user_type", userType.ToString()));
_registrationDuration.Record(durationMs,
new KeyValuePair<string, object?>("user_type", userType.ToString()));
}
}
`csharp
## 🧪 Padrões de Teste
### **Test Structure**
```csharp
/// <summary>
/// Classe base para testes de unidade do domínio
/// </summary>
public abstract class DomainTestBase
{
protected static User CreateValidUser(
string externalId = "test-external-id",
string email = "test@example.com",
UserType userType = UserType.Customer)
{
return User.Create(
ExternalUserId.From(externalId),
new Email(email),
new FullName("Test", "User"),
userType
);
}
}
/// <summary>
/// Testes para o agregado User
/// </summary>
public sealed class UserTests : DomainTestBase
{
[Fact]
public void Create_ValidData_ShouldCreateUser()
{
// Arrange
var externalId = ExternalUserId.From("test-id");
var email = new Email("test@example.com");
var fullName = new FullName("Test", "User");
var userType = UserType.Customer;
// Act
var user = User.Create(externalId, email, fullName, userType);
// Assert
user.Should().NotBeNull();
user.ExternalId.Should().Be(externalId);
user.Email.Should().Be(email);
user.FullName.Should().Be(fullName);
user.UserType.Should().Be(userType);
user.Status.Should().Be(UserStatus.Active);
user.DomainEvents.Should().ContainSingle()
.Which.Should().BeOfType<UserRegisteredDomainEvent>();
}
}
`yaml
### **Integration Tests**
```csharp
/// <summary>
/// Classe base para testes de integração
/// </summary>
public abstract class IntegrationTestBase : IClassFixture<TestWebApplicationFactory>
{
protected readonly TestWebApplicationFactory Factory;
protected readonly HttpClient Client;
protected readonly IServiceScope Scope;
protected IntegrationTestBase(TestWebApplicationFactory factory)
{
Factory = factory;
Client = factory.CreateClient();
Scope = factory.Services.CreateScope();
}
protected T GetService<T>() where T : notnull
=> Scope.ServiceProvider.GetRequiredService<T>();
}
/// <summary>
/// Testes de integração para endpoints de usuário
/// </summary>
public sealed class UserEndpointsTests : IntegrationTestBase
{
public UserEndpointsTests(TestWebApplicationFactory factory) : base(factory) { }
[Fact]
public async Task RegisterUser_ValidData_ShouldReturnCreated()
{
// Arrange
var request = new RegisterUserRequest(
ExternalId: "test-external-id",
Email: "test@example.com",
FirstName: "Test",
LastName: "User",
UserType: "Customer"
);
// Act
var response = await Client.PostAsJsonAsync("/api/users/register", request);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var result = await response.Content.ReadFromJsonAsync<RegisterUserResponse>();
result.Should().NotBeNull();
result!.UserId.Should().NotBeEmpty();
}
}
`csharp
## 🔌 Module APIs - Comunicação Entre Módulos
### **Padrão Module APIs**
O padrão Module APIs é usado para comunicação síncrona e type-safe entre módulos. Cada módulo pode expor uma API pública através de uma interface bem definida, permitindo que outros módulos a consumam diretamente, sem acoplamento forte com a implementação interna.
### **Estrutura Recomendada**
```csharp
/// <summary>
/// Interface da API pública do módulo Users
/// Define contratos para comunicação síncrona entre módulos.
/// </summary>
public interface IUsersModuleApi : IModuleApi
{
Task<Result<ModuleUserDto?>> GetUserByIdAsync(Guid userId, CancellationToken cancellationToken = default);
Task<Result<bool>> CheckUserExistsAsync(Guid userId, CancellationToken cancellationToken = default);
}
/// <summary>
/// Implementação da API do módulo Users
/// Localizada em: src/Modules/Users/Application/ModuleApi/
/// </summary>
[ModuleApi("Users", "1.0")]
public sealed class UsersModuleApi : IUsersModuleApi
{
// A implementação utiliza os handlers e serviços internos do módulo Users
// para responder às solicitações, sem expor detalhes da camada de domínio.
}
📡 Integration Events - Comunicação Assíncrona¶
Padrão Integration Events¶
Para comunicação assíncrona e desacoplada, o projeto utiliza o padrão de Integration Events. Um módulo publica um evento em um message bus (como RabbitMQ ou Azure Service Bus) quando um estado importante é alterado. Outros módulos podem se inscrever para receber notificações desses eventos e reagir a eles, sem que o publicador precise conhecê-los.
Este padrão é ideal para: - Notificar outros módulos sobre a criação, atualização ou exclusão de entidades. - Disparar fluxos de trabalho em background. - Manter a consistência eventual entre diferentes Bounded Contexts.
Estrutura Recomendada¶
/// <summary>
/// Define um evento de integração que ocorreu no sistema.
/// Herda de IEvent e adiciona um campo 'Source' para identificar o módulo de origem.
/// </summary>
public interface IIntegrationEvent : IEvent
{
string Source { get; }
}
/// <summary>
/// Exemplo de um evento de integração publicado quando um usuário é registrado.
/// Este evento carrega os dados essenciais para que outros módulos possam reagir.
/// </summary>
public sealed record UserRegisteredIntegrationEvent(
Guid UserId,
string Username,
string Email,
DateTime RegisteredAt
) : IIntegrationEvent;
🚦 Status Atual da Implementação¶
Status: ✅ PARCIALMENTE IMPLEMENTADO (Sprint 1 Dias 3-6, Nov 2025)
Module APIs Implementados:¶
1. IDocumentsModuleApi ✅ COMPLETO¶
Localização: src/Shared/Contracts/Modules/Documents/IDocumentsModuleApi.cs
Implementação: src/Modules/Documents/Application/ModuleApi/DocumentsModuleApi.cs
Métodos (7):
Task<Result<ModuleDocumentDto?>> GetDocumentByIdAsync(Guid documentId, CancellationToken ct);
Task<Result<IReadOnlyList<ModuleDocumentDto>>> GetProviderDocumentsAsync(Guid providerId, CancellationToken ct);
Task<Result<ModuleDocumentStatusDto?>> GetDocumentStatusAsync(Guid documentId, CancellationToken ct);
Task<Result<bool>> HasVerifiedDocumentsAsync(Guid providerId, CancellationToken ct);
Task<Result<bool>> HasRequiredDocumentsAsync(Guid providerId, CancellationToken ct);
Task<Result<bool>> HasPendingDocumentsAsync(Guid providerId, CancellationToken ct);
Task<Result<bool>> HasRejectedDocumentsAsync(Guid providerId, CancellationToken ct);
Usado por:
- ✅ ActivateProviderCommandHandler (Providers) - valida documentos antes de ativação
Exemplo de Uso:
// src/Modules/Providers/Application/Handlers/Commands/ActivateProviderCommandHandler.cs
public sealed class ActivateProviderCommandHandler(
IProviderRepository providerRepository,
IDocumentsModuleApi documentsModuleApi, // ✅ Injetado
ILogger<ActivateProviderCommandHandler> logger
) : ICommandHandler<ActivateProviderCommand, Result>
{
public async Task<Result> HandleAsync(ActivateProviderCommand command, CancellationToken ct)
{
// Validar documentos via Documents module
var hasRequiredResult = await documentsModuleApi.HasRequiredDocumentsAsync(command.ProviderId, ct);
if (!hasRequiredResult.Value)
return Result.Failure("Provider must have all required documents before activation");
var hasVerifiedResult = await documentsModuleApi.HasVerifiedDocumentsAsync(command.ProviderId, ct);
if (!hasVerifiedResult.Value)
return Result.Failure("Provider must have verified documents before activation");
var hasPendingResult = await documentsModuleApi.HasPendingDocumentsAsync(command.ProviderId, ct);
if (hasPendingResult.Value)
return Result.Failure("Provider cannot be activated while documents are pending verification");
var hasRejectedResult = await documentsModuleApi.HasRejectedDocumentsAsync(command.ProviderId, ct);
if (hasRejectedResult.Value)
return Result.Failure("Provider cannot be activated with rejected documents");
// Ativar provider
provider.Activate(command.ActivatedBy);
await providerRepository.UpdateAsync(provider, ct);
return Result.Success();
}
}
2. IServiceCatalogsModuleApi ⏳ STUB IMPLEMENTADO¶
Localização: src/Shared/Contracts/Modules/ServiceCatalogs/IServiceCatalogsModuleApi.cs
Implementação: src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs
Métodos (3):
Task<Result<ServiceValidationResult>> ValidateServicesAsync(IReadOnlyCollection<Guid> serviceIds, CancellationToken ct);
Task<Result<ServiceInfoDto?>> GetServiceByIdAsync(Guid serviceId, CancellationToken ct);
Task<Result<List<ServiceInfoDto>>> GetServicesByCategoryAsync(Guid categoryId, CancellationToken ct);
Status: Stub implementado, aguarda integração com Provider entity (ProviderServices many-to-many table)
TODO:
- Criar tabela ProviderServices no módulo Providers
- Implementar validação de serviços ao associar provider
3. ISearchProvidersModuleApi ✅ COMPLETO¶
Localização: src/Shared/Contracts/Modules/SearchProviders/ISearchProvidersModuleApi.cs
Implementação: src/Modules/SearchProviders/Application/ModuleApi/SearchProvidersModuleApi.cs
Métodos (3):
Task<Result<ModulePagedSearchResultDto>> SearchProvidersAsync(
double latitude, double longitude, double radiusInKm, Guid[]? serviceIds,
decimal? minRating, ESubscriptionTier[]? subscriptionTiers,
int pageNumber, int pageSize, CancellationToken ct);
Task<Result> IndexProviderAsync(Guid providerId, CancellationToken ct); // ✅ NOVO (Sprint 1)
Task<Result> RemoveProviderAsync(Guid providerId, CancellationToken ct); // ✅ NOVO (Sprint 1)
Usado por:
- ✅ ProviderVerificationStatusUpdatedDomainEventHandler (Providers) - indexa/remove providers em busca
Exemplo de Uso:
// src/Modules/Providers/Infrastructure/Events/Handlers/ProviderVerificationStatusUpdatedDomainEventHandler.cs
public sealed class ProviderVerificationStatusUpdatedDomainEventHandler(
IMessageBus messageBus,
ProvidersDbContext context,
ISearchModuleApi searchModuleApi, // ✅ Injetado
ILogger<ProviderVerificationStatusUpdatedDomainEventHandler> logger
) : IEventHandler<ProviderVerificationStatusUpdatedDomainEvent>
{
public async Task HandleAsync(ProviderVerificationStatusUpdatedDomainEvent domainEvent, CancellationToken ct)
{
var provider = await context.Providers.FirstOrDefaultAsync(p => p.Id == domainEvent.AggregateId, ct);
// Integração com SearchProviders: indexar quando verificado
if (domainEvent.NewStatus == EVerificationStatus.Verified)
{
var indexResult = await searchModuleApi.IndexProviderAsync(provider.Id.Value, ct);
if (indexResult.IsFailure)
logger.LogError("Failed to index provider {ProviderId}: {Error}",
domainEvent.AggregateId, indexResult.Error);
}
// Remover do índice quando rejeitado/suspenso
else if (domainEvent.NewStatus == EVerificationStatus.Rejected ||
domainEvent.NewStatus == EVerificationStatus.Suspended)
{
var removeResult = await searchModuleApi.RemoveProviderAsync(provider.Id.Value, ct);
if (removeResult.IsFailure)
logger.LogError("Failed to remove provider {ProviderId}: {Error}",
domainEvent.AggregateId, removeResult.Error);
}
// Publicar integration event
var integrationEvent = domainEvent.ToIntegrationEvent(provider.UserId, provider.Name);
await messageBus.PublishAsync(integrationEvent, cancellationToken: ct);
}
}
4. ILocationModuleApi ✅ JÁ EXISTIA¶
Localização: src/Shared/Contracts/Modules/Locations/ILocationModuleApi.cs
Implementação: src/Modules/Locations/Application/ModuleApi/LocationModuleApi.cs
Métodos: GetAddressFromCepAsync, ValidateCepAsync, GeocodeAddressAsync
Status: Pronto para uso, não utilizado ainda (baixa prioridade)
Integration Events Implementados:¶
ProviderVerificationStatusUpdated¶
- Publicado por:
ProviderVerificationStatusUpdatedDomainEventHandler(Providers) - Consumido por: Nenhum módulo ainda (preparado para futura expansão)
- Payload: ProviderId, UserId, Name, OldStatus, NewStatus, UpdatedAt
Padrão de Implementação (Resumo):¶
1. Definir Interface em Shared/Contracts/Modules/[ModuleName]
public interface IDocumentsModuleApi : IModuleApi
{
Task<Result<bool>> HasVerifiedDocumentsAsync(Guid providerId, CancellationToken ct);
}
2. Implementar em Module/Application/ModuleApi¶
[ModuleApi("Documents", "1.0")]
public sealed class DocumentsModuleApi(IQueryDispatcher queryDispatcher) : IDocumentsModuleApi
{
public async Task<Result<bool>> HasVerifiedDocumentsAsync(Guid providerId, CancellationToken ct)
{
var query = new GetProviderDocumentsQuery(providerId);
var result = await queryDispatcher.QueryAsync<GetProviderDocumentsQuery, Result<List<DocumentDto>>>(query, ct);
return Result.Success(result.Value?.Any(d => d.Status == EDocumentStatus.Verified) ?? false);
}
}
3. Registrar em DI (Module/Application/Extensions.cs)¶
4. Injetar e Usar em Outro Módulo¶
public sealed class ActivateProviderCommandHandler(
IDocumentsModuleApi documentsApi) // ✅ Cross-module dependency
{
public async Task<Result> HandleAsync(...)
{
var hasVerified = await documentsApi.HasVerifiedDocumentsAsync(providerId, ct);
if (!hasVerified.Value)
return Result.Failure("Documents not verified");
}
}
Benefícios Alcançados:¶
✅ Type-Safe: Contratos bem definidos em Shared/Contracts
✅ Testável: Fácil mockar IModuleApi em unit tests
✅ Desacoplado: Módulos não conhecem implementação interna de outros
✅ Versionado: Atributo [ModuleApi] permite versionamento
✅ Observável: Logging integrado em todas as operações
✅ Resiliente: Result pattern para error handling consistente
Próximos Passos (Sprint 2):¶
- Implementar full provider data sync (IndexProviderAsync com dados completos)
- Criar IProvidersModuleApi para SearchProviders consumir
- Implementar ProviderServices many-to-many table
- Integrar IServiceCatalogsModuleApi em Provider lifecycle
- Adicionar integration event handlers entre módulos
💡 Exemplos Conceituais de Implementação¶
A seguir, exemplos de como implementar os dois padrões de comunicação.
1. Exemplo de IModuleApi (Comunicação Síncrona)¶
Cenário: Ao criar um novo Provider, o módulo Providers precisa verificar se o UserId associado já existe no módulo Users.
Passos de Implementação:
- Injetar
IUsersModuleApi: NoCreateProviderCommandHandlerdo móduloProviders, injete a interfaceIUsersModuleApi. - Chamar o Método da API: Utilize o método
CheckUserExistsAsyncpara validar a existência do usuário.
Exemplo de Código (Conceitual):
// Local: C:\Code\MeAjudaAi\src\Modules\Providers\Application\Providers\Commands\CreateProvider\CreateProviderCommandHandler.cs
// 1. Injetar a IUsersModuleApi
public class CreateProviderCommandHandler(IUsersModuleApi usersModuleApi, /* outras dependências */)
: IRequestHandler<CreateProviderCommand, Result<ProviderDto>>
{
public async Task<Result<ProviderDto>> Handle(CreateProviderCommand request, CancellationToken cancellationToken)
{
// 2. Chamar a API para verificar se o usuário existe
var userExistsResult = await _usersModuleApi.CheckUserExistsAsync(request.UserId, cancellationToken);
if (userExistsResult.IsFailure || !userExistsResult.Value)
{
return Result.Failure<ProviderDto>(new Error("User.NotFound", "O usuário especificado não existe."));
}
// --- Lógica para criação do provider ---
// ...
}
}
2. Exemplo de IIntegrationEvent (Comunicação Assíncrona)¶
Cenário: Quando um novo usuário se registra, o módulo Users publica um UserRegisteredIntegrationEvent. O módulo Search escuta este evento para indexar o novo usuário em seu sistema de busca.
Passos de Implementação:
A. Publicando o Evento (Módulo Users)
- Injetar
IMessageBus: NoCreateUserCommandHandler, injete o serviço de message bus. - Publicar o Evento: Após criar o usuário com sucesso, publique o evento no barramento.
Exemplo de Código (Publicador):
// Local: C:\Code\MeAjudaAi\src\Modules\Users\Application\Users\Commands\CreateUser\CreateUserCommandHandler.cs
// 1. Injetar o message bus
public class CreateUserCommandHandler(IMessageBus messageBus, /* outras dependências */)
: IRequestHandler<CreateUserCommand, Result<UserDto>>
{
public async Task<Result<UserDto>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
// --- Lógica para criar o usuário ---
var user = new User(/* ... */);
await _userRepository.AddAsync(user, cancellationToken);
// 2. Criar e publicar o evento de integração
var integrationEvent = new UserRegisteredIntegrationEvent(
user.Id.Value,
user.Username.Value,
user.Email.Value,
user.CreatedAt
);
await _messageBus.PublishAsync(integrationEvent, cancellationToken);
return Result.Success(user.ToDto());
}
}
B. Consumindo o Evento (Módulo Search)
- Criar um Event Handler: No módulo
Search, crie uma classe que implementaIEventHandler<UserRegisteredIntegrationEvent>. - Implementar a Lógica: No método
HandleAsync, implemente a lógica para indexar o usuário. - Registrar o Handler: Adicione o handler no contêiner de injeção de dependência do módulo
Search.
Exemplo de Código (Consumidor):
// Local: C:\Code\MeAjudaAi\src\Modules\SearchProviders\Application\EventHandlers\UserRegisteredIntegrationEventHandler.cs
// 1. Criar o handler
public class UserRegisteredIntegrationEventHandler : IEventHandler<UserRegisteredIntegrationEvent>
{
private readonly ISearchIndexer _searchIndexer;
public UserRegisteredIntegrationEventHandler(ISearchIndexer searchIndexer)
{
_searchIndexer = searchIndexer;
}
// 2. Implementar a lógica de tratamento
public async Task HandleAsync(UserRegisteredIntegrationEvent @event, CancellationToken cancellationToken)
{
var userDocument = new SearchableUser
{
Id = @event.UserId,
Username = @event.Username,
Email = @event.Email
};
await _searchIndexer.IndexUserAsync(userDocument, cancellationToken);
}
}
// Local: C:\Code\MeAjudaAi\src\Modules\SearchProviders\Infrastructure\Extensions.cs
public static IServiceCollection AddSearchInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
// ... outras configurações
// 3. Registrar o handler
services.AddScoped<IEventHandler<UserRegisteredIntegrationEvent>, UserRegisteredIntegrationEventHandler>();
return services;
}
📡 API Collections e Documentação¶
Estratégia Multi-Formato¶
O projeto utiliza múltiplos formatos de collections para diferentes necessidades:
1. OpenAPI/Swagger (PRINCIPAL)¶
- 🎯 Documentação oficial gerada automaticamente do código
- 🔄 Sempre atualizada com o código fonte
- 🌐 Padrão da indústria para APIs REST
- 📊 UI interativa disponível em
/api-docs
// Endpoints automaticamente documentados
[HttpPost("register")]
[ProducesResponseType<RegisterUserResponse>(201)]
[ProducesResponseType<ApiErrorResponse>(400)]
public async Task<IActionResult> RegisterUser([FromBody] RegisterUserCommand command)
{
// Implementação...
}
2. Bruno Collections (.bru) - DESENVOLVIMENTO¶
- ✅ Controle de versão no Git
- ✅ Leve e eficiente para desenvolvedores
- ✅ Variáveis de ambiente configuráveis
- ✅ Scripts pré/pós-request em JavaScript
# Estrutura Bruno
src/Shared/API.Collections/
├── Common/
│ ├── GlobalVariables.bru
│ ├── StandardHeaders.bru
│ └── EnvironmentVariables.bru
├── Setup/
│ ├── SetupGetKeycloakToken.bru
│ └── HealthCheckAll.bru
└── Modules/
└── Users/
├── CreateUser.bru
├── GetUsers.bru
└── UpdateUser.bru
`$([System.Environment]::NewLine)
- 🤝 **Compartilhamento fácil** com QA, PO, clientes
- 🔄 **Geração automática** via OpenAPI
- 🧪 **Testes automáticos** integrados
- 📊 **Monitoring e reports** nativos
### **Geração Automática de Collections**
#### **Comandos Disponíveis**
`ash
# Gerar todas as collections
cd tools/api-collections
./generate-all-collections.sh # Linux/Mac
./generate-all-collections.bat # Windows
# Apenas Postman
npm run generate:postman
# Validar collections
npm run validate
Estrutura de Output¶
src/Shared/API.Collections/Generated/
├── MeAjudaAi-API-Collection.json # Collection principal
├── MeAjudaAi-development-Environment.json # Ambiente desenvolvimento
├── MeAjudaAi-production-Environment.json # Ambiente produção
└── README.md # Instruções de uso
Configurações Avançadas do Swagger¶
Filtros Personalizados¶
Melhorias Implementadas¶
- 🔒 Segurança JWT: Configuração automática de Bearer tokens
- 📊 Schemas Reutilizáveis: Componentes comuns (paginação, erros)
- 🌍 Multi-ambiente: URLs para dev/production
Boas Práticas para Collections¶
✅ RECOMENDADO¶
- Manter OpenAPI como fonte única da verdade
- Bruno para desenvolvimento diário
- Postman para colaboração e testes
- Regenerar collections após mudanças na API
- Versionar Bruno collections no Git
❌ EVITAR¶
- Edição manual de Postman collections geradas
- Duplicação de documentação entre formatos
- Collections desatualizadas sem regeneração
- Hardcoding de URLs nos collections
Workflow Recomendado¶
- Desenvolver API com documentação OpenAPI
- Testar localmente com Bruno collections
- Gerar Postman collections para colaboração
- Compartilhar com equipe via Postman workspace
- Regenerar collections em cada release
Exportação OpenAPI para Clientes REST¶
Comando Único¶
`ash
Gera especificação OpenAPI completa¶
.\scripts\export-openapi.ps1 -OutputPath "api/api-spec.json" ```
Características:
- ✅ Funciona offline (não precisa rodar aplicação)
- ✅ Health checks incluídos (/health, /health/ready, /health/live)
- ✅ Schemas com exemplos realistas
- ✅ Múltiplos ambientes (dev, production)
- ⚠️ Arquivo não versionado (incluído no .gitignore)
Importar em Clientes de API¶
APIDog: Importar → Do Arquivo → Selecionar arquivo
Postman: Importar → Arquivo → Fazer Envio de Arquivos → Selecionar arquivo
Insomnia: Import/Export → Import Data → Selecionar arquivo
Bruno: Import → OpenAPI → Selecionar arquivo
Thunder Client: Import → OpenAPI → Selecionar arquivo
Monitoramento e Testes¶
Especificação OpenAPI inclui:
- ✅ Health endpoints para monitoramento
- ✅ Schemas de erro padronizados
- ✅ Paginação consistente
- ✅ Exemplos realistas para desenvolvimento
- ✅ Documentação rica com descrições detalhadas
json
// Health check response example
{
"status": "Saudável",
"timestamp": "2024-01-15T10:30:00Z",
"version": "1.0.0",
"environment": "Development",
"checks": {
"database": { "status": "Healthy", "duration": "00:00:00.0123456" },
"cache": { "status": "Healthy", "duration": "00:00:00.0087432" }
}
}text
📖 Próximos Passos: Este documento serve como base para o desenvolvimento. Consulte também a documentação de infraestrutura e guia de CI/CD para informações complementares.