Skip to main content

Service Layer Overview

The Medical Appointment Management API uses a service-oriented architecture where the service layer encapsulates all business logic and data operations. This design keeps controllers thin and focused solely on HTTP concerns.

Why Use Services?

Separation of Concerns

  • Controllers handle HTTP - routing, request/response formatting, status codes
  • Services handle business logic - validation, data manipulation, business rules

Benefits

Testability

Services can be tested independently without HTTP infrastructure

Reusability

Business logic can be shared across multiple controllers or consumers

Maintainability

Changes to business logic don’t affect HTTP layer

Single Responsibility

Each service focuses on one domain entity

Service Architecture

Each service follows an interface-based design:
IService (interface) ← Service (implementation)


    Controller
Controllers depend on interfaces, not concrete implementations. This enables:
  • Dependency injection
  • Easy mocking for tests
  • Flexibility to swap implementations

Service Interfaces

All service interfaces are located in the Services/ directory and follow the naming pattern I{Entity}Service.

IPacienteService

Defines operations for patient management.
Services/IPacienteService.cs
using System;
using preliminarServicios.Models.Dtos;
using preliminarServicios.Models.Entities;

namespace preliminarServicios.Services;

public interface IPacienteService
{
    List<Paciente> ObtenerPacientes();
    Paciente AgregarPaciente(CreatePacienteDto paciente);
    Paciente ObtenerPaciente(int id);
    void EliminarPaciente(int id);
    Paciente ActualizarPaciente(int id, CreatePacienteDto paciente);
}
Methods:
MethodReturnsDescription
ObtenerPacientes()List<Paciente>Retrieve all patients
AgregarPaciente(dto)PacienteCreate a new patient
ObtenerPaciente(id)PacienteGet patient by ID
ActualizarPaciente(id, dto)PacienteUpdate patient information
EliminarPaciente(id)voidDelete a patient

IMedicoService

Defines operations for doctor management.
Services/IMedicoService.cs
public interface IMedicoService
{
    List<Medico> ObtenerMedicos();
    Medico AgregarMedico(CreateMedicoDto medico);
    Medico ObtenerMedico(int id);
    void EliminarMedico(int id);
    Medico ActualizarMedico(int id, CreateMedicoDto medico);
}

IEspecialidadService

Defines operations for specialty management.
Services/IEspecialidadService.cs
public interface IEspecialidadService
{
    List<Especialidad> ObtenerEspecialidades();
    Especialidad AgregarEspecialidad(CreateEspecialidadDto especialidad);
    Especialidad ObtenerEspecialidad(int id);
    void EliminarEspecialidad(int id);
    Especialidad ActualizarEspecialidad(int id, CreateEspecialidadDto especialidad);
}

ICitaService

Defines operations for appointment management.
Services/ICitaService.cs
public interface ICitaService
{
    List<Cita> ObtenerCitas();
    Cita AgregarCita(CreateCitaDto cita);
    Cita ObtenerCita(int id);
    void EliminarCita(int id);
    Cita ActualizarCita(int id, CreateCitaDto cita);
}

Service Implementations

Service implementations are located in Services/{Entity}Service.cs.

Implementation Pattern

Each service implementation:
  1. Maintains in-memory storage:
    private readonly List<Paciente> _pacientes = [];
    private int _nextId = 1;
    
  2. Implements CRUD operations:
    • Create: Add to list, assign ID
    • Read: Find by ID or return all
    • Update: Find by ID, update properties
    • Delete: Remove from list
  3. Handles business logic:
    • ID generation
    • Validation
    • Data transformation (DTO → Entity)

Example: PacienteService

Services/PacienteService.cs
using System;
using preliminarServicios.Models.Dtos;
using preliminarServicios.Models.Entities;

namespace preliminarServicios.Services;

public class PacienteService : IPacienteService
{
    private readonly List<Paciente> _pacientes = [];
    private int _nextId = 1;

    public List<Paciente> ObtenerPacientes()
    {
        return _pacientes;
    }

    public Paciente AgregarPaciente(CreatePacienteDto pacienteDto)
    {
        var paciente = new Paciente
        {
            Id = _nextId++,
            Nombre = pacienteDto.Nombre,
            Apellido = pacienteDto.Apellido,
            Dni = pacienteDto.Dni,
            Email = pacienteDto.Email,
            Telefono = pacienteDto.Telefono,
            FechaNacimiento = pacienteDto.FechaNacimiento
        };
        
        _pacientes.Add(paciente);
        return paciente;
    }

    public Paciente ObtenerPaciente(int id)
    {
        var paciente = _pacientes.FirstOrDefault(p => p.Id == id);
        if (paciente == null)
            throw new KeyNotFoundException($"Paciente con ID {id} no encontrado");
        return paciente;
    }

    public void EliminarPaciente(int id)
    {
        var paciente = ObtenerPaciente(id);
        _pacientes.Remove(paciente);
    }

    public Paciente ActualizarPaciente(int id, CreatePacienteDto pacienteDto)
    {
        var paciente = ObtenerPaciente(id);
        
        paciente.Nombre = pacienteDto.Nombre;
        paciente.Apellido = pacienteDto.Apellido;
        paciente.Dni = pacienteDto.Dni;
        paciente.Email = pacienteDto.Email;
        paciente.Telefono = pacienteDto.Telefono;
        paciente.FechaNacimiento = pacienteDto.FechaNacimiento;
        
        return paciente;
    }
}
The actual implementation in the source code currently throws NotImplementedException placeholders. The example above shows the intended implementation pattern.

Dependency Injection

Service Registration

Services are registered in Program.cs using ASP.NET Core’s dependency injection container:
Program.cs
using preliminarServicios.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddOpenApi();

// Register services as singletons
builder.Services.AddSingleton<IPacienteService, PacienteService>();
builder.Services.AddSingleton<IMedicoService, MedicoService>();
builder.Services.AddSingleton<IEspecialidadService, EspecialidadService>();
builder.Services.AddSingleton<ICitaService, CitaService>();

Service Lifetimes

ASP.NET Core supports three service lifetimes:

Singleton (Current Implementation)

builder.Services.AddSingleton<IService, ServiceImpl>();
  • One instance for the entire application lifetime
  • Same instance shared across all requests
  • In-memory data persists until app restart
Current Choice: The API uses singletons to maintain in-memory data across requests. This is suitable for development/prototyping but NOT for production.
builder.Services.AddScoped<IService, ServiceImpl>();
  • New instance per HTTP request
  • Shared within the request scope
  • Ideal for database contexts (Entity Framework)
When adding a database with Entity Framework Core, switch to scoped services:
builder.Services.AddScoped<IPacienteService, PacienteService>();

Transient

builder.Services.AddTransient<IService, ServiceImpl>();
  • New instance every time it’s requested
  • No state is shared
  • Use for lightweight, stateless services

Constructor Injection

Controllers receive services via constructor injection:
public class PacienteController : ControllerBase
{
    private readonly IPacienteService _pacienteService;

    public PacienteController(IPacienteService pacienteService)
    {
        _pacienteService = pacienteService;
    }

    [HttpGet]
    public ActionResult<List<Paciente>> GetPacientes()
    {
        return Ok(_pacienteService.ObtenerPacientes());
    }
}
Benefits:
  • No manual instantiation
  • Easy to mock for testing
  • Framework manages lifetimes
  • Clear dependencies

Data Transformations

DTO to Entity

Services transform DTOs to entities when creating/updating:
public Paciente AgregarPaciente(CreatePacienteDto dto)
{
    var paciente = new Paciente
    {
        Id = _nextId++,
        Nombre = dto.Nombre,
        Apellido = dto.Apellido,
        Dni = dto.Dni,
        Email = dto.Email,
        Telefono = dto.Telefono,
        FechaNacimiento = dto.FechaNacimiento
    };
    
    _pacientes.Add(paciente);
    return paciente;
}

Entity to Response DTO

For complex responses, services can return enriched DTOs:
public CitaDto ObtenerCitaDetallada(int id)
{
    var cita = ObtenerCita(id);
    var paciente = _pacienteService.ObtenerPaciente(cita.PacienteId);
    var medico = _medicoService.ObtenerMedico(cita.MedicoId);
    
    return new CitaDto
    {
        Id = cita.Id,
        Paciente = new PacienteResumenDto
        {
            Id = paciente.Id,
            Nombre = paciente.Nombre,
            Apellido = paciente.Apellido
        },
        Medico = new MedicoResumenDto
        {
            Id = medico.Id,
            Nombre = medico.Nombre,
            Apellido = medico.Apellido,
            NumeroLicencia = medico.NumeroLicencia
        },
        Costo = cita.Costo,
        Motivo = cita.Motivo,
        FechaInicio = cita.FechaInicio,
        FechaFin = cita.FechaFin,
        Observaciones = cita.Observaciones,
        Estado = cita.Estado
    };
}

Business Logic Examples

Validation

public Paciente AgregarPaciente(CreatePacienteDto dto)
{
    // Check for duplicate DNI
    if (_pacientes.Any(p => p.Dni == dto.Dni))
        throw new InvalidOperationException($"Ya existe un paciente con DNI {dto.Dni}");
    
    // Check for duplicate email
    if (_pacientes.Any(p => p.Email == dto.Email))
        throw new InvalidOperationException($"Ya existe un paciente con email {dto.Email}");
    
    // Validate age
    var age = DateOnly.FromDateTime(DateTime.Now).Year - dto.FechaNacimiento.Year;
    if (age < 0 || age > 150)
        throw new InvalidOperationException("Fecha de nacimiento inválida");
    
    // Create entity
    var paciente = new Paciente { /* ... */ };
    _pacientes.Add(paciente);
    return paciente;
}

Appointment Scheduling Rules

public Cita AgregarCita(CreateCitaDto dto)
{
    // Validate dates
    if (dto.FechaInicio >= dto.FechaFin)
        throw new InvalidOperationException("La fecha de inicio debe ser anterior a la fecha de fin");
    
    if (dto.FechaInicio < DateTime.Now)
        throw new InvalidOperationException("No se pueden crear citas en el pasado");
    
    // Check doctor availability
    var conflictos = _citas.Where(c => 
        c.MedicoId == dto.MedicoId &&
        c.Estado != CitaEstado.Cancelada &&
        (
            (dto.FechaInicio >= c.FechaInicio && dto.FechaInicio < c.FechaFin) ||
            (dto.FechaFin > c.FechaInicio && dto.FechaFin <= c.FechaFin)
        )
    );
    
    if (conflictos.Any())
        throw new InvalidOperationException("El médico no está disponible en ese horario");
    
    // Create appointment
    var cita = new Cita { /* ... */ };
    _citas.Add(cita);
    return cita;
}

Error Handling

Services throw exceptions that controllers catch and convert to appropriate HTTP responses:

Service Layer

public Paciente ObtenerPaciente(int id)
{
    var paciente = _pacientes.FirstOrDefault(p => p.Id == id);
    if (paciente == null)
        throw new KeyNotFoundException($"Paciente con ID {id} no encontrado");
    return paciente;
}

Controller Layer

[HttpGet("{id}")]
public ActionResult<Paciente> GetPaciente(int id)
{
    try
    {
        return Ok(_pacienteService.ObtenerPaciente(id));
    }
    catch (KeyNotFoundException)
    {
        return NotFound();
    }
}

Testing Services

Interface-based design makes services easy to test:
public class PacienteServiceTests
{
    [Fact]
    public void AgregarPaciente_CreatesPatientWithId()
    {
        // Arrange
        var service = new PacienteService();
        var dto = new CreatePacienteDto
        {
            Nombre = "Test",
            Apellido = "User",
            Dni = "12345678A",
            Email = "[email protected]",
            FechaNacimiento = new DateOnly(1990, 1, 1)
        };
        
        // Act
        var result = service.AgregarPaciente(dto);
        
        // Assert
        Assert.NotEqual(0, result.Id);
        Assert.Equal("Test", result.Nombre);
    }
}

Migration to Database

When moving from in-memory to database storage:

1. Add Entity Framework Core

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

2. Create DbContext

public class AppDbContext : DbContext
{
    public DbSet<Paciente> Pacientes { get; set; }
    public DbSet<Medico> Medicos { get; set; }
    public DbSet<Especialidad> Especialidades { get; set; }
    public DbSet<Cita> Citas { get; set; }
}

3. Update Service Implementation

public class PacienteService : IPacienteService
{
    private readonly AppDbContext _context;
    
    public PacienteService(AppDbContext context)
    {
        _context = context;
    }
    
    public Paciente AgregarPaciente(CreatePacienteDto dto)
    {
        var paciente = new Paciente { /* map properties */ };
        _context.Pacientes.Add(paciente);
        _context.SaveChanges();
        return paciente;
    }
}

4. Update Service Registration

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services.AddScoped<IPacienteService, PacienteService>();

Next Steps

Architecture

See how services fit into the overall architecture

Development Guide

Learn how to implement service methods

API Reference

See services in action through the API

Testing Guide

Learn how to test services

Build docs developers (and LLMs) love