En tres lecciones distintas del Módulo 7 —ASP.NET Core, Blazor y MAUI— apareció el mismo mensaje de pasada: "esto es inyección de dependencias, se estudiará en profundidad en el Módulo 8". Ha llegado ese momento. Esta lección explica primero el problema de fondo que resuelve la inversión de control (acoplar código a implementaciones concretas dificulta el cambio y, sobre todo, dificulta las pruebas), después compara las distintas formas de inyectar una dependencia, y por último aplica todo eso al caso más concreto de BiblioTech: extraer una interfaz IRepositorioBiblioteca a partir de las cuatro formas de persistencia construidas en el Módulo 5 (texto, JSON, SQLite, Entity Framework Core), de modo que Biblioteca pueda usar cualquiera de ellas sin saber cuál está usando en cada momento. De paso, se explica en detalle el contenedor de servicios de ASP.NET Core que ya usaste, sin profundizar, en la lección de Minimal APIs.

Contenido

  1. El problema: acoplamiento fuerte y dificultad para testear
  2. La inversión de control como solución
  3. Inyección por constructor frente a por propiedad o método
  4. Extrayendo IRepositorioBiblioteca de los repositorios del Módulo 5
  5. Biblioteca inyectada con IRepositorioBiblioteca
  6. Contenedores de DI en profundidad: AddSingleton/AddScoped/AddTransient
  7. Ejemplo completo: registrar y resolver en ASP.NET Core

  1. El problema: acoplamiento fuerte y dificultad para testear

Imagina que Biblioteca guarda y carga su catálogo llamando directamente a los métodos de persistencia en SQLite construidos en el Módulo 5:

class Biblioteca
{
    public List<MaterialBibliotecario> Catalogo { get; } = new List<MaterialBibliotecario>();

    public void GuardarCatalogo()
    {
        // Biblioteca "sabe" que la persistencia es SQLite, con detalles concretos de conexion
        using SqliteConnection conexion = new SqliteConnection("Data Source=bibliotech.db");
        conexion.Open();
        // ... SQL de INSERT/UPDATE, uno por cada Libro del catalogo ...
    }
}

Este diseño tiene dos problemas serios, aunque el código "funcione":

  • Acoplamiento fuerte: Biblioteca queda ligada a SQLite de forma permanente. Cambiar a JSON, o a Entity Framework, o a un tercer sistema de persistencia futuro, obliga a modificar Biblioteca directamente —una clase que, en principio, solo debería preocuparse de la lógica de préstamos y catálogo, no de los detalles de cómo se guardan los datos.
  • Dificultad para testear: para escribir una prueba unitaria (Lección 4 de este módulo) sobre la lógica de Biblioteca, haría falta una base de datos SQLite real disponible, con datos conocidos, cada vez que se ejecuten los tests. Eso hace las pruebas lentas, frágiles (dependen de un fichero externo) y difíciles de ejecutar en cualquier máquina sin preparación previa.

  1. La inversión de control como solución

La inversión de control (IoC) invierte quién decide qué implementación concreta se usa: en vez de que Biblioteca cree directamente su propia dependencia de persistencia (new SqliteConnection(...)), la recibe ya construida desde fuera, a través de una abstracción (una interfaz) que no revela los detalles de la implementación concreta:

flowchart LR
    subgraph "Sin inversion de control"
        B1["Biblioteca"] -->|"new SqliteConnection(...)"| S1["SQLite concreto"]
    end
    subgraph "Con inversion de control"
        B2["Biblioteca"] -->|"depende de"| I["IRepositorioBiblioteca (interfaz)"]
        S2["RepositorioSqlite"] -.->|"implementa"| I
        S3["RepositorioJson"] -.->|"implementa"| I
        Ext["Codigo externo (Program.cs, un contenedor de DI...)"] -->|"decide cual crear e inyecta"| B2
    end

Biblioteca deja de decidir "cómo" se persisten los datos: solo declara que necesita algo que cumpla el contrato IRepositorioBiblioteca, y quien la construye decide qué implementación concreta le entrega. La inyección de dependencias es, precisamente, la técnica mediante la cual esa dependencia "entra" desde fuera —por constructor, propiedad o método, como ve el siguiente apartado— en vez de crearse internamente con new. Es la forma más común de aplicar el principio de inversión de control.

  1. Inyección por constructor frente a por propiedad o método

Existen tres formas de inyectar una dependencia en una clase:

Forma Cómo se declara Cuándo se resuelve la dependencia
Por constructor Parámetro del constructor, asignado a un campo/propiedad de solo lectura Al crear el objeto; nunca puede quedar en un estado a medio configurar
Por propiedad Una propiedad pública con set, asignada después de crear el objeto En cualquier momento tras la construcción; el objeto puede existir sin la dependencia todavía asignada
Por método Un parámetro de un método concreto, en vez de un miembro de la clase Solo en el momento de llamar a ese método concreto
// Inyeccion por constructor (la recomendada por defecto)
class Biblioteca
{
    private readonly IRepositorioBiblioteca _repositorio;

    public Biblioteca(IRepositorioBiblioteca repositorio)
    {
        _repositorio = repositorio; // obligatoria: no se puede crear Biblioteca sin ella
    }
}

// Inyeccion por propiedad
class Biblioteca
{
    public IRepositorioBiblioteca? Repositorio { get; set; } // opcional: puede quedar sin asignar

    public void GuardarCatalogo()
    {
        Repositorio?.Guardar(Catalogo); // hay que comprobar que no sea null antes de usarla
    }

    public List<MaterialBibliotecario> Catalogo { get; } = new List<MaterialBibliotecario>();
}

// Inyeccion por metodo
class ImportadorMateriales
{
    public void ImportarDesde(IRepositorioBiblioteca repositorio, Biblioteca biblioteca)
    {
        biblioteca.Catalogo.AddRange(repositorio.CargarCatalogo()); // solo valida durante esta llamada
    }
}

La inyección por constructor es la práctica recomendada por defecto en la inmensa mayoría de casos, por dos razones: hace la dependencia obligatoria (no se puede crear una Biblioteca sin proporcionar su repositorio, lo que elimina de raíz cualquier NullReferenceException por olvido), y hace explícito, con solo mirar la firma del constructor, de qué depende exactamente la clase. La inyección por propiedad se reserva para dependencias genuinamente opcionales (por ejemplo, un servicio de registro/logging que puede faltar sin que el objeto deje de funcionar); la inyección por método, para dependencias que solo hacen falta durante una operación puntual, no durante toda la vida del objeto.

  1. Extrayendo IRepositorioBiblioteca de los repositorios del Módulo 5

El Módulo 5 construyó cuatro formas distintas de persistir el catálogo de BiblioTech: texto plano (GuardarCatalogoTexto/CargarCatalogoTexto), JSON (GuardarEstadoJson/CargarEstadoJson), SQLite con ADO.NET (GuardarLibrosEnSqlite/CargarLibrosDesdeSqlite) y Entity Framework Core (BibliotecaDbContext). Las cuatro resuelven, en el fondo, la misma pregunta: "guarda este catálogo" y "recupérame el catálogo guardado". Esa es la señal exacta de que hace falta una interfaz común:

interface IRepositorioBiblioteca
{
    void GuardarCatalogo(List<MaterialBibliotecario> catalogo);
    List<MaterialBibliotecario> CargarCatalogo();
}

Cada mecanismo del Módulo 5 pasa a ser una clase independiente que implementa esta interfaz, reutilizando la lógica ya escrita entonces:

class RepositorioTexto : IRepositorioBiblioteca
{
    private readonly string _ruta;

    public RepositorioTexto(string ruta) => _ruta = ruta;

    public void GuardarCatalogo(List<MaterialBibliotecario> catalogo)
    {
        using StreamWriter escritor = new StreamWriter(_ruta); // Modulo 5, GuardarCatalogoTexto
        foreach (MaterialBibliotecario material in catalogo)
        {
            escritor.WriteLine($"{material.Titulo}|{material.Autor}|{material.Disponible}");
        }
    }

    public List<MaterialBibliotecario> CargarCatalogo()
    {
        // misma logica que CargarCatalogoTexto del Modulo 5, adaptada para devolver la lista
        // en vez de rellenar directamente Biblioteca.Catalogo
        List<MaterialBibliotecario> catalogo = new List<MaterialBibliotecario>();
        // ... lectura linea a linea con StreamReader, reconstruyendo cada Libro ...
        return catalogo;
    }
}

class RepositorioJson : IRepositorioBiblioteca
{
    private readonly string _ruta;

    public RepositorioJson(string ruta) => _ruta = ruta;

    public void GuardarCatalogo(List<MaterialBibliotecario> catalogo)
    {
        string json = JsonSerializer.Serialize(catalogo); // Modulo 5, GuardarEstadoJson
        File.WriteAllText(_ruta, json);
    }

    public List<MaterialBibliotecario> CargarCatalogo()
    {
        string json = File.ReadAllText(_ruta);
        return JsonSerializer.Deserialize<List<MaterialBibliotecario>>(json) ?? new List<MaterialBibliotecario>();
    }
}

class RepositorioSqlite : IRepositorioBiblioteca
{
    private readonly string _cadenaConexion;

    public RepositorioSqlite(string cadenaConexion) => _cadenaConexion = cadenaConexion;

    public void GuardarCatalogo(List<MaterialBibliotecario> catalogo)
    {
        // misma logica que GuardarLibrosEnSqlite del Modulo 5, con SqliteConnection/SqliteCommand
    }

    public List<MaterialBibliotecario> CargarCatalogo()
    {
        // misma logica que CargarLibrosDesdeSqlite del Modulo 5, con SqliteDataReader
        return new List<MaterialBibliotecario>();
    }
}

class RepositorioEntityFramework : IRepositorioBiblioteca
{
    private readonly BibliotecaDbContext _contexto;

    public RepositorioEntityFramework(BibliotecaDbContext contexto) => _contexto = contexto;

    public void GuardarCatalogo(List<MaterialBibliotecario> catalogo)
    {
        _contexto.AddRange(catalogo.OfType<Libro>()); // simplificado: EF Core distingue Libro/Revista por su tipo
        _contexto.SaveChanges(); // version sincrona de SaveChangesAsync, Modulo 5
    }

    public List<MaterialBibliotecario> CargarCatalogo()
    {
        return _contexto.Libros.Cast<MaterialBibliotecario>().ToList(); // consulta LINQ, Modulo 5
    }
}

Ninguna de estas cuatro clases modifica la lógica de persistencia que ya construiste en el Módulo 5 —solo la reorganiza detrás de un contrato común. RepositorioEntityFramework, en particular, recibe su propio BibliotecaDbContext también por inyección de constructor: la inyección de dependencias se aplica en cadena, no solo en el punto más externo.

  1. Biblioteca inyectada con IRepositorioBiblioteca

Con la interfaz ya definida, Biblioteca deja de conocer cualquier detalle concreto de persistencia:

class Biblioteca
{
    private readonly IRepositorioBiblioteca _repositorio;

    public List<MaterialBibliotecario> Catalogo { get; private set; } = new List<MaterialBibliotecario>();

    public Biblioteca(IRepositorioBiblioteca repositorio)
    {
        _repositorio = repositorio;
    }

    public void GuardarCatalogo() => _repositorio.GuardarCatalogo(Catalogo);

    public void CargarCatalogo() => Catalogo = _repositorio.CargarCatalogo();
}
// Con RepositorioJson
Biblioteca bibliotecaJson = new Biblioteca(new RepositorioJson("catalogo.json"));

// Exactamente la misma clase Biblioteca, ahora con SQLite, sin cambiar ni una linea de Biblioteca
Biblioteca bibliotecaSqlite = new Biblioteca(new RepositorioSqlite("Data Source=bibliotech.db"));

Biblioteca es idéntica en ambos casos: lo único que cambia es qué implementación concreta de IRepositorioBiblioteca se le entrega al construirla. Este es exactamente el objetivo de la inversión de control planteado en el apartado 2, ya resuelto de forma concreta sobre el dominio de BiblioTech. Además, como se anticipó en el apartado 1, esta misma estructura es la que permitirá, en la Lección 4, sustituir IRepositorioBiblioteca por un doble de prueba (mock) sin necesitar ningún fichero ni base de datos real durante los tests.

  1. Contenedores de DI en profundidad: AddSingleton/AddScoped/AddTransient

La lección de ASP.NET Core (Módulo 7) introdujo el contenedor de servicios de forma básica, con builder.Services.AddSingleton<Biblioteca>(). Ahora que conoces la teoría completa de la inyección de dependencias, es el momento de entender por qué existen tres métodos distintos de registro, no solo AddSingleton:

Método Cuántas instancias crea Cuándo usarlo
AddSingleton<T>() Una única instancia, compartida durante toda la vida de la aplicación Estado verdaderamente global y seguro de compartir entre peticiones concurrentes (por ejemplo, IPoliticaSancion de la lección anterior, si no guarda estado propio)
AddScoped<T>() Una instancia nueva por cada petición HTTP (o "ámbito"), compartida dentro de esa misma petición Dependencias con estado propio de una operación, como BibliotecaDbContext (recuerda el error común de la lección de ASP.NET Core: nunca como singleton)
AddTransient<T>() Una instancia nueva cada vez que se pide, incluso varias veces dentro de la misma petición Dependencias ligeras, sin estado, que no importa crear repetidamente
var builder = WebApplication.CreateBuilder(args);

// IRepositorioBiblioteca resuelto como una instancia de RepositorioEntityFramework
builder.Services.AddScoped<IRepositorioBiblioteca, RepositorioEntityFramework>();
builder.Services.AddScoped<BibliotecaDbContext>(); // una instancia nueva por peticion, correcto (Modulo 7)
builder.Services.AddScoped<Biblioteca>(); // Biblioteca ahora tambien depende de IRepositorioBiblioteca

var app = builder.Build();

app.MapGet("/libros", (Biblioteca biblioteca) =>
{
    biblioteca.CargarCatalogo();
    return Results.Ok(biblioteca.Catalogo);
});

AddScoped<IRepositorioBiblioteca, RepositorioEntityFramework>() registra dos tipos: el primero es el tipo que se pedirá (la interfaz), el segundo es la implementación concreta que el contenedor debe construir cuando alguien pida ese tipo. Cuando el endpoint declara Biblioteca biblioteca como parámetro, ASP.NET Core resuelve automáticamente toda la cadena: construye un RepositorioEntityFramework, que a su vez necesita un BibliotecaDbContext (también registrado), y con ese repositorio ya construido, construye finalmente la Biblioteca que el endpoint recibe. Ninguna de estas construcciones intermedias la escribe el programador a mano: el contenedor de DI la resuelve leyendo los constructores de cada clase registrada.

AddScoped para Biblioteca (en vez de AddSingleton, como se vio de forma simplificada en el Módulo 7) es ahora la elección correcta: como Biblioteca depende de un BibliotecaDbContext —que ya sabes que debe vivir solo durante una petición—, Biblioteca misma debe tener el mismo ciclo de vida que su dependencia, o arrastraría un DbContext obsoleto entre peticiones distintas.

  1. Ejemplo completo: registrar y resolver en ASP.NET Core

Uniendo todas las piezas anteriores, así queda el registro completo de servicios de una API de BiblioTech que usa Entity Framework Core como mecanismo de persistencia, con Biblioteca inyectada con IRepositorioBiblioteca en vez de acoplada directamente a EF Core:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<BibliotecaDbContext>(opciones =>
    opciones.UseSqlite("Data Source=bibliotech.db")); // AddDbContext ya registra el ciclo de vida correcto (Scoped)

builder.Services.AddScoped<IRepositorioBiblioteca, RepositorioEntityFramework>();
builder.Services.AddScoped<Biblioteca>();

var app = builder.Build();

app.MapPost("/catalogo/guardar", (Biblioteca biblioteca) =>
{
    biblioteca.GuardarCatalogo();
    return Results.Ok("Catalogo guardado.");
});

app.Run();

Para probar con otro mecanismo de persistencia —por ejemplo, en un entorno de pruebas manuales sin base de datos— bastaría con cambiar una sola línea:

// En vez de EF Core, usa JSON: ni Biblioteca ni los endpoints necesitan ningun cambio
builder.Services.AddSingleton<IRepositorioBiblioteca>(new RepositorioJson("catalogo.json"));

Este es el resultado práctico de haber invertido el control desde el apartado 2: cambiar por completo el mecanismo de persistencia de toda la API es, ahora, una única línea en Program.cs, en vez de una modificación dispersa por todo Biblioteca.

Errores Comunes y Consejos

  • Registrar Biblioteca como AddSingleton cuando depende de un BibliotecaDbContext Scoped: el contenedor de ASP.NET Core lo detecta y lanza una excepción al arrancar (captive dependency, una dependencia de vida corta "atrapada" dentro de un objeto de vida más larga). El ciclo de vida de una clase nunca debe ser más largo que el de sus dependencias.
  • Inyectar por propiedad "por comodidad" en vez de por constructor: hace que la dependencia parezca opcional cuando en realidad es obligatoria, y permite crear el objeto en un estado incompleto (sin la dependencia asignada) que solo falla más tarde, al usarla.
  • Confundir la interfaz IRepositorioBiblioteca con "una capa inútil de más": parece redundante mientras solo hay una implementación real, pero su valor aparece justo cuando hace falta cambiar de implementación (apartado 7) o sustituirla por un doble de prueba (Lección 4).
  • Consejo: si al diseñar una clase te preguntas "¿esto debería ser new aquí dentro, o recibido desde fuera?", la pregunta de oro es: ¿alguna vez necesitaré sustituir esta dependencia por otra implementación, o por una versión de prueba? Si la respuesta es sí, inyéctala.

Ejercicios

  1. Define una interfaz INotificador con un único método void Notificar(string mensaje), y dos implementaciones: NotificadorConsola (que use Console.WriteLine) y NotificadorSilencioso (que no haga nada). Modifica Biblioteca para que reciba un INotificador por constructor y lo use dentro de RegistrarPrestamo en vez de llamar directamente a Console.WriteLine.

  2. En el registro de servicios de ASP.NET Core del apartado 7, explica por qué IRepositorioBiblioteca y Biblioteca deben registrarse como AddScoped (y no AddSingleton) mientras dependan, directa o indirectamente, de BibliotecaDbContext.

Soluciones

interface INotificador
{
    void Notificar(string mensaje);
}

class NotificadorConsola : INotificador
{
    public void Notificar(string mensaje) => Console.WriteLine(mensaje);
}

class NotificadorSilencioso : INotificador
{
    public void Notificar(string mensaje) { /* no hace nada */ }
}

class Biblioteca
{
    private readonly IRepositorioBiblioteca _repositorio;
    private readonly INotificador _notificador;

    public List<Prestamo> Prestamos { get; } = new List<Prestamo>();

    public Biblioteca(IRepositorioBiblioteca repositorio, INotificador notificador)
    {
        _repositorio = repositorio;
        _notificador = notificador;
    }

    public void RegistrarPrestamo(Prestamo prestamo)
    {
        Prestamos.Add(prestamo);
        _notificador.Notificar($"Prestamo de '{prestamo.Libro.Titulo}' registrado.");
    }
}

BibliotecaDbContext se registra con AddDbContext (equivalente a AddScoped) porque no está pensado para compartirse entre peticiones concurrentes (lección de ASP.NET Core, Módulo 7). Si IRepositorioBiblioteca (que envuelve ese DbContext) o Biblioteca (que depende de IRepositorioBiblioteca) se registraran como AddSingleton, la primera petición HTTP dejaría "atrapado" dentro de ese singleton un BibliotecaDbContext que debería haberse desechado al terminar esa petición, y las peticiones siguientes reutilizarían un contexto obsoleto o ya liberado, provocando errores en tiempo de ejecución.

Conclusión

En esta lección has entendido el problema de fondo que resuelve la inversión de control (acoplamiento fuerte y dificultad para testear), comparado la inyección por constructor —la recomendada por defecto— con la inyección por propiedad y por método, extraído una interfaz IRepositorioBiblioteca que unifica las cuatro formas de persistencia del Módulo 5 detrás de un contrato común, e inyectado esa interfaz en Biblioteca para desacoplarla de cualquier mecanismo concreto. También has entendido en profundidad, por fin, la diferencia entre AddSingleton, AddScoped y AddTransient en el contenedor de servicios de ASP.NET Core, que solo se había mencionado de pasada en el Módulo 7.

Todo este trabajo de desacoplar Biblioteca de sus dependencias concretas tiene un beneficio que todavía no has explotado: ahora es posible sustituir IRepositorioBiblioteca por una versión de prueba que no toque ningún fichero ni base de datos real. Esa es, precisamente, la puerta de entrada a la siguiente lección: pruebas unitarias, donde IRepositorioBiblioteca se sustituirá por un doble de prueba (mock) para verificar la lógica de Biblioteca de forma rápida, aislada y repetible.

Curso de Programación en C#

Módulo 1: Introducción a C#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados