Skip to main content

Overview

This guide covers how to extend the API with new features, add endpoints, implement service methods, and follow best practices for development.

Development Workflow

1

Make changes to the code

Edit controllers, services, or models
2

Test your changes

Run the application and test via Scalar UI or curl
3

Verify in documentation

Check that OpenAPI documentation updates automatically
4

Commit your changes

Use Git to commit and push your work

Hot Reload for Fast Development

Use dotnet watch for automatic reloading:
dotnet watch run
Now when you save changes to .cs files, the application automatically recompiles and restarts.
Hot reload works for most changes but requires a full restart for:
  • Changes to Program.cs
  • Adding new NuGet packages
  • Changes to appsettings.json

Implementing Service Methods

Currently, service implementations throw NotImplementedException. Let’s implement them.

Example: Implementing 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)
    {
        // Validate unique DNI
        if (_pacientes.Any(p => p.Dni == pacienteDto.Dni))
            throw new InvalidOperationException($"Ya existe un paciente con DNI {pacienteDto.Dni}");

        // Validate unique email
        if (_pacientes.Any(p => p.Email == pacienteDto.Email))
            throw new InvalidOperationException($"Ya existe un paciente con email {pacienteDto.Email}");

        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);

        // Check if DNI is changing and already exists
        if (paciente.Dni != pacienteDto.Dni && _pacientes.Any(p => p.Dni == pacienteDto.Dni))
            throw new InvalidOperationException($"Ya existe un paciente con DNI {pacienteDto.Dni}");

        // Check if email is changing and already exists
        if (paciente.Email != pacienteDto.Email && _pacientes.Any(p => p.Email == pacienteDto.Email))
            throw new InvalidOperationException($"Ya existe un paciente con email {pacienteDto.Email}");

        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;
    }
}

Implementing CitaService with Business Logic

Services/CitaService.cs
public class CitaService : ICitaService
{
    private readonly List<Cita> _citas = [];
    private int _nextId = 1;
    private readonly IPacienteService _pacienteService;
    private readonly IMedicoService _medicoService;

    public CitaService(IPacienteService pacienteService, IMedicoService medicoService)
    {
        _pacienteService = pacienteService;
        _medicoService = medicoService;
    }

    public Cita AgregarCita(CreateCitaDto citaDto)
    {
        // Validate patient exists
        _ = _pacienteService.ObtenerPaciente(citaDto.PacienteId);

        // Validate doctor exists
        _ = _medicoService.ObtenerMedico(citaDto.MedicoId);

        // Validate dates
        if (citaDto.FechaInicio >= citaDto.FechaFin)
            throw new InvalidOperationException("La fecha de inicio debe ser anterior a la fecha de fin");

        if (citaDto.FechaInicio < DateTime.Now)
            throw new InvalidOperationException("No se pueden crear citas en el pasado");

        // Check for scheduling conflicts
        var conflicto = _citas.Any(c =>
            c.MedicoId == citaDto.MedicoId &&
            c.Estado != CitaEstado.Cancelada &&
            (
                (citaDto.FechaInicio >= c.FechaInicio && citaDto.FechaInicio < c.FechaFin) ||
                (citaDto.FechaFin > c.FechaInicio && citaDto.FechaFin <= c.FechaFin) ||
                (citaDto.FechaInicio <= c.FechaInicio && citaDto.FechaFin >= c.FechaFin)
            )
        );

        if (conflicto)
            throw new InvalidOperationException("El médico no está disponible en ese horario");

        var cita = new Cita
        {
            Id = _nextId++,
            PacienteId = citaDto.PacienteId,
            MedicoId = citaDto.MedicoId,
            Costo = citaDto.Costo,
            Motivo = citaDto.Motivo,
            FechaInicio = citaDto.FechaInicio,
            FechaFin = citaDto.FechaFin,
            Observaciones = citaDto.Observaciones,
            Estado = citaDto.Estado
        };

        _citas.Add(cita);
        return cita;
    }

    // ... other methods
}
When CitaService depends on other services, update the registration in Program.cs to inject dependencies:
builder.Services.AddSingleton<IPacienteService, PacienteService>();
builder.Services.AddSingleton<IMedicoService, MedicoService>();
builder.Services.AddSingleton<IEspecialidadService, EspecialidadService>();
builder.Services.AddSingleton<ICitaService>(sp => new CitaService(
    sp.GetRequiredService<IPacienteService>(),
    sp.GetRequiredService<IMedicoService>()
));

Adding New Endpoints

Step 1: Update the Controller

Add HTTP endpoint methods to controllers:
Controllers/PacienteController.cs
using Microsoft.AspNetCore.Mvc;
using preliminarServicios.Models.Dtos;
using preliminarServicios.Models.Entities;
using preliminarServicios.Services;

namespace preliminarServicios.Controllers;

[Route("api/[controller]")]
[ApiController]
public class PacienteController : ControllerBase
{
    private readonly IPacienteService _pacienteService;

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

    [HttpGet]
    public ActionResult<List<Paciente>> GetPacientes()
    {
        return Ok(_pacienteService.ObtenerPacientes());
    }

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

    [HttpPost]
    public ActionResult<Paciente> CreatePaciente(CreatePacienteDto pacienteDto)
    {
        try
        {
            var paciente = _pacienteService.AgregarPaciente(pacienteDto);
            return CreatedAtAction(nameof(GetPaciente), new { id = paciente.Id }, paciente);
        }
        catch (InvalidOperationException ex)
        {
            return BadRequest(ex.Message);
        }
    }

    [HttpPut("{id}")]
    public ActionResult<Paciente> UpdatePaciente(int id, CreatePacienteDto pacienteDto)
    {
        try
        {
            var paciente = _pacienteService.ActualizarPaciente(id, pacienteDto);
            return Ok(paciente);
        }
        catch (KeyNotFoundException)
        {
            return NotFound();
        }
        catch (InvalidOperationException ex)
        {
            return BadRequest(ex.Message);
        }
    }

    [HttpDelete("{id}")]
    public ActionResult DeletePaciente(int id)
    {
        try
        {
            _pacienteService.EliminarPaciente(id);
            return NoContent();
        }
        catch (KeyNotFoundException)
        {
            return NotFound();
        }
    }
}

Step 2: Verify OpenAPI Documentation

The OpenAPI specification updates automatically. Visit:
https://localhost:5001/scalar/v1
Your new endpoints will appear in the documentation.

Adding Custom Endpoints

Example: Get Appointments by Patient

Add a custom endpoint to get all appointments for a specific patient:
Controllers/CitaController.cs
[HttpGet("paciente/{pacienteId}")]
public ActionResult<List<Cita>> GetCitasByPaciente(int pacienteId)
{
    var citas = _citaService.ObtenerCitas()
        .Where(c => c.PacienteId == pacienteId)
        .ToList();
    return Ok(citas);
}

Example: Get Appointments by Status

[HttpGet("estado/{estado}")]
public ActionResult<List<Cita>> GetCitasByEstado(CitaEstado estado)
{
    var citas = _citaService.ObtenerCitas()
        .Where(c => c.Estado == estado)
        .ToList();
    return Ok(citas);
}

Example: Cancel Appointment

[HttpPost("{id}/cancelar")]
public ActionResult<Cita> CancelarCita(int id)
{
    try
    {
        var cita = _citaService.ObtenerCita(id);
        
        if (cita.Estado == CitaEstado.Completada)
            return BadRequest("No se puede cancelar una cita completada");
        
        if (cita.Estado == CitaEstado.Cancelada)
            return BadRequest("La cita ya está cancelada");
        
        cita.Estado = CitaEstado.Cancelada;
        return Ok(cita);
    }
    catch (KeyNotFoundException)
    {
    return NotFound();
    }
}

Adding Data Validation

Using Data Annotations

Add validation attributes to DTOs:
Models/Dtos/CreatePacienteDto.cs
using System.ComponentModel.DataAnnotations;

public class CreatePacienteDto
{
    [Required(ErrorMessage = "El nombre es obligatorio")]
    [StringLength(100, MinimumLength = 2, ErrorMessage = "El nombre debe tener entre 2 y 100 caracteres")]
    public required string Nombre { get; set; }

    [Required(ErrorMessage = "El apellido es obligatorio")]
    [StringLength(100, MinimumLength = 2)]
    public required string Apellido { get; set; }

    [Required(ErrorMessage = "El DNI es obligatorio")]
    [RegularExpression(@"^\d{8}[A-Z]$", ErrorMessage = "El DNI debe tener 8 dígitos seguidos de una letra mayúscula")]
    public required string Dni { get; set; }

    [Required(ErrorMessage = "El email es obligatorio")]
    [EmailAddress(ErrorMessage = "Email inválido")]
    public required string Email { get; set; }

    [Phone(ErrorMessage = "Teléfono inválido")]
    public string? Telefono { get; set; }

    [Required]
    public DateOnly FechaNacimiento { get; set; }
}

Custom Validation Attributes

Create custom validators:
Validation/EdadMinimaAttribute.cs
using System.ComponentModel.DataAnnotations;

public class EdadMinimaAttribute : ValidationAttribute
{
    private readonly int _edadMinima;

    public EdadMinimaAttribute(int edadMinima)
    {
        _edadMinima = edadMinima;
    }

    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        if (value is DateOnly fechaNacimiento)
        {
            var edad = DateOnly.FromDateTime(DateTime.Now).Year - fechaNacimiento.Year;
            if (edad < _edadMinima)
                return new ValidationResult($"El paciente debe tener al menos {_edadMinima} años");
        }
        return ValidationResult.Success;
    }
}
Use it:
[EdadMinima(18, ErrorMessage = "El paciente debe ser mayor de edad")]
public DateOnly FechaNacimiento { get; set; }

Adding Logging

Use ASP.NET Core’s built-in logging:
Controllers/PacienteController.cs
public class PacienteController : ControllerBase
{
    private readonly IPacienteService _pacienteService;
    private readonly ILogger<PacienteController> _logger;

    public PacienteController(IPacienteService pacienteService, ILogger<PacienteController> logger)
    {
        _pacienteService = pacienteService;
        _logger = logger;
    }

    [HttpPost]
    public ActionResult<Paciente> CreatePaciente(CreatePacienteDto pacienteDto)
    {
        _logger.LogInformation("Creando paciente con DNI: {Dni}", pacienteDto.Dni);
        
        try
        {
            var paciente = _pacienteService.AgregarPaciente(pacienteDto);
            _logger.LogInformation("Paciente creado con ID: {Id}", paciente.Id);
            return CreatedAtAction(nameof(GetPaciente), new { id = paciente.Id }, paciente);
        }
        catch (InvalidOperationException ex)
        {
            _logger.LogWarning("Error al crear paciente: {Message}", ex.Message);
            return BadRequest(ex.Message);
        }
    }
}

Migrating to a Database

Step 1: Install Entity Framework Core

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

Step 2: Create DbContext

Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using preliminarServicios.Models.Entities;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Configure relationships
        modelBuilder.Entity<Cita>()
            .HasOne<Paciente>()
            .WithMany()
            .HasForeignKey(c => c.PacienteId);

        modelBuilder.Entity<Cita>()
            .HasOne<Medico>()
            .WithMany()
            .HasForeignKey(c => c.MedicoId);

        modelBuilder.Entity<Medico>()
            .HasOne<Especialidad>()
            .WithMany()
            .HasForeignKey(m => m.EspecialidadId);
    }
}

Step 3: Configure in Program.cs

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Change from Singleton to Scoped
builder.Services.AddScoped<IPacienteService, PacienteService>();
builder.Services.AddScoped<IMedicoService, MedicoService>();
builder.Services.AddScoped<IEspecialidadService, EspecialidadService>();
builder.Services.AddScoped<ICitaService, CitaService>();

Step 4: Add Connection String

appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MedicalAppointments;Trusted_Connection=true;MultipleActiveResultSets=true"
  },
  "Logging": { /* ... */ }
}

Step 5: Update Service Implementation

public class PacienteService : IPacienteService
{
    private readonly AppDbContext _context;

    public PacienteService(AppDbContext context)
    {
        _context = context;
    }

    public List<Paciente> ObtenerPacientes()
    {
        return _context.Pacientes.ToList();
    }

    public Paciente AgregarPaciente(CreatePacienteDto dto)
    {
        var paciente = new Paciente { /* map properties */ };
        _context.Pacientes.Add(paciente);
        _context.SaveChanges();
        return paciente;
    }
}

Step 6: Create Migrations

dotnet ef migrations add InitialCreate
dotnet ef database update

Best Practices

Use Dependency Injection

Always inject dependencies via constructors, never use new

Validate Input

Use Data Annotations and service-layer validation

Handle Errors Properly

Catch specific exceptions and return appropriate HTTP status codes

Log Important Events

Use ILogger to track operations and errors

Keep Controllers Thin

Move all business logic to services

Use DTOs

Separate API contracts from internal models

Next Steps

Testing Guide

Learn how to test your API

Architecture

Understand the overall architecture

Services

Deep dive into service patterns

API Reference

Browse all available endpoints

Build docs developers (and LLMs) love