Módulo 19: Proyecto final — Gestión de Biblioteca

Objetivos del módulo

  • Integrar todos los conocimientos del curso en un proyecto completo
  • Aplicar buenas prácticas: organización, validación, manejo de errores
  • Trabajar con EF Core, LINQ, async/await, POO e interfaces

1. Descripción del proyecto

Construirás un sistema de gestión de biblioteca por consola que permita:

  • Gestionar libros, autores y miembros
  • Registrar préstamos y devoluciones
  • Buscar y filtrar con LINQ
  • Persistir datos con EF Core + SQLite
  • Exportar informes

2. Estructura del proyecto

Así se vería la estructura del proyecto en el Explorador de soluciones de Visual Studio Community:

Estructura del proyecto en Visual Studio Explorador de soluciones mostrando la estructura organizada por carpetas

BibliotecaApp/
│── Program.cs              ← Punto de entrada
│── Data/
│   └── BibliotecaContext.cs  ← DbContext
│── Models/
│   ├── Libro.cs
│   ├── Autor.cs
│   ├── Miembro.cs
│   ├── Prestamo.cs
│   └── Enums.cs            ← GeneroLibro, EstadoPrestamo
│── Services/
│   ├── ILibroService.cs
│   ├── LibroService.cs
│   ├── IMiembroService.cs
│   ├── MiembroService.cs
│   ├── IPrestamoService.cs
│   └── PrestamoService.cs
│── Utils/
│   ├── ConsoleHelper.cs    ← Utilidades de consola
│   └── Validador.cs        ← Validaciones comunes
│── biblioteca.db           ← Base de datos SQLite (generada)

3. Modelos

// Models/Enums.cs
enum GeneroLibro
{
    Novela, Ciencia, Historia, Tecnologia,
    Biografia, Poesia, Infantil, Otro
}

enum EstadoPrestamo { Activo, Devuelto, Vencido }

// Models/Autor.cs
class Autor
{
    public int Id { get; set; }
    public string Nombre { get; set; } = "";
    public string Nacionalidad { get; set; } = "";
    public List<Libro> Libros { get; set; } = new();

    public override string ToString() => $"{Nombre} ({Nacionalidad})";
}

// Models/Libro.cs
class Libro
{
    public int Id { get; set; }
    public string Titulo { get; set; } = "";
    public string ISBN { get; set; } = "";
    public int AñoPublicacion { get; set; }
    public GeneroLibro Genero { get; set; }
    public int Ejemplares { get; set; }
    public int EjemplaresDisponibles { get; set; }

    public int AutorId { get; set; }
    public Autor Autor { get; set; } = null!;
    public List<Prestamo> Prestamos { get; set; } = new();

    public bool HayDisponibles => EjemplaresDisponibles > 0;

    public override string ToString() =>
        $"\"{Titulo}\" de {Autor?.Nombre ?? "?"} ({AñoPublicacion}) " +
        $"[{EjemplaresDisponibles}/{Ejemplares} disponibles]";
}

// Models/Miembro.cs
class Miembro
{
    public int Id { get; set; }
    public string Nombre { get; set; } = "";
    public string Email { get; set; } = "";
    public DateTime FechaAlta { get; set; } = DateTime.Now;
    public List<Prestamo> Prestamos { get; set; } = new();

    public int PrestamosActivos => Prestamos.Count(p => p.Estado == EstadoPrestamo.Activo);
}

// Models/Prestamo.cs
class Prestamo
{
    public int Id { get; set; }
    public DateTime FechaPrestamo { get; set; } = DateTime.Now;
    public DateTime FechaLimite { get; set; }
    public DateTime? FechaDevolucion { get; set; }
    public EstadoPrestamo Estado { get; set; } = EstadoPrestamo.Activo;

    public int LibroId { get; set; }
    public Libro Libro { get; set; } = null!;
    public int MiembroId { get; set; }
    public Miembro Miembro { get; set; } = null!;

    public bool EstaVencido => Estado == EstadoPrestamo.Activo
                                && DateTime.Now > FechaLimite;
}

4. DbContext

// Data/BibliotecaContext.cs
using Microsoft.EntityFrameworkCore;

class BibliotecaContext : DbContext
{
    public DbSet<Libro> Libros { get; set; }
    public DbSet<Autor> Autores { get; set; }
    public DbSet<Miembro> Miembros { get; set; }
    public DbSet<Prestamo> Prestamos { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseSqlite("Data Source=biblioteca.db");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Libro>()
            .HasIndex(l => l.ISBN).IsUnique();

        modelBuilder.Entity<Miembro>()
            .HasIndex(m => m.Email).IsUnique();
    }
}

5. Servicios (lógica de negocio)

// Services/IPrestamoService.cs
interface IPrestamoService
{
    Task<Prestamo> RealizarPrestamoAsync(int libroId, int miembroId, int diasPrestamo = 14);
    Task<bool> DevolverAsync(int prestamoId);
    Task<List<Prestamo>> ObtenerActivosAsync();
    Task<List<Prestamo>> ObtenerVencidosAsync();
}

// Services/PrestamoService.cs
class PrestamoService : IPrestamoService
{
    private readonly BibliotecaContext _db;
    private const int MAX_PRESTAMOS_ACTIVOS = 3;

    public PrestamoService(BibliotecaContext db)
    {
        _db = db;
    }

    public async Task<Prestamo> RealizarPrestamoAsync(int libroId, int miembroId, int dias = 14)
    {
        var libro = await _db.Libros.FindAsync(libroId)
            ?? throw new ArgumentException("Libro no encontrado");

        var miembro = await _db.Miembros
            .Include(m => m.Prestamos)
            .FirstOrDefaultAsync(m => m.Id == miembroId)
            ?? throw new ArgumentException("Miembro no encontrado");

        if (!libro.HayDisponibles)
            throw new InvalidOperationException($"No hay ejemplares disponibles de '{libro.Titulo}'");

        if (miembro.PrestamosActivos >= MAX_PRESTAMOS_ACTIVOS)
            throw new InvalidOperationException(
                $"{miembro.Nombre} ya tiene {MAX_PRESTAMOS_ACTIVOS} préstamos activos");

        var prestamo = new Prestamo
        {
            LibroId = libroId,
            MiembroId = miembroId,
            FechaLimite = DateTime.Now.AddDays(dias)
        };

        libro.EjemplaresDisponibles--;
        _db.Prestamos.Add(prestamo);
        await _db.SaveChangesAsync();

        return prestamo;
    }

    public async Task<bool> DevolverAsync(int prestamoId)
    {
        var prestamo = await _db.Prestamos
            .Include(p => p.Libro)
            .FirstOrDefaultAsync(p => p.Id == prestamoId);

        if (prestamo is null || prestamo.Estado != EstadoPrestamo.Activo)
            return false;

        prestamo.Estado = EstadoPrestamo.Devuelto;
        prestamo.FechaDevolucion = DateTime.Now;
        prestamo.Libro.EjemplaresDisponibles++;

        await _db.SaveChangesAsync();
        return true;
    }

    public async Task<List<Prestamo>> ObtenerActivosAsync()
    {
        return await _db.Prestamos
            .Include(p => p.Libro)
            .Include(p => p.Miembro)
            .Where(p => p.Estado == EstadoPrestamo.Activo)
            .OrderBy(p => p.FechaLimite)
            .ToListAsync();
    }

    public async Task<List<Prestamo>> ObtenerVencidosAsync()
    {
        return await _db.Prestamos
            .Include(p => p.Libro)
            .Include(p => p.Miembro)
            .Where(p => p.Estado == EstadoPrestamo.Activo && p.FechaLimite < DateTime.Now)
            .ToListAsync();
    }
}

6. Menú principal

// Program.cs
using var db = new BibliotecaContext();
await db.Database.EnsureCreatedAsync();

var prestamoService = new PrestamoService(db);

bool salir = false;
while (!salir)
{
    Console.Clear();
    Console.WriteLine("╔════════════════════════════╗");
    Console.WriteLine("║   GESTIÓN DE BIBLIOTECA    ║");
    Console.WriteLine("╠════════════════════════════╣");
    Console.WriteLine("║  1. Gestionar libros       ║");
    Console.WriteLine("║  2. Gestionar autores      ║");
    Console.WriteLine("║  3. Gestionar miembros     ║");
    Console.WriteLine("║  4. Préstamos              ║");
    Console.WriteLine("║  5. Informes               ║");
    Console.WriteLine("║  0. Salir                  ║");
    Console.WriteLine("╚════════════════════════════╝");

    switch (Console.ReadLine())
    {
        case "1": await MenuLibrosAsync(db); break;
        case "2": await MenuAutoresAsync(db); break;
        case "3": await MenuMiembrosAsync(db); break;
        case "4": await MenuPrestamosAsync(prestamoService, db); break;
        case "5": await MenuInformesAsync(db); break;
        case "0": salir = true; break;
    }
}

7. Informes con LINQ

static async Task MenuInformesAsync(BibliotecaContext db)
{
    // Libros más prestados
    var masPopulares = await db.Libros
        .Include(l => l.Prestamos)
        .Include(l => l.Autor)
        .OrderByDescending(l => l.Prestamos.Count)
        .Take(10)
        .Select(l => new
        {
            l.Titulo,
            Autor = l.Autor.Nombre,
            VecesPrestado = l.Prestamos.Count
        })
        .ToListAsync();

    Console.WriteLine("\n📊 TOP 10 libros más prestados:");
    foreach (var (libro, i) in masPopulares.Select((l, i) => (l, i)))
    {
        Console.WriteLine($"  {i + 1}. {libro.Titulo} ({libro.Autor}) - {libro.VecesPrestado} préstamos");
    }

    // Estadísticas por género
    var porGenero = await db.Libros
        .GroupBy(l => l.Genero)
        .Select(g => new
        {
            Genero = g.Key,
            Total = g.Count(),
            PrestamosTotal = g.Sum(l => l.Prestamos.Count)
        })
        .OrderByDescending(g => g.PrestamosTotal)
        .ToListAsync();

    Console.WriteLine("\n📊 Estadísticas por género:");
    foreach (var g in porGenero)
    {
        Console.WriteLine($"  {g.Genero}: {g.Total} libros, {g.PrestamosTotal} préstamos");
    }

    // Miembros más activos
    var miembrosActivos = await db.Miembros
        .Include(m => m.Prestamos)
        .OrderByDescending(m => m.Prestamos.Count)
        .Take(5)
        .ToListAsync();

    Console.WriteLine("\n📊 Miembros más activos:");
    foreach (var m in miembrosActivos)
    {
        Console.WriteLine($"  {m.Nombre}: {m.Prestamos.Count} préstamos totales " +
                          $"({m.PrestamosActivos} activos)");
    }
}

8. Funcionalidades a implementar

Obligatorias

Función Descripción
CRUD Libros Crear, leer, actualizar, eliminar libros
CRUD Autores Crear, leer, actualizar, eliminar autores
CRUD Miembros Crear, leer, actualizar, eliminar miembros
Préstamo Registrar préstamo con validaciones
Devolución Registrar devolución, actualizar stock
Búsquedas Por título, autor, ISBN, género
Informes Top libros, estadísticas, vencidos

Opcionales (para nota extra)

Función Descripción
Exportar CSV Exportar informes a archivo CSV
Exportar JSON Exportar catálogo completo a JSON
Historial Ver historial de un miembro/libro
Reservas Sistema de reservas cuando no hay stock
Multas Calcular multas por devolución tardía

9. Rúbrica de evaluación

Criterio Puntuación
Modelos bien diseñados con relaciones 15%
CRUD completo y funcionando 20%
Préstamos con validaciones 15%
Uso correcto de LINQ 15%
Async/await en operaciones de BD 10%
Manejo de errores 10%
Interfaz de consola usable 5%
Código limpio y organizado 5%
Funcionalidades extra 5%

10. Pasos para empezar

  1. dotnet new console -n BibliotecaApp
  2. Añadir paquetes EF Core + SQLite
  3. Crear los modelos (copiando de la sección 3)
  4. Crear el DbContext
  5. dotnet ef migrations add Inicial
  6. dotnet ef database update
  7. Implementar los servicios uno por uno
  8. Crear el menú de consola
  9. Probar cada funcionalidad
  10. Añadir informes y funcionalidades extra

💡 Consejo: No intentes hacer todo de golpe. Empieza con un CRUD sencillo (solo libros), asegúrate de que funciona, y ve añadiendo funcionalidades.


¡Enhorabuena!

Has completado el Curso de C# con .NET 10. Has aprendido desde los tipos de datos más básicos hasta Entity Framework Core, pasando por POO, LINQ, async/await y mucho más.

¿Qué sigue?

  • Explora ASP.NET Core para crear aplicaciones web
  • Aprende Blazor para aplicaciones web con C#
  • Descubre MAUI para aplicaciones móviles
  • Continúa con el Curso de Testing para aprender a probar tu código