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:
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
dotnet new console -n BibliotecaApp- Añadir paquetes EF Core + SQLite
- Crear los modelos (copiando de la sección 3)
- Crear el DbContext
dotnet ef migrations add Inicialdotnet ef database update- Implementar los servicios uno por uno
- Crear el menú de consola
- Probar cada funcionalidad
- 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