Módulo 18: ADO.NET
Objetivos del módulo
- Comprender qué es ADO.NET y cuándo usarlo
- Conectarse directamente a una base de datos desde C# 14 con .NET 10
- Ejecutar consultas SQL con
DbConnection,DbCommandy parámetros - Leer datos con
DbDataReader - Implementar un CRUD completo usando SQL directo
- Usar transacciones, consultas asíncronas y manejo correcto de errores
- Conocer propiedades avanzadas: pooling, timeout, aislamiento,
CommandType,DataTabley proveedores
1. ¿Qué es ADO.NET?
📘 Concepto: ADO.NET es el conjunto de clases de .NET para comunicarse directamente con bases de datos. Permite abrir conexiones, ejecutar SQL, leer resultados y controlar transacciones sin usar un ORM como Entity Framework Core.
Entity Framework Core trabaja por encima de ADO.NET. Cuando EF Core guarda o consulta datos, por debajo termina usando conexiones, comandos y proveedores de datos.
ADO.NET se sitúa entre tu código C# y el proveedor específico de la base de datos
| Concepto | Significado |
|---|---|
DbConnection | Conexión con la base de datos |
DbCommand | Comando SQL que se ejecuta contra la conexión |
DbParameter | Valor seguro que se envía a una consulta SQL |
DbDataReader | Lector rápido, hacia delante, para recorrer filas |
DataTable | Tabla en memoria, útil para datos desconectados |
| Transacción | Bloque de operaciones que se confirman o cancelan juntas |
| Proveedor | Librería concreta para SQLite, SQL Server, PostgreSQL, MySQL, etc. |
¿Cuándo usar ADO.NET?
| Usa ADO.NET cuando… | Usa EF Core cuando… |
|---|---|
| Necesitas control total del SQL | Quieres trabajar con clases y LINQ |
| Vas a ejecutar consultas muy específicas | Quieres productividad y migraciones |
| Tienes procedimientos almacenados complejos | Quieres mapear relaciones entre entidades |
| Estás optimizando rendimiento fino | La lógica encaja bien con un modelo de dominio |
| Mantienes una base de datos heredada | Quieres reducir código repetitivo |
💡 Consejo: ADO.NET no reemplaza a EF Core. Es una herramienta más cercana al metal. En proyectos reales pueden convivir: EF Core para CRUD habitual y ADO.NET para consultas críticas o muy específicas.
2. Instalar paquetes
ADO.NET define las clases base en .NET, pero necesitas un proveedor para la base de datos concreta.
Para este módulo usaremos SQLite porque crea una base de datos en un archivo y es fácil de probar:
dotnet new console -n TiendaAdoNet
cd TiendaAdoNet
dotnet add package Microsoft.Data.Sqlite
Si quisieras usar SQL Server:
dotnet add package Microsoft.Data.SqlClient
Ejemplo de referencias en el .csproj:
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.8" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.1" />
</ItemGroup>
⚠️ Importante: No necesitas instalar los dos proveedores a la vez. Instala solo el proveedor de la base de datos que vayas a usar.
Instalar desde Visual Studio Community
Puedes instalar el proveedor desde Herramientas → Administrador de paquetes NuGet → Administrar paquetes NuGet para la solución….
Busca:
Microsoft.Data.Sqlitepara SQLiteMicrosoft.Data.SqlClientpara SQL Server
Selecciona el proyecto correcto y pulsa Instalar.
Instalar desde Visual Studio Code
En VS Code abre la terminal integrada en la carpeta donde está el .csproj:
dotnet add package Microsoft.Data.Sqlite
3. Crear la base de datos
Empezaremos con una tabla Productos en SQLite.
using Microsoft.Data.Sqlite;
const string connectionString = "Data Source=tienda.db";
await using SqliteConnection connection = new(connectionString);
await connection.OpenAsync();
string sql = """
CREATE TABLE IF NOT EXISTS Productos (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Nombre TEXT NOT NULL,
Categoria TEXT NOT NULL,
Precio REAL NOT NULL,
Stock INTEGER NOT NULL,
FechaCreacion TEXT NOT NULL
);
""";
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = sql;
await command.ExecuteNonQueryAsync();
Console.WriteLine("Base de datos preparada.");
| Parte | Explicación |
|---|---|
SqliteConnection | Conexión específica para SQLite |
OpenAsync() | Abre la conexión |
CreateCommand() | Crea un comando asociado a la conexión |
CommandText | SQL que se va a ejecutar |
ExecuteNonQueryAsync() | Ejecuta SQL que no devuelve filas |
💡 Consejo: Usa
await usingcon conexiones y comandos. Así se cierran correctamente aunque ocurra una excepción.
4. Tres formas de ejecutar comandos
ADO.NET tiene tres métodos principales para ejecutar SQL:
| Método | Devuelve | Uso típico |
|---|---|---|
ExecuteNonQuery / ExecuteNonQueryAsync | Número de filas afectadas | INSERT, UPDATE, DELETE, CREATE TABLE |
ExecuteScalar / ExecuteScalarAsync | Un único valor | COUNT, MAX, nuevo Id |
ExecuteReader / ExecuteReaderAsync | Filas completas | SELECT |
ExecuteNonQueryAsync
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
UPDATE Productos
SET Stock = Stock + 10
WHERE Id = @id;
""";
command.Parameters.AddWithValue("@id", 1);
int filasAfectadas = await command.ExecuteNonQueryAsync();
Console.WriteLine($"Filas modificadas: {filasAfectadas}");
ExecuteScalarAsync
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = "SELECT COUNT(*) FROM Productos;";
long total = (long)(await command.ExecuteScalarAsync() ?? 0);
Console.WriteLine($"Total de productos: {total}");
ExecuteReaderAsync
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
SELECT Id, Nombre, Precio
FROM Productos
ORDER BY Nombre;
""";
await using SqliteDataReader reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
int id = reader.GetInt32(0);
string nombre = reader.GetString(1);
double precio = reader.GetDouble(2);
Console.WriteLine($"{id}: {nombre} - {precio:C2}");
}
5. Parámetros y seguridad
⚠️ Importante: Nunca construyas SQL concatenando texto del usuario. Usa parámetros para evitar inyección SQL y errores de formato.
Mal:
string nombre = Console.ReadLine()!;
command.CommandText = $"SELECT * FROM Productos WHERE Nombre = '{nombre}'";
Bien:
string nombre = Console.ReadLine()!;
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
SELECT Id, Nombre, Precio
FROM Productos
WHERE Nombre = @nombre;
""";
command.Parameters.AddWithValue("@nombre", nombre);
Ejemplo de ataque si concatenas:
' OR 1=1 --
Con parámetros, ese texto se interpreta como un valor normal, no como parte del SQL.
Parámetros con tipo
AddWithValue es cómodo para ejemplos, pero en código profesional conviene declarar tipos cuando necesitas precisión:
SqliteParameter precio = command.CreateParameter();
precio.ParameterName = "@precio";
precio.SqliteType = SqliteType.Real;
precio.Value = 89.99;
command.Parameters.Add(precio);
6. Modelo de datos
Usaremos este modelo para el CRUD:
public class Producto
{
public int Id { get; set; }
public string Nombre { get; set; } = "";
public string Categoria { get; set; } = "";
public decimal Precio { get; set; }
public int Stock { get; set; }
public DateTime FechaCreacion { get; set; }
public override string ToString()
{
return $"{Id}: {Nombre} ({Categoria}) - {Precio:C2} - Stock: {Stock}";
}
}
Y una clase repositorio para concentrar el acceso a datos:
public class ProductoRepository
{
private readonly string _connectionString;
public ProductoRepository(string connectionString)
{
_connectionString = connectionString;
}
private SqliteConnection CrearConexion()
{
return new SqliteConnection(_connectionString);
}
}
7. CRUD completo con ADO.NET
Las operaciones CRUD se ejecutan con SQL parametrizado
Crear la tabla
public async Task InicializarAsync()
{
await using SqliteConnection connection = CrearConexion();
await connection.OpenAsync();
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
CREATE TABLE IF NOT EXISTS Productos (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Nombre TEXT NOT NULL,
Categoria TEXT NOT NULL,
Precio REAL NOT NULL,
Stock INTEGER NOT NULL,
FechaCreacion TEXT NOT NULL
);
""";
await command.ExecuteNonQueryAsync();
}
Create: insertar producto
public async Task<int> CrearAsync(Producto producto)
{
await using SqliteConnection connection = CrearConexion();
await connection.OpenAsync();
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
INSERT INTO Productos (Nombre, Categoria, Precio, Stock, FechaCreacion)
VALUES (@nombre, @categoria, @precio, @stock, @fechaCreacion);
SELECT last_insert_rowid();
""";
command.Parameters.AddWithValue("@nombre", producto.Nombre);
command.Parameters.AddWithValue("@categoria", producto.Categoria);
command.Parameters.AddWithValue("@precio", producto.Precio);
command.Parameters.AddWithValue("@stock", producto.Stock);
command.Parameters.AddWithValue("@fechaCreacion", producto.FechaCreacion.ToString("O"));
long id = (long)(await command.ExecuteScalarAsync() ?? 0);
return (int)id;
}
Read: obtener todos
public async Task<List<Producto>> ObtenerTodosAsync()
{
List<Producto> productos = new();
await using SqliteConnection connection = CrearConexion();
await connection.OpenAsync();
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
SELECT Id, Nombre, Categoria, Precio, Stock, FechaCreacion
FROM Productos
ORDER BY Nombre;
""";
await using SqliteDataReader reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
productos.Add(MapearProducto(reader));
}
return productos;
}
Read: obtener por Id
public async Task<Producto?> ObtenerPorIdAsync(int id)
{
await using SqliteConnection connection = CrearConexion();
await connection.OpenAsync();
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
SELECT Id, Nombre, Categoria, Precio, Stock, FechaCreacion
FROM Productos
WHERE Id = @id;
""";
command.Parameters.AddWithValue("@id", id);
await using SqliteDataReader reader = await command.ExecuteReaderAsync();
if (!await reader.ReadAsync())
return null;
return MapearProducto(reader);
}
Update: actualizar producto
public async Task<bool> ActualizarAsync(Producto producto)
{
await using SqliteConnection connection = CrearConexion();
await connection.OpenAsync();
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
UPDATE Productos
SET Nombre = @nombre,
Categoria = @categoria,
Precio = @precio,
Stock = @stock
WHERE Id = @id;
""";
command.Parameters.AddWithValue("@id", producto.Id);
command.Parameters.AddWithValue("@nombre", producto.Nombre);
command.Parameters.AddWithValue("@categoria", producto.Categoria);
command.Parameters.AddWithValue("@precio", producto.Precio);
command.Parameters.AddWithValue("@stock", producto.Stock);
int filas = await command.ExecuteNonQueryAsync();
return filas == 1;
}
Delete: eliminar producto
public async Task<bool> EliminarAsync(int id)
{
await using SqliteConnection connection = CrearConexion();
await connection.OpenAsync();
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = "DELETE FROM Productos WHERE Id = @id;";
command.Parameters.AddWithValue("@id", id);
int filas = await command.ExecuteNonQueryAsync();
return filas == 1;
}
Mapear filas a objetos
private static Producto MapearProducto(SqliteDataReader reader)
{
return new Producto
{
Id = reader.GetInt32(reader.GetOrdinal("Id")),
Nombre = reader.GetString(reader.GetOrdinal("Nombre")),
Categoria = reader.GetString(reader.GetOrdinal("Categoria")),
Precio = reader.GetDecimal(reader.GetOrdinal("Precio")),
Stock = reader.GetInt32(reader.GetOrdinal("Stock")),
FechaCreacion = DateTime.Parse(reader.GetString(reader.GetOrdinal("FechaCreacion")))
};
}
8. Programa completo de prueba
using Microsoft.Data.Sqlite;
const string connectionString = "Data Source=tienda.db";
ProductoRepository repository = new(connectionString);
await repository.InicializarAsync();
int id = await repository.CrearAsync(new Producto
{
Nombre = "Teclado mecánico",
Categoria = "Periféricos",
Precio = 89.99m,
Stock = 12,
FechaCreacion = DateTime.UtcNow
});
Console.WriteLine($"Producto creado con Id {id}");
Producto? producto = await repository.ObtenerPorIdAsync(id);
Console.WriteLine(producto);
if (producto is not null)
{
producto.Precio = 79.99m;
producto.Stock += 5;
await repository.ActualizarAsync(producto);
}
List<Producto> productos = await repository.ObtenerTodosAsync();
foreach (Producto p in productos)
{
Console.WriteLine(p);
}
await repository.EliminarAsync(id);
Console.WriteLine("Producto eliminado.");
💡 Consejo: En una aplicación real separarías las clases en archivos:
Producto.cs,ProductoRepository.csyProgram.cs.
9. Consultas con filtros y paginación
public async Task<List<Producto>> BuscarAsync(
string? categoria,
decimal? precioMaximo,
int pagina,
int tamano)
{
List<Producto> productos = new();
if (pagina < 1) pagina = 1;
if (tamano is < 1 or > 100) tamano = 10;
await using SqliteConnection connection = CrearConexion();
await connection.OpenAsync();
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
SELECT Id, Nombre, Categoria, Precio, Stock, FechaCreacion
FROM Productos
WHERE (@categoria IS NULL OR Categoria = @categoria)
AND (@precioMaximo IS NULL OR Precio <= @precioMaximo)
ORDER BY Nombre
LIMIT @tamano OFFSET @offset;
""";
command.Parameters.AddWithValue("@categoria", (object?)categoria ?? DBNull.Value);
command.Parameters.AddWithValue("@precioMaximo", (object?)precioMaximo ?? DBNull.Value);
command.Parameters.AddWithValue("@tamano", tamano);
command.Parameters.AddWithValue("@offset", (pagina - 1) * tamano);
await using SqliteDataReader reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
productos.Add(MapearProducto(reader));
}
return productos;
}
10. Transacciones
📘 Concepto: Una transacción permite ejecutar varias operaciones como una sola unidad. Si todo va bien se confirma con
Commit; si algo falla se cancela conRollback.
Ejemplo: registrar una venta y descontar stock. Ambas operaciones deben completarse juntas.
public async Task RegistrarVentaAsync(int productoId, int cantidad)
{
await using SqliteConnection connection = CrearConexion();
await connection.OpenAsync();
using SqliteTransaction transaction = connection.BeginTransaction();
try
{
await using SqliteCommand descontarStock = connection.CreateCommand();
descontarStock.Transaction = transaction;
descontarStock.CommandText = """
UPDATE Productos
SET Stock = Stock - @cantidad
WHERE Id = @productoId AND Stock >= @cantidad;
""";
descontarStock.Parameters.AddWithValue("@productoId", productoId);
descontarStock.Parameters.AddWithValue("@cantidad", cantidad);
int filas = await descontarStock.ExecuteNonQueryAsync();
if (filas != 1)
throw new InvalidOperationException("No hay stock suficiente.");
await using SqliteCommand insertarVenta = connection.CreateCommand();
insertarVenta.Transaction = transaction;
insertarVenta.CommandText = """
INSERT INTO Ventas (ProductoId, Cantidad, Fecha)
VALUES (@productoId, @cantidad, @fecha);
""";
insertarVenta.Parameters.AddWithValue("@productoId", productoId);
insertarVenta.Parameters.AddWithValue("@cantidad", cantidad);
insertarVenta.Parameters.AddWithValue("@fecha", DateTime.UtcNow.ToString("O"));
await insertarVenta.ExecuteNonQueryAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
11. Propiedades avanzadas de conexión
La cadena de conexión controla detalles importantes:
const string connectionString = """
Data Source=tienda.db;
Mode=ReadWriteCreate;
Cache=Shared;
Pooling=True;
Default Timeout=30
""";
| Propiedad | Uso |
|---|---|
Data Source | Archivo SQLite o servidor, según proveedor |
Mode | Modo de apertura en SQLite |
Cache | Caché compartida en SQLite |
Pooling | Reutilización de conexiones |
Default Timeout | Tiempo máximo de espera |
Ejemplo SQL Server:
const string connectionString = """
Server=localhost;
Database=Tienda;
Trusted_Connection=True;
TrustServerCertificate=True;
Connect Timeout=30;
Pooling=True;
Max Pool Size=100;
""";
💡 Consejo: El pooling hace que abrir y cerrar conexiones sea barato. Por eso lo correcto es abrir la conexión lo más tarde posible y cerrarla cuanto antes.
12. Propiedades avanzadas de comandos
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = "SELECT COUNT(*) FROM Productos;";
command.CommandTimeout = 30;
command.CommandType = System.Data.CommandType.Text;
| Propiedad | Significado |
|---|---|
CommandText | SQL o nombre del procedimiento |
CommandTimeout | Segundos máximos antes de cancelar |
CommandType.Text | El comando es SQL normal |
CommandType.StoredProcedure | El comando llama a un procedimiento almacenado |
Transaction | Transacción asociada al comando |
Ejemplo con SQL Server y procedimiento almacenado:
using Microsoft.Data.SqlClient;
using System.Data;
await using SqlConnection connection = new(connectionString);
await connection.OpenAsync();
await using SqlCommand command = connection.CreateCommand();
command.CommandType = CommandType.StoredProcedure;
command.CommandText = "dbo.ObtenerProductosPorCategoria";
command.CommandTimeout = 60;
command.Parameters.Add(new SqlParameter("@Categoria", SqlDbType.NVarChar, 80)
{
Value = "Periféricos"
});
await using SqlDataReader reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
Console.WriteLine(reader["Nombre"]);
}
13. DbConnection y código independiente del proveedor
Si quieres escribir código menos acoplado a SQLite o SQL Server, puedes usar las clases base:
using System.Data.Common;
public class ConsultaService
{
private readonly DbProviderFactory _factory;
private readonly string _connectionString;
public ConsultaService(DbProviderFactory factory, string connectionString)
{
_factory = factory;
_connectionString = connectionString;
}
public async Task<int> ContarAsync()
{
await using DbConnection connection = _factory.CreateConnection()
?? throw new InvalidOperationException("No se pudo crear la conexión.");
connection.ConnectionString = _connectionString;
await connection.OpenAsync();
await using DbCommand command = connection.CreateCommand();
command.CommandText = "SELECT COUNT(*) FROM Productos;";
object? result = await command.ExecuteScalarAsync();
return Convert.ToInt32(result);
}
}
Uso con SQLite:
using Microsoft.Data.Sqlite;
ConsultaService service = new(
SqliteFactory.Instance,
"Data Source=tienda.db");
int total = await service.ContarAsync();
💡 Consejo: Esta técnica es útil en librerías, pero en aplicaciones sencillas suele ser más claro usar directamente
SqliteConnectionoSqlConnection.
14. Leer valores nulos
Las bases de datos pueden devolver NULL. En ADO.NET debes comprobarlo:
int ordinalDescripcion = reader.GetOrdinal("Descripcion");
string? descripcion = reader.IsDBNull(ordinalDescripcion)
? null
: reader.GetString(ordinalDescripcion);
Otra opción usando el indexador:
object valor = reader["Descripcion"];
string? descripcion = valor == DBNull.Value ? null : (string)valor;
15. DataTable: datos desconectados
📘 Concepto:
DataTablerepresenta una tabla en memoria. Es útil cuando quieres cargar datos, cerrar la conexión y trabajar con ellos después.
using System.Data;
public async Task<DataTable> ObtenerTablaProductosAsync()
{
DataTable table = new("Productos");
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Nombre", typeof(string));
table.Columns.Add("Precio", typeof(decimal));
await using SqliteConnection connection = CrearConexion();
await connection.OpenAsync();
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = "SELECT Id, Nombre, Precio FROM Productos ORDER BY Nombre;";
await using SqliteDataReader reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
table.Rows.Add(
reader.GetInt32(0),
reader.GetString(1),
reader.GetDecimal(2)
);
}
return table;
}
Recorrer un DataTable:
DataTable table = await repository.ObtenerTablaProductosAsync();
foreach (DataRow row in table.Rows)
{
Console.WriteLine($"{row["Id"]}: {row["Nombre"]} - {row["Precio"]}");
}
⚠️ Importante:
DataTablees cómodo, pero no es tan tipado como trabajar con clases propias. En C# moderno se suele preferir mapear a modelos comoProducto.
16. DataAdapter en SQL Server
DataAdapter es una pieza clásica de ADO.NET para llenar DataSet o DataTable. Es más habitual en aplicaciones antiguas de escritorio o WinForms.
Ejemplo con SQL Server:
using Microsoft.Data.SqlClient;
using System.Data;
DataTable table = new();
await using SqlConnection connection = new(connectionString);
using SqlDataAdapter adapter = new(
"SELECT Id, Nombre, Precio FROM Productos ORDER BY Nombre",
connection);
adapter.Fill(table);
foreach (DataRow row in table.Rows)
{
Console.WriteLine($"{row["Id"]}: {row["Nombre"]} - {row["Precio"]}");
}
17. Bulk insert sencillo
SQLite no tiene el mismo mecanismo de carga masiva que SQL Server, pero puedes insertar muchos registros de forma eficiente usando una transacción y reutilizando el comando:
public async Task InsertarMuchosAsync(List<Producto> productos)
{
await using SqliteConnection connection = CrearConexion();
await connection.OpenAsync();
using SqliteTransaction transaction = connection.BeginTransaction();
await using SqliteCommand command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = """
INSERT INTO Productos (Nombre, Categoria, Precio, Stock, FechaCreacion)
VALUES (@nombre, @categoria, @precio, @stock, @fechaCreacion);
""";
SqliteParameter nombre = command.Parameters.Add("@nombre", SqliteType.Text);
SqliteParameter categoria = command.Parameters.Add("@categoria", SqliteType.Text);
SqliteParameter precio = command.Parameters.Add("@precio", SqliteType.Real);
SqliteParameter stock = command.Parameters.Add("@stock", SqliteType.Integer);
SqliteParameter fecha = command.Parameters.Add("@fechaCreacion", SqliteType.Text);
foreach (Producto producto in productos)
{
nombre.Value = producto.Nombre;
categoria.Value = producto.Categoria;
precio.Value = producto.Precio;
stock.Value = producto.Stock;
fecha.Value = producto.FechaCreacion.ToString("O");
await command.ExecuteNonQueryAsync();
}
await transaction.CommitAsync();
}
Con SQL Server existe SqlBulkCopy, muy útil para cargas grandes:
using Microsoft.Data.SqlClient;
using SqlBulkCopy bulkCopy = new(connectionString);
bulkCopy.DestinationTableName = "Productos";
// bulkCopy.WriteToServer(dataTable);
18. Manejo de errores
try
{
int id = await repository.CrearAsync(producto);
Console.WriteLine($"Producto creado: {id}");
}
catch (SqliteException ex) when (ex.SqliteErrorCode == 19)
{
Console.WriteLine("Error de restricción: puede que el producto esté duplicado.");
}
catch (SqliteException ex)
{
Console.WriteLine($"Error SQLite: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Error inesperado: {ex.Message}");
}
Ejemplo SQL Server:
catch (SqlException ex) when (ex.Number == 2627)
{
Console.WriteLine("Clave única duplicada.");
}
💡 Consejo: Captura primero excepciones específicas del proveedor y deja
Exceptioncomo último recurso.
19. Configuración con appsettings.json
Aunque sea una aplicación de consola, puedes guardar la cadena de conexión fuera del código.
dotnet add package Microsoft.Extensions.Configuration.Json
appsettings.json:
{
"ConnectionStrings": {
"Tienda": "Data Source=tienda.db"
}
}
Lectura:
using Microsoft.Extensions.Configuration;
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.Build();
string connectionString = configuration.GetConnectionString("Tienda")
?? throw new InvalidOperationException("Falta la cadena de conexión.");
⚠️ Importante: No subas contraseñas reales a Git. Usa variables de entorno, Secret Manager o un gestor de secretos.
20. Comparativa de proveedores
| Base de datos | Paquete NuGet | Clases principales |
|---|---|---|
| SQLite | Microsoft.Data.Sqlite | SqliteConnection, SqliteCommand |
| SQL Server | Microsoft.Data.SqlClient | SqlConnection, SqlCommand |
| PostgreSQL | Npgsql | NpgsqlConnection, NpgsqlCommand |
| MySQL/MariaDB | MySqlConnector | MySqlConnection, MySqlCommand |
El patrón es casi siempre el mismo:
await using var connection = new TipoConnection(connectionString);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = "SELECT ...";
await using var reader = await command.ExecuteReaderAsync();
21. Buenas prácticas
| Práctica | Motivo |
|---|---|
| Usa parámetros siempre | Evita inyección SQL y problemas de formato |
| Abre conexiones tarde y ciérralas pronto | Aprovecha el pooling |
Usa await using | Libera recursos correctamente |
| Usa transacciones en operaciones relacionadas | Evita datos inconsistentes |
Controla CommandTimeout | Evita esperas infinitas |
| Mapea resultados en métodos pequeños | Mejora legibilidad |
No devuelvas DataReader fuera del repositorio | Mantiene controlada la conexión |
| Guarda connection strings fuera del código | Mejora seguridad y despliegue |
| Prefiere async en I/O | La app escala mejor |
| Registra errores sin filtrar secretos | Ayuda a diagnosticar sin exponer datos |
22. Ejercicios
Ejercicio 1: CRUD de clientes
Crea una tabla Clientes con Id, Nombre, Email, Telefono y FechaAlta. Implementa:
- Crear cliente
- Obtener por Id
- Buscar por email
- Actualizar teléfono
- Eliminar cliente
Ejercicio 2: Pedidos con transacción
Crea tablas Pedidos y DetallePedido. Al crear un pedido:
- Inserta la cabecera del pedido
- Inserta sus líneas
- Descuenta stock
- Si algo falla, cancela todo con rollback
Ejercicio 3: Buscador avanzado
Implementa una búsqueda de productos con filtros opcionales:
- Categoría
- Precio mínimo
- Precio máximo
- Texto en el nombre
- Paginación
Ejercicio 4: Informe con ExecuteScalar
Crea métodos para calcular:
- Total de productos
- Stock total
- Precio medio
- Producto más caro
Ejercicio 5: Importación masiva
Lee un archivo CSV de productos e insértalos en SQLite usando una transacción.
Resumen
| Tema | Idea clave |
|---|---|
| ADO.NET | Acceso directo a datos desde .NET |
| Proveedor | Librería específica para cada base de datos |
Connection | Abre la comunicación con la BD |
Command | Ejecuta SQL |
| Parámetros | Envían valores de forma segura |
ExecuteNonQuery | SQL sin filas de resultado |
ExecuteScalar | Un único valor |
ExecuteReader | Lectura de filas |
| Transacción | Confirma o cancela varias operaciones |
DataTable | Datos en memoria |
| Pooling | Reutilización eficiente de conexiones |