Integración de Procedimientos Almacenados en Entity Framework Core

Entity Framework Core (EF Core) es un mapeador objeto-relacional (ORM) ampliamente utilizado que simplifica la interacción con bases de datos relacionales al permitir a los desarrolladores trabajar con objetos C# en lugar de consultas SQL directas. Sin embargo, en ciertos escenarios, el uso de procedimientos almacenados puede ser preferible o incluso necesario, por razones como el rendimiento optimizado, la encapsulación de lógica de negocio compleja, o el cumplimiento de políticas de base de datos existentes. Este artículo explora cómo integrar procedimientos almacenados con EF Core, cubriendo tanto consultas sin parámetros como con parámetros, y la ejecución de comandos SQL.

Definición de un Procedimiento Almacenado Simple

Comenzaremos con un procedimiento almacenado básico diseñado para recuperar todos los registros de una tabla de productos, sin requerir ningún parámetro. Supongamos que tenemos una tabla llamada Productos con campos como ProductoId, NombreProducto, UnidadesEnStock y PrecioUnitario.


CREATE PROCEDURE [dbo].[SP_ObtenerTodosLosProductos]
AS
BEGIN
    SELECT ProductoId, NombreProducto, UnidadesEnStock, PrecioUnitario
    FROM Productos;
END;
GO

Preparación del Entorno Entity Framework Core

Para interactuar con la base de datos utilizando EF Core, necesitamos un DbContext y clases de modelo que representen nuestras tablas. Si ya dispones de una base de datos existente, puedes generar estos componentes mediante el comando scaffold de EF Core. Por ejemplo, para una base de datos SQL Server:


Scaffold-DbContext "Server=(localdb)\mssqllocaldb;Database=TiendaEFDB;Integrated Security=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir DatosModelos

Este comando creará una clase DbContext (por ejemplo, TiendaEFDBContext) y las clases de entidad correspondientes (como Producto) dentro del directorio DatosModelos.

Invocación de Procedimientos Almacenados sin Parámetros

EF Core permite ejecutar procedimientos almacenados cuyos resultados se mapean a una entidad existente. Esto se logra utilizando los métodos FromSqlRaw o FromSqlInterpolated en un DbSet. A continuación, un ejemplo de cómo invocar nuestro procedimiento SP_ObtenerTodosLosProductos desde un controlador de API de ASP.NET Core:


using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MiAplicacion.DatosModelos; // Ajusta el namespace de tu DbContext y entidades

namespace MiAplicacion.Controladores
{
    [ApiController]
    [Route("api/productos")]
    public class ProductosQueryController : ControllerBase
    {
        private readonly TiendaEFDBContext _dbContext;

        public ProductosQueryController(TiendaEFDBContext dbContext)
        {
            _dbContext = dbContext;
        }

        [HttpGet("todos")]
        public async Task<ActionResult<IEnumerable<Producto>>> RecuperarTodosLosProductos()
        {
            // FromSqlRaw ejecuta SQL directo y mapea los resultados a la entidad 'Producto'.
            // Es crucial que los nombres de las columnas del SP coincidan con las propiedades de la entidad.
            var listaProductos = await _dbContext.Productos
                                                 .FromSqlRaw("EXEC [dbo].[SP_ObtenerTodosLosProductos]")
                                                 .ToListAsync();
            return Ok(listaProductos);
        }
    }
}

Es esencial que el conjunto de columnas devuelto por el procedimiento almacenado coincida con las propiedades definidas en la clase de entidad Producto para un mapeo corrceto por parte de EF Core.

Ejecución de Procedimientos Almacenados con Parámetros

Los procedimientos almacenados a menudo requieren parámetros para realizar operaciones específicas o filtrar datos. Modifiquemos un procedimiento para que acepte un identificador de producto:


ALTER PROCEDURE [dbo].[SP_ObtenerProductoPorId]
    @identificador INT
AS
BEGIN
    SELECT ProductoId, NombreProducto, UnidadesEnStock, PrecioUnitario
    FROM Productos
    WHERE ProductoId = @identificador;
END;
GO

Para pasar parámetros desde EF Core, se puede emplear la clase SqlParameter junto con FromSqlRaw, o la interpolación de cadenas directamente con FromSqlInterpolated, que ofrece una sintaxis más limpia y segura contra inyecciones SQL.


using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient; // Necesario para SqlParameter
using Microsoft.EntityFrameworkCore;
using MiAplicacion.DatosModelos;

namespace MiAplicacion.Controladores
{
    [ApiController]
    [Route("api/productos")]
    public class ProductosQueryController : ControllerBase
    {
        private readonly TiendaEFDBContext _dbContext;

        public ProductosQueryController(TiendaEFDBContext dbContext)
        {
            _dbContext = dbContext;
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<Producto>> RecuperarProductoPorId(int id)
        {
            // Opción 1: Usando SqlParameter para pasar el parámetro.
            var paramId = new SqlParameter("@identificador", id);
            var productoPorId = await _dbContext.Productos
                                                 .FromSqlRaw("EXEC [dbo].[SP_ObtenerProductoPorId] @identificador", paramId)
                                                 .FirstOrDefaultAsync();

            // Opción 2: Usando FromSqlInterpolated para una sintaxis más concisa y segura.
            // EF Core maneja la creación de SqlParameter internamente.
            // var productoPorId = await _dbContext.Productos
            //                                      .FromSqlInterpolated($"EXEC [dbo].[SP_ObtenerProductoPorId] {id}")
            //                                      .FirstOrDefaultAsync();
            
            if (productoPorId == null)
            {
                return NotFound($"Producto con ID {id} no encontrado.");
            }
            return Ok(productoPorId);
        }
    }
}

Ejecución de Comandos SQL no de Consulta

Para procedimeintos almacenados o comandos SQL que no devuelven conjuntos de resultados mapeados a entidades (por ejemplo, operaciones de inserción, actualización, eliminación o procedimientos que solo modifican datos), EF Core ofrece los métodos ExecuteSqlRawAsync o ExecuteSqlInterpolatedAsync, accesibles a través de la propiedad Database del DbContext. Estos métodos devuelven el número de filas afectadas por la operación.


using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using MiAplicacion.DatosModelos;

namespace MiAplicacion.Controladores
{
    [ApiController]
    [Route("api/administracion/productos")]
    public class ProductosAdminController : ControllerBase
    {
        private readonly TiendaEFDBContext _dbContext;

        public ProductosAdminController(TiendaEFDBContext dbContext)
        {
            _dbContext = dbContext;
        }

        [HttpPut("actualizar-stock/{id}")]
        public async Task<ActionResult> ModificarStockProducto(int id, int nuevoStock)
        {
            // Supongamos que tenemos un SP para actualizar el stock:
            // CREATE PROCEDURE [dbo].[SP_ActualizarStockProducto] @p_id INT, @p_stock INT AS UPDATE Productos SET UnidadesEnStock = @p_stock WHERE ProductoId = @p_id;
            
            // Usando ExecuteSqlRawAsync con un array de SqlParameter
            var parametrosSql = new[]
            {
                new SqlParameter("@p_id", id),
                new SqlParameter("@p_stock", nuevoStock)
            };
            
            int filasModificadas = await _dbContext.Database
                                                 .ExecuteSqlRawAsync("EXEC [dbo].[SP_ActualizarStockProducto] @p_id, @p_stock", parametrosSql);

            // Alternativamente, usando ExecuteSqlInterpolatedAsync para mayor claridad y seguridad:
            // int filasModificadas = await _dbContext.Database
            //                                      .ExecuteSqlInterpolatedAsync($"EXEC [dbo].[SP_ActualizarStockProducto] {id}, {nuevoStock}");

            if (filasModificadas > 0)
            {
                return Ok($"Stock del producto {id} actualizado exitosamente. Filas afectadas: {filasModificadas}");
            }
            return NotFound($"No se encontró el producto {id} o el stock no pudo ser actualizado.");
        }
    }
}

Etiquetas: Entity Framework Core Procedimientos Almacenados SQL Server C# ASP.NET Core

Publicado el 6-1 09:36