Skip to content

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}.Tests

2. 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.yml

3. 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:

EndpointPropositoQue verifica
/health/liveLiveness probe (K8s)Solo que el proceso responde
/health/readyReadiness 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_password

7. 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}.Api

Habilitar 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

Reimagine Tech LLC — Documentacion Interna