Guia: Crear un Nuevo Microservicio
Esta guia describe el proceso completo para crear un nuevo microservicio en la plataforma Imagy, desde la generacion del proyecto hasta el primer deployment.
Prerequisitos
- .NET 10 SDK instalado
- Docker Desktop corriendo
- Acceso al repositorio de la organizacion en GitHub
- Infraestructura local levantada (
docker-compose.infra.yml)
1. Generar el Proyecto
bash
# Crear el repositorio
mkdir imagy-{domain}
cd imagy-{domain}
# Crear la solucion
dotnet new sln -n Imagy.{Domain}
# Crear el proyecto API
dotnet new webapi -n Imagy.{Domain}.Api -o src/Imagy.{Domain}.Api
dotnet sln add src/Imagy.{Domain}.Api
# Crear el proyecto de dominio
dotnet new classlib -n Imagy.{Domain}.Domain -o src/Imagy.{Domain}.Domain
dotnet sln add src/Imagy.{Domain}.Domain
# Crear el proyecto de infraestructura
dotnet new classlib -n Imagy.{Domain}.Infrastructure -o src/Imagy.{Domain}.Infrastructure
dotnet sln add src/Imagy.{Domain}.Infrastructure
# Crear el proyecto de tests
dotnet new xunit -n Imagy.{Domain}.Tests -o tests/Imagy.{Domain}.Tests
dotnet sln add tests/Imagy.{Domain}.Tests2. Estructura del Proyecto
imagy-{domain}/
src/
Imagy.{Domain}.Api/
Program.cs
appsettings.json
appsettings.Development.json
Endpoints/
Middleware/
Dockerfile
Imagy.{Domain}.Domain/
Entities/
Events/
Interfaces/
Services/
Imagy.{Domain}.Infrastructure/
Persistence/
DbContext.cs
Migrations/
Configurations/
Messaging/
Consumers/
Publishers/
ExternalServices/
tests/
Imagy.{Domain}.Tests/
Unit/
Integration/
Contracts/
docker-compose.yml
docker-compose.infra.yml
.github/
workflows/
ci.yml
deploy.yml3. Configurar Dependency Injection
En Program.cs, configurar el pipeline completo:
csharp
var builder = WebApplication.CreateBuilder(args);
// --- Servicios de infraestructura ---
builder.Services.AddPlatformDefaults(builder.Configuration);
builder.Services.AddMultiTenancy(builder.Configuration);
builder.Services.AddJwtAuthentication(builder.Configuration);
// --- Base de datos ---
builder.Services.AddDbContext<DomainDbContext>(options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString("DefaultConnection"),
npgsql => npgsql.MigrationsAssembly(typeof(DomainDbContext).Assembly.FullName)
));
// --- Messaging (MassTransit + RabbitMQ) ---
builder.Services.AddMassTransit(x =>
{
x.AddConsumersFromNamespaceContaining<SampleEventConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(builder.Configuration.GetConnectionString("RabbitMq"));
cfg.ConfigureEndpoints(context);
});
});
// --- Health Checks ---
builder.Services.AddHealthChecks()
.AddNpgSql(builder.Configuration.GetConnectionString("DefaultConnection")!)
.AddRabbitMQ()
.AddRedis(builder.Configuration.GetConnectionString("Redis")!);
// --- Servicios de dominio ---
builder.Services.AddScoped<ISampleService, SampleService>();
var app = builder.Build();
// --- Middleware pipeline ---
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<TenantResolutionMiddleware>();
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/health/live", new() { Predicate = _ => false });
app.MapHealthChecks("/health/ready");
app.MapEndpoints();
app.Run();4. Configurar Middleware Pipeline
CorrelationIdMiddleware
csharp
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
?? Guid.NewGuid().ToString();
context.Items["CorrelationId"] = correlationId;
context.Response.Headers["X-Correlation-Id"] = correlationId;
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await _next(context);
}
}
}TenantResolutionMiddleware
csharp
public class TenantResolutionMiddleware
{
private readonly RequestDelegate _next;
public TenantResolutionMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context, DomainDbContext dbContext)
{
var tenantId = context.User.FindFirst("tenant_id")?.Value;
if (tenantId is not null)
{
dbContext.Database.ExecuteSqlRaw(
"SET app.current_tenant_id = {0}", tenantId);
}
await _next(context);
}
}5. Health Checks
El servicio debe exponer dos endpoints de salud:
| Endpoint | Proposito | Que verifica |
|---|---|---|
/health/live | Liveness probe (K8s) | Solo que el proceso responde |
/health/ready | Readiness probe (ECS) | DB, RabbitMQ, Redis disponibles |
6. Docker
Dockerfile
dockerfile
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.sln .
COPY src/Imagy.{Domain}.Api/*.csproj src/Imagy.{Domain}.Api/
COPY src/Imagy.{Domain}.Domain/*.csproj src/Imagy.{Domain}.Domain/
COPY src/Imagy.{Domain}.Infrastructure/*.csproj src/Imagy.{Domain}.Infrastructure/
RUN dotnet restore
COPY . .
RUN dotnet publish src/Imagy.{Domain}.Api -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "Imagy.{Domain}.Api.dll"]docker-compose.yml (desarrollo local)
yaml
services:
api:
build:
context: .
dockerfile: src/Imagy.{Domain}.Api/Dockerfile
ports:
- "{LOCAL_PORT}:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Host=host.docker.internal;Port=5432;Database=imagy_{domain};Username=imagy_{domain}_app;Password=dev_password
- ConnectionStrings__RabbitMq=amqp://guest:guest@host.docker.internal:5672
- ConnectionStrings__Redis=host.docker.internal:6379
depends_on:
- migrations
migrations:
build:
context: .
dockerfile: src/Imagy.{Domain}.Api/Dockerfile
command: ["dotnet", "Imagy.{Domain}.Api.dll", "--migrate"]
environment:
- ConnectionStrings__DefaultConnection=Host=host.docker.internal;Port=5432;Database=imagy_{domain};Username=imagy_{domain}_app;Password=dev_password7. Migraciones de Base de Datos
bash
# Crear la base de datos
psql -h localhost -U postgres -c "CREATE DATABASE imagy_{domain};"
psql -h localhost -U postgres -c "CREATE USER imagy_{domain}_app WITH PASSWORD 'dev_password' NOBYPASSRLS;"
psql -h localhost -U postgres -c "GRANT ALL ON DATABASE imagy_{domain} TO imagy_{domain}_app;"
# Crear primera migracion
cd src/Imagy.{Domain}.Infrastructure
dotnet ef migrations add InitialCreate --startup-project ../Imagy.{Domain}.Api
# Aplicar migracion
dotnet ef database update --startup-project ../Imagy.{Domain}.ApiHabilitar RLS en la migracion inicial:
csharp
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// Crear tablas...
// Habilitar RLS
migrationBuilder.Sql("ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY;");
migrationBuilder.Sql(@"
CREATE POLICY tenant_isolation ON {table_name}
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
");
}
}8. Primer Endpoint
Usar Minimal APIs con el patron de la plataforma:
csharp
// Endpoints/SampleEndpoints.cs
public static class SampleEndpoints
{
public static void MapSampleEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/samples")
.RequireAuthorization("operator")
.WithTags("Samples");
group.MapPost("/", CreateSample)
.WithName("CreateSample")
.Produces<ApiResponse<SampleResponse>>(201)
.Produces<ProblemDetails>(400);
group.MapGet("/{id:guid}", GetSample)
.WithName("GetSample")
.Produces<ApiResponse<SampleResponse>>(200)
.Produces(404);
}
private static async Task<IResult> CreateSample(
CreateSampleRequest request,
ISampleService service,
HttpContext context,
CancellationToken ct)
{
var result = await service.CreateAsync(request, ct);
return Results.Created($"/api/v1/samples/{result.Id}", new ApiResponse<SampleResponse>(result));
}
private static async Task<IResult> GetSample(
Guid id,
ISampleService service,
CancellationToken ct)
{
var result = await service.GetByIdAsync(id, ct);
return result is null ? Results.NotFound() : Results.Ok(new ApiResponse<SampleResponse>(result));
}
}Registrar en Program.cs:
csharp
app.MapSampleEndpoints();9. Primer Evento
Definir el evento (Domain)
csharp
// Domain/Events/SampleCreatedEvent.cs
public record SampleCreatedEvent
{
public Guid EventId { get; init; } = Guid.NewGuid();
public string EventType => "domain.sample.created";
public string Version => "1.0";
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
public Guid CorrelationId { get; init; }
public Guid TenantId { get; init; }
public required SampleCreatedData Data { get; init; }
}
public record SampleCreatedData
{
public required Guid SampleId { get; init; }
public required string Name { get; init; }
}Publicar el evento
csharp
// Domain/Services/SampleService.cs
public class SampleService : ISampleService
{
private readonly IPublishEndpoint _publishEndpoint;
public async Task<SampleResponse> CreateAsync(CreateSampleRequest request, CancellationToken ct)
{
// ... crear entidad ...
await _publishEndpoint.Publish(new SampleCreatedEvent
{
CorrelationId = _correlationId,
TenantId = _tenantId,
Data = new SampleCreatedData
{
SampleId = entity.Id,
Name = entity.Name
}
}, ct);
return response;
}
}Consumir en otro servicio
csharp
// Infrastructure/Messaging/Consumers/SampleCreatedConsumer.cs
public class SampleCreatedConsumer : IConsumer<SampleCreatedEvent>
{
public async Task Consume(ConsumeContext<SampleCreatedEvent> context)
{
var message = context.Message;
// Procesar evento...
}
}10. Primer Test
csharp
// tests/Imagy.{Domain}.Tests/Integration/SampleEndpointTests.cs
public class SampleEndpointTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public SampleEndpointTests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Reemplazar DB con Testcontainers PostgreSQL
// Reemplazar RabbitMQ con InMemory
});
}).CreateClient();
}
[Fact]
public async Task CreateSample_WithValidData_Returns201()
{
// Arrange
var request = new { name = "Test Sample" };
// Act
var response = await _client.PostAsJsonAsync("/api/v1/samples", request);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var body = await response.Content.ReadFromJsonAsync<ApiResponse<SampleResponse>>();
body!.Data.Name.Should().Be("Test Sample");
}
}11. Checklist Final
- [ ] Proyecto compila sin errores (
dotnet build) - [ ] Tests pasan (
dotnet test) - [ ] Docker image se construye (
docker build .) - [ ] Health checks responden (
/health/live,/health/ready) - [ ] Migraciones se aplican correctamente
- [ ] RLS habilitado en todas las tablas con
tenant_id - [ ] Eventos siguen el envelope estandar de la plataforma
- [ ] Endpoints documentados con OpenAPI/Swagger
- [ ] CI pipeline configurado (
.github/workflows/ci.yml) - [ ] README.md con instrucciones de desarrollo local
Referencias
- Arquitectura General — Patrones y principios
- Estrategia de Datos — RLS, migraciones, naming
- Comunicacion — Eventos, MassTransit, RabbitMQ
- Infraestructura — Docker, ECS, deployment
- Multi-tenancy — Aislamiento por tenant