Introducción al Diseño del Framework
El desarrollo de aplicaciones Windows Forms (WinForms) a menudo requiere una estructura robusta para gestionar la seguridad y los permisso de los usuarios. Este artículo introduce los fundamentos de un framwork diseñado para este propósito, enfocándose inicialmente en la configuración de la base de datos y una capa de acceso a datos reutilizable, componentes esenciales para cualquier sistema de gestión de permisos.
Diseño y Creación de la Base de Datos
Para un sistema de permisos, la base de datos es el pilar central que almacena la información sobre usuarios, roles, menús y las relaciones entre ellos. A continuación, se detalla el esquema de la base de datos y el script SQL para su creación e inicialización.
Esquema de la Base de Datos
tbl_Usuarios: Almacena la información básica de los usuarios, incluyendo su ID, nombre de usuario y contraseña.tbl_Roles: Define los diferentes roles dentro del sistema (ej. Administrador, Usuario Estándar), junto con una descripción y un indicador de si es un rol de administrador.tbl_Menus: Contiene la estructura jerárquica de los menús de la aplicación, con detalles como el ID del menú, su nombre, el ID del menú padre, el nombre del formulario asociado y una posible tecla de acceso rápido.tbl_UsuariosRoles: Establece la relación muchos a muchos entre usuarios y roles, permitiendo que un usuario tenga múltiples roles.tbl_RolesMenus: Define los permisos, vinculando roles con menús específicos, lo que significa que los usuarios con un determinado rol tendrán acceso a los menús asociados.
Proceso de Creación e Inserción de Datos
El siguiente script SQL creará la base de datos SistemaPermisosDB y las tablas necesarias, además de poblar algunas tablas con datos iniciales para demostración. Ajuste las rutas de los archivos .mdf y .ldf según su entorno.
USE [master]
GO
-- Crear la base de datos
CREATE DATABASE SistemaPermisosDB
ON PRIMARY
( NAME = 'SistemaPermisosDB', FILENAME = 'C:\SQL_DATA\SistemaPermisosDB.mdf' , SIZE = 5120KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
LOG ON
( NAME = 'SistemaPermisosDB_log', FILENAME = 'C:\SQL_DATA\SistemaPermisosDB_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
GO
ALTER DATABASE [SistemaPermisosDB] SET COMPATIBILITY_LEVEL = 110
GO
IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [SistemaPermisosDB].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO
ALTER DATABASE [SistemaPermisosDB] SET ANSI_NULL_DEFAULT OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET ANSI_NULLS OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET ANSI_PADDING OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET ANSI_WARNINGS OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET ARITHABORT OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET AUTO_CLOSE OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET AUTO_CREATE_STATISTICS ON
GO
ALTER DATABASE [SistemaPermisosDB] SET AUTO_SHRINK OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET AUTO_UPDATE_STATISTICS ON
GO
ALTER DATABASE [SistemaPermisosDB] SET CURSOR_CLOSE_ON_COMMIT OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET CURSOR_DEFAULT GLOBAL
GO
ALTER DATABASE [SistemaPermisosDB] SET CONCAT_NULL_YIELDS_NULL OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET NUMERIC_ROUNDABORT OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET QUOTED_IDENTIFIER OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET RECURSIVE_TRIGGERS OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET DISABLE_BROKER
GO
ALTER DATABASE [SistemaPermisosDB] SET AUTO_UPDATE_STATISTICS_ASYNC OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET DATE_CORRELATION_OPTIMIZATION OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET TRUSTWORTHY OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET ALLOW_SNAPSHOT_ISOLATION OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET PARAMETERIZATION SIMPLE
GO
ALTER DATABASE [SistemaPermisosDB] SET READ_COMMITTED_SNAPSHOT OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET HONOR_BROKER_PRIORITY OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET RECOVERY FULL
GO
ALTER DATABASE [SistemaPermisosDB] SET MULTI_USER
GO
ALTER DATABASE [SistemaPermisosDB] SET PAGE_VERIFY CHECKSUM
GO
ALTER DATABASE [SistemaPermisosDB] SET DB_CHAINING OFF
GO
ALTER DATABASE [SistemaPermisosDB] SET FILESTREAM( NON_TRANSACTED_ACCESS = OFF )
GO
ALTER DATABASE [SistemaPermisosDB] SET TARGET_RECOVERY_TIME = 0 SECONDS
GO
EXEC sys.sp_db_vardecimal_storage_format N'SistemaPermisosDB', N'ON'
GO
USE [SistemaPermisosDB]
GO
-- Tabla de Menús
CREATE TABLE [dbo].[tbl_Menus](
[IdMenu] [int] IDENTITY(1,1) NOT NULL,
[NombreMenu] [nvarchar](50) NOT NULL,
[IdPadre] [int] NOT NULL,
[NombreFormulario] [varchar](500) NULL,
[TeclaAcceso] [nchar](10) NULL,
CONSTRAINT [PK_tbl_Menus] PRIMARY KEY CLUSTERED
(
[IdMenu] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
-- Tabla de Roles
CREATE TABLE [dbo].[tbl_Roles](
[IdRol] [int] IDENTITY(1,1) NOT NULL,
[NombreRol] [nvarchar](50) NULL,
[Descripcion] [nvarchar](500) NULL,
[EsAdmin] [bit] NOT NULL, -- Cambiado a bit para booleano
CONSTRAINT [PK_tbl_Roles] PRIMARY KEY CLUSTERED
(
[IdRol] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
-- Tabla de Relación Roles-Menús
CREATE TABLE [dbo].[tbl_RolesMenus](
[IdRolMenu] [int] IDENTITY(1,1) NOT NULL,
[IdRol] [int] NOT NULL,
[IdMenu] [int] NOT NULL,
CONSTRAINT [PK_tbl_RolesMenus] PRIMARY KEY CLUSTERED
(
[IdRolMenu] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
-- Tabla de Usuarios
CREATE TABLE [dbo].[tbl_Usuarios](
[IdUsuario] [int] IDENTITY(101,1) NOT NULL,
[NombreUsuario] [varchar](20) NOT NULL,
[Contrasena] [varchar](50) NOT NULL,
CONSTRAINT [PK_tbl_Usuarios] PRIMARY KEY CLUSTERED
(
[IdUsuario] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
-- Tabla de Relación Usuarios-Roles
CREATE TABLE [dbo].[tbl_UsuariosRoles](
[IdUsuarioRol] [int] IDENTITY(1,1) NOT NULL,
[IdUsuario] [int] NOT NULL,
[IdRol] [int] NOT NULL,
CONSTRAINT [PK_tbl_UsuariosRoles] PRIMARY KEY CLUSTERED
(
[IdUsuarioRol] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
-- Insertar datos iniciales en tbl_Menus
SET IDENTITY_INSERT [dbo].[tbl_Menus] ON
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (1, N'Administración del Sistema', 0, NULL, NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (2, N'Añadir Nuevo Menú', 1, N'sm.FrmAddMenuInfo', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (3, N'Listado de Menús', 1, N'sm.FrmMenuList', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (4, N'Listado de Roles', 1, N'sm.FrmRoleList', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (5, N'Asignación de Permisos', 1, N'sm.FrmRight', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (6, N'Listado de Usuarios', 1, N'sm.FrmUserList', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (8, N'Añadir Usuario', 6, N'sm.FrmUserInfo', NULL)
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (10, N'Gestión de Calificaciones', 0, N'', N'Alt+S ')
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (11, N'Listado de Calificaciones', 10, N'score.FrmScoreList', N' ')
INSERT [dbo].[tbl_Menus] ([IdMenu], [NombreMenu], [IdPadre], [NombreFormulario], [TeclaAcceso]) VALUES (12, N'Detalle de Calificación', 10, N'score.FrmScoreInfo', N' ')
SET IDENTITY_INSERT [dbo].[tbl_Menus] OFF
-- Insertar datos iniciales en tbl_Roles
SET IDENTITY_INSERT [dbo].[tbl_Roles] ON
INSERT [dbo].[tbl_Roles] ([IdRol], [NombreRol], [Descripcion], [EsAdmin]) VALUES (1, N'Administrador Maestro', N'Posee todos los privilegios del sistema', 1)
INSERT [dbo].[tbl_Roles] ([IdRol], [NombreRol], [Descripcion], [EsAdmin]) VALUES (3, N'Administrador de Sistema', N'Responsable de la gestión de funciones del sistema.', 0)
INSERT [dbo].[tbl_Roles] ([IdRol], [NombreRol], [Descripcion], [EsAdmin]) VALUES (4, N'Administrador de Calificaciones', N'Responsable de la gestión de la información de calificaciones', 0)
SET IDENTITY_INSERT [dbo].[tbl_Roles] OFF
-- Insertar datos iniciales en tbl_RolesMenus
SET IDENTITY_INSERT [dbo].[tbl_RolesMenus] ON
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (7, 3, 1)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (8, 3, 2)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (9, 3, 3)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (10, 3, 6)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (11, 3, 8)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (12, 4, 10)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (13, 4, 11)
INSERT [dbo].[tbl_RolesMenus] ([IdRolMenu], [IdRol], [IdMenu]) VALUES (14, 4, 12)
SET IDENTITY_INSERT [dbo].[tbl_RolesMenus] OFF
-- Insertar datos iniciales en tbl_Usuarios
SET IDENTITY_INSERT [dbo].[tbl_Usuarios] ON
INSERT [dbo].[tbl_Usuarios] ([IdUsuario], [NombreUsuario], [Contrasena]) VALUES (101, N'admin', N'admin')
INSERT [dbo].[tbl_Usuarios] ([IdUsuario], [NombreUsuario], [Contrasena]) VALUES (102, N'usuario001', N'123456')
SET IDENTITY_INSERT [dbo].[tbl_Usuarios] OFF
-- Insertar datos iniciales en tbl_UsuariosRoles
SET IDENTITY_INSERT [dbo].[tbl_UsuariosRoles] ON
INSERT [dbo].[tbl_UsuariosRoles] ([IdUsuarioRol], [IdUsuario], [IdRol]) VALUES (1, 101, 1)
INSERT [dbo].[tbl_UsuariosRoles] ([IdUsuarioRol], [IdUsuario], [IdRol]) VALUES (3, 102, 3)
SET IDENTITY_INSERT [dbo].[tbl_UsuariosRoles] OFF
-- Definir restricciones y relaciones de clave externa
ALTER TABLE [dbo].[tbl_Roles] ADD CONSTRAINT [DF_tbl_Roles_EsAdmin] DEFAULT ((0)) FOR [EsAdmin]
GO
ALTER TABLE [dbo].[tbl_RolesMenus] WITH CHECK ADD CONSTRAINT [FK_tbl_RolesMenus_tbl_Roles] FOREIGN KEY([IdRol])
REFERENCES [dbo].[tbl_Roles] ([IdRol])
GO
ALTER TABLE [dbo].[tbl_RolesMenus] CHECK CONSTRAINT [FK_tbl_RolesMenus_tbl_Roles]
GO
ALTER TABLE [dbo].[tbl_UsuariosRoles] WITH CHECK ADD CONSTRAINT [FK_tbl_UsuariosRoles_tbl_Usuarios] FOREIGN KEY([IdUsuario])
REFERENCES [dbo].[tbl_Usuarios] ([IdUsuario])
GO
ALTER TABLE [dbo].[tbl_UsuariosRoles] CHECK CONSTRAINT [FK_tbl_UsuariosRoles_tbl_Usuarios]
GO
ALTER TABLE [dbo].[tbl_UsuariosRoles] WITH CHECK ADD CONSTRAINT [FK_tbl_UsuariosRoles_tbl_Roles] FOREIGN KEY([IdRol])
REFERENCES [dbo].[tbl_Roles] ([IdRol])
GO
ALTER TABLE [dbo].[tbl_UsuariosRoles] CHECK CONSTRAINT [FK_tbl_UsuariosRoles_tbl_Roles]
GO
USE [master]
GO
ALTER DATABASE [SistemaPermisosDB] SET READ_WRITE
GO
Capa de Acceso a Datos
Una capa de acceso a datos (DAL) es crucial para desacoplar la lógica de negocio de los detalles de la base de datos. Se presenta una clase genérica para la interacción con SQL Server, facilitando operaciones CRUD y transacciones.
Clase de Acceso a Datos Genérica
La clase AccesoDatosBD es una utilidad estática que encapsula las operaciones comunes de la base de datos, como ejecutar comandos SQL o procedimientos almacenados, y recuperar datos.
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Proyecto.DAL.Utilidades
{
/// <summary>
/// Clase estática de utilidad para el acceso a datos de SQL Server.
/// </summary>
public static class AccesoDatosBD
{
/// <summary>
/// Cadena de conexión a la base de datos.
/// </summary>
private static readonly string cadenaConexion = ConfigurationManager.ConnectionStrings["cadenaConexionDB"].ConnectionString;
/// <summary>
/// Ejecuta una consulta SQL o procedimiento almacenado que no devuelve resultados (INSERT, UPDATE, DELETE).
/// Devuelve el número de filas afectadas.
/// </summary>
/// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
/// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
/// <param name="parametros">Array de parámetros SQL.</param>
/// <returns>Número de filas afectadas.</returns>
public static int EjecutarComandoNoConsulta(string consultaSql, int tipoComando, params SqlParameter[] parametros)
{
int filasAfectadas = 0;
using (SqlConnection conexion = new SqlConnection(cadenaConexion))
{
SqlCommand comando = ConstruirComandoSql(conexion, consultaSql, tipoComando, null, parametros);
filasAfectadas = comando.ExecuteNonQuery();
comando.Parameters.Clear();
}
return filasAfectadas;
}
/// <summary>
/// Ejecuta una consulta SQL y devuelve el valor de la primera columna de la primera fila.
/// Útil para obtener valores únicos o agregados (ej. COUNT, MAX).
/// </summary>
/// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
/// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
/// <param name="parametros">Array de parámetros SQL.</param>
/// <returns>El valor de la primera columna de la primera fila, o null si no hay resultados.</returns>
public static object EjecutarEscalar(string consultaSql, int tipoComando, params SqlParameter[] parametros)
{
object resultado = null;
using (SqlConnection conexion = new SqlConnection(cadenaConexion))
{
SqlCommand comando = ConstruirComandoSql(conexion, consultaSql, tipoComando, null, parametros);
resultado = comando.ExecuteScalar();
comando.Parameters.Clear();
return (resultado == null || resultado == DBNull.Value) ? null : resultado;
}
}
/// <summary>
/// Ejecuta una consulta SQL y devuelve un SqlDataReader para leer los resultados.
/// La conexión se cerrará automáticamente cuando se cierre el lector.
/// </summary>
/// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
/// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
/// <param name="parametros">Array de parámetros SQL.</param>
/// <returns>Un objeto SqlDataReader.</returns>
/// <exception cref="Exception">Lanzada si ocurre un error al crear el lector.</exception>
public static SqlDataReader EjecutarLector(string consultaSql, int tipoComando, params SqlParameter[] parametros)
{
SqlConnection conexion = new SqlConnection(cadenaConexion);
SqlCommand comando = ConstruirComandoSql(conexion, consultaSql, tipoComando, null, parametros);
try
{
return comando.ExecuteReader(CommandBehavior.CloseConnection);
}
catch (Exception ex)
{
conexion.Close();
throw new Exception("Error al obtener SqlDataReader.", ex);
}
}
/// <summary>
/// Ejecuta una consulta SQL y rellena un DataTable con los resultados.
/// Ideal para recuperar datos de una única tabla.
/// </summary>
/// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
/// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
/// <param name="parametros">Array de parámetros SQL.</param>
/// <returns>Un DataTable con los datos resultantes.</returns>
public static DataTable ObtenerTablaDatos(string consultaSql, int tipoComando, params SqlParameter[] parametros)
{
DataTable tablaDatos = new DataTable();
using (SqlConnection conexion = new SqlConnection(cadenaConexion))
{
SqlCommand comando = ConstruirComandoSql(conexion, consultaSql, tipoComando, null, parametros);
using (SqlDataAdapter adaptador = new SqlDataAdapter(comando))
{
adaptador.Fill(tablaDatos);
}
}
return tablaDatos;
}
/// <summary>
/// Ejecuta una consulta SQL y rellena un DataSet con los resultados.
/// </summary>
/// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
/// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
/// <param name="parametros">Array de parámetros SQL.</param>
/// <returns>Un DataSet con los datos resultantes.</returns>
public static DataSet ObtenerConjuntoDatos(string consultaSql, int tipoComando, params SqlParameter[] parametros)
{
DataSet conjuntoDatos = new DataSet();
using (SqlConnection conexion = new SqlConnection(cadenaConexion))
{
SqlCommand comando = ConstruirComandoSql(conexion, consultaSql, tipoComando, null, parametros);
using (SqlDataAdapter adaptador = new SqlDataAdapter(comando))
{
adaptador.Fill(conjuntoDatos);
}
}
return conjuntoDatos;
}
/// <summary>
/// Ejecuta una lista de sentencias SQL dentro de una transacción.
/// </summary>
/// <param name="listaSql">Lista de cadenas SQL a ejecutar.</param>
/// <returns>True si la transacción se completó exitosamente, false en caso contrario.</returns>
/// <exception cref="Exception">Lanzada si la transacción falla y se revierte.</exception>
public static bool EjecutarTransaccion(List<string> listaSql)
{
using (SqlConnection conexion = new SqlConnection(cadenaConexion))
{
conexion.Open();
SqlTransaction transaccion = conexion.BeginTransaction();
SqlCommand comando = ConstruirComandoSql(conexion, "", (int)CommandType.Text, transaccion);
try
{
foreach (string sqlItem in listaSql)
{
if (!string.IsNullOrWhiteSpace(sqlItem))
{
comando.CommandText = sqlItem;
comando.ExecuteNonQuery();
}
}
transaccion.Commit();
return true;
}
catch (Exception ex)
{
transaccion.Rollback();
throw new Exception("Error al ejecutar la transacción de comandos SQL.", ex);
}
}
}
/// <summary>
/// Ejecuta una lista de objetos CommandInfo dentro de una transacción.
/// Permite ejecutar diferentes tipos de comandos (SQL o procedimientos) con sus respectivos parámetros.
/// </summary>
/// <param name="listaComandos">Lista de objetos CommandInfo.</param>
/// <returns>True si la transacción se completó exitosamente, false en caso contrario.</returns>
/// <exception cref="Exception">Lanzada si la transacción falla y se revierte.</exception>
public static bool EjecutarTransaccionComandos(List<InformacionComando> listaComandos)
{
using (SqlConnection conexion = new SqlConnection(cadenaConexion))
{
conexion.Open();
SqlTransaction transaccion = conexion.BeginTransaction();
SqlCommand comando = ConstruirComandoSql(conexion, "", (int)CommandType.Text, transaccion); // Inicialmente como texto
try
{
foreach (InformacionComando infoComando in listaComandos)
{
comando.CommandText = infoComando.TextoComando;
comando.CommandType = infoComando.EsProcedimientoAlmacenado ? CommandType.StoredProcedure : CommandType.Text;
comando.Parameters.Clear(); // Limpiar parámetros para cada comando
if (infoComando.Parametros != null && infoComando.Parametros.Length > 0)
{
comando.Parameters.AddRange(infoComando.Parametros);
}
comando.ExecuteNonQuery();
}
transaccion.Commit();
return true;
}
catch (Exception ex)
{
transaccion.Rollback();
throw new Exception("Error al ejecutar la transacción con objetos CommandInfo.", ex);
}
}
}
/// <summary>
/// Ejecuta una operación personalizada dentro de una transacción, usando un delegado Func.
/// Permite una mayor flexibilidad para operaciones complejas que requieren múltiples interacciones con la base de datos.
/// </summary>
/// <typeparam name="T">Tipo de retorno del delegado Func.</typeparam>
/// <param name="accion">Delegado Func que encapsula la lógica de la transacción.</param>
/// <returns>El resultado de la operación definida en el delegado.</returns>
public static T EjecutarTransaccionPersonalizada<T>(Func<IDbCommand, T> accion)
{
using (IDbConnection conexion = new SqlConnection(cadenaConexion))
{
conexion.Open();
IDbTransaction transaccion = conexion.BeginTransaction();
IDbCommand comando = conexion.CreateCommand();
comando.Transaction = transaccion;
try
{
T resultado = accion(comando);
transaccion.Commit();
return resultado;
}
catch (Exception ex)
{
transaccion.Rollback();
throw new Exception("Error al ejecutar la transacción personalizada.", ex);
}
}
}
/// <summary>
/// Método auxiliar para construir un objeto SqlCommand.
/// </summary>
/// <param name="conexion">Objeto de conexión SQL.</param>
/// <param name="consultaSql">Sentencia SQL o nombre del procedimiento almacenado.</param>
/// <param name="tipoComando">Tipo de comando (1: Texto, 2: Procedimiento Almacenado).</param>
/// <param name="transaccion">Objeto de transacción SQL (opcional).</param>
/// <param name="parametros">Array de parámetros SQL (opcional).</param>
/// <returns>Un objeto SqlCommand configurado.</returns>
private static SqlCommand ConstruirComandoSql(SqlConnection conexion, string consultaSql, int tipoComando, SqlTransaction transaccion, params SqlParameter[] parametros)
{
if (conexion == null) throw new ArgumentNullException(nameof(conexion), "El objeto de conexión no puede ser nulo.");
SqlCommand comando = new SqlCommand(consultaSql, conexion);
comando.CommandType = (tipoComando == 2) ? CommandType.StoredProcedure : CommandType.Text;
if (conexion.State == ConnectionState.Closed)
{
conexion.Open();
}
if (transaccion != null)
{
comando.Transaction = transaccion;
}
if (parametros != null && parametros.Length > 0)
{
comando.Parameters.AddRange(parametros);
}
return comando;
}
}
}
Clase de Información de Comando para Transacciones
La clase InformacionComando se utiliza para encapsular los detalles de un comando SQL o procedimiento almacenado que forma parte de una transacción, permitiendo un manejo más estructurado de operaciones por lotes.
using System.Data.Common; // Para DbParameter
namespace Proyecto.DAL.Utilidades
{
/// <summary>
/// Encapsula la información de un comando SQL o procedimiento almacenado.
/// Útil para operaciones transaccionales por lotes.
/// </summary>
public class InformacionComando
{
public string TextoComando { get; set; } // Sentencia SQL o nombre del procedimiento
public DbParameter[] Parametros { get; set; } // Lista de parámetros para el comando
public bool EsProcedimientoAlmacenado { get; set; } // Indica si es un procedimiento almacenado
public InformacionComando() { }
public InformacionComando(string textoComando, bool esProcedimiento)
{
TextoComando = textoComando;
EsProcedimientoAlmacenado = esProcedimiento;
}
public InformacionComando(string textoComando, bool esProcedimiento, DbParameter[] parametros)
{
TextoComando = textoComando;
EsProcedimientoAlmacenado = esProcedimiento;
Parametros = parametros;
}
}
}
Configuración de la Cadena de Conexión
La cadena de conexión a la base de datos se almacena en el archivo de configuración de la aplicación (App.config o Web.config).
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name ="cadenaConexionDB" connectionString ="server=.;database=SistemaPermisosDB;uid=sa;pwd=123;" providerName="System.Data.SqlClient"/>
</connectionStrings>
</configuration>
Clases de Utilidad Comunes
Un conjunto de clases de utilidad puede simplificar tareas repetitivas. A continuación, se presenta una clase estática para el manejo de cadenas y conversiones de tipo.
Utilidades de Cadena y Conversión
La clase UtilidadesCadena proporciona métodos de extensión para la conversión segura de tipos y manipulación de cadenas, mejorando la robustez de la aplicación.
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq; // Necesario para algunas operaciones de LINQ si se usaran en el futuro
namespace Proyecto.Comun
{
/// <summary>
/// Clase estática con métodos de extensión para utilidades de cadena y conversión de tipos.
/// </summary>
public static class UtilidadesCadena
{
/// <summary>
/// Convierte una cadena a un entero de forma segura.
/// Devuelve 0 si la conversión falla.
/// </summary>
/// <param name="valorCadena">Cadena a convertir.</param>
/// <returns>Valor entero o 0.</returns>
public static int AEntero(this string valorCadena)
{
int resultado = 0;
int.TryParse(valorCadena, out resultado);
return resultado;
}
/// <summary>
/// Convierte una cadena a un decimal de forma segura.
/// Devuelve 0 si la conversión falla.
/// </summary>
/// <param name="valorCadena">Cadena a convertir.</param>
/// <returns>Valor decimal o 0.</returns>
public static decimal ADecimal(this string valorCadena)
{
decimal resultado = 0;
decimal.TryParse(valorCadena, out resultado);
return resultado;
}
/// <summary>
/// Convierte un objeto a un entero de forma segura.
/// Devuelve 0 si la conversión falla.
/// </summary>
/// <param name="valorObjeto">Objeto a convertir.</param>
/// <returns>Valor entero o 0.</returns>
public static int AEntero(this object valorObjeto)
{
int resultado = 0;
try
{
resultado = Convert.ToInt32(valorObjeto);
}
catch
{
resultado = 0; // En caso de error de conversión, se devuelve 0.
}
return resultado;
}
/// <summary>
/// Divide una cadena por un delimitador y devuelve una lista de cadenas.
/// </summary>
/// <param name="cadenaFuente">La cadena original.</param>
/// <param name="delimitador">El carácter delimitador.</param>
/// <param name="aMinusculas">Indica si las cadenas resultantes deben convertirse a minúsculas.</param>
/// <returns>Una lista de cadenas.</returns>
public static List<string> ObtenerListaCadenas(this string cadenaFuente, char delimitador, bool aMinusculas)
{
List<string> lista = new List<string>();
string[] partes = cadenaFuente.Split(delimitador);
foreach (string parte in partes)
{
if (!string.IsNullOrWhiteSpace(parte) && parte != delimitador.ToString())
{
string valor = parte;
if (aMinusculas)
{
valor = parte.ToLower();
}
lista.Add(valor);
}
}
return lista;
}
/// <summary>
/// Divide una cadena por comas y devuelve un array de cadenas.
/// </summary>
/// <param name="cadenaFuente">La cadena original.</param>
/// <returns>Un array de cadenas.</returns>
public static string[] ObtenerArrayCadenas(this string cadenaFuente)
{
return cadenaFuente.Split(new Char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
/// <summary>
/// Concatena una lista de cadenas usando un delimitador específico.
/// </summary>
/// <param name="listaCadenas">La lista de cadenas.</param>
/// <param name="delimitador">El delimitador a usar.</param>
/// <returns>Una única cadena unida.</returns>
public static string UnirListaCadenas(this List<string> listaCadenas, string delimitador)
{
return string.Join(delimitador, listaCadenas);
}
/// <summary>
/// Concatena una lista de enteros usando una coma como delimitador.
/// </summary& /// <param name="listaEnteros">La lista de enteros.</param>
/// <returns>Una cadena de enteros separados por comas.</returns>
public static string UnirListaEnteros(this List<int> listaEnteros)
{
return string.Join(",", listaEnteros);
}
/// <summary>
/// Obtiene una cadena de valores de un diccionario (int, int) separados por comas.
/// </summary>
/// <param name="diccionario">El diccionario de enteros.</param>
/// <returns>Una cadena con los valores del diccionario.</returns>
public static string ObtenerValoresDiccionario(this Dictionary<int, int> diccionario)
{
if (diccionario == null || !diccionario.Any())
{
return string.Empty;
}
return string.Join(",", diccionario.Values);
}
/// <summary>
/// Elimina la última coma de una cadena, si existe.
/// </summary>
/// <param name="cadena">La cadena a procesar.</param>
/// <returns>La cadena sin la última coma.</returns>
public static string EliminarUltimaComa(this string cadena)
{
if (string.IsNullOrEmpty(cadena) || !cadena.Contains(',')) return cadena;
return cadena.TrimEnd(',',' '); // Mejor usar TrimEnd para espacios y comas.
}
/// <summary>
/// Elimina el último carácter específico de una cadena, si existe.
/// </summary>
/// <param name="cadena">La cadena a procesar.</param>
/// <param name="caracter">El carácter a eliminar del final.</param>
/// <returns>La cadena sin el último carácter especificado.</returns>
public static string EliminarUltimoCaracterEspecifico(this string cadena, string caracter)
{
if (string.IsNullOrEmpty(cadena) || !cadena.EndsWith(caracter)) return cadena;
return cadena.Substring(0, cadena.LastIndexOf(caracter));
}
/// <summary>
/// Divide una cadena por un delimitador y devuelve una lista de subcadenas únicas.
/// Elimina duplicados y cadenas vacías.
/// </summary>
/// <param name="cadenaFuente">La cadena original.</param>
/// <param name="delimitador">El carácter delimitador.</param>
/// <returns>Una lista de subcadenas únicas.</returns>
public static List<string> ObtenerSubcadenasUnicas(string cadenaFuente, char delimitador)
{
return cadenaFuente.Split(delimitador, StringSplitOptions.RemoveEmptyEntries)
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s.Trim())
.Distinct()
.ToList();
}
/// <summary>
/// Calcula la longitud de una cadena, tratando los caracteres multi-byte (ej. caracteres chinos) como dos unidades.
/// </summary>
/// <param name="cadena">La cadena de entrada.</param>
/// <returns>La longitud calculada.</returns>
public static int LongitudCadena(string cadena)
{
Encoding ascii = Encoding.ASCII;
int longitudCalculada = 0;
byte[] bytes = ascii.GetBytes(cadena);
foreach (byte b in bytes)
{
if (b == 63) // 63 es el código ASCII para '?', que a menudo representa un caracter no ASCII.
longitudCalculada += 2;
else
longitudCalculada += 1;
}
return longitudCalculada;
}
/// <summary>
/// Recorta una cadena a una longitud máxima especificada.
/// Añade puntos suspensivos (...) si la cadena original es más larga.
/// </summary>
/// <param name="cadena">La cadena a recortar.</param>
/// <param name="longitudMaxima">La longitud máxima deseada.</param>
/// <returns>La cadena recortada.</returns>
public static string RecortarCadena(string cadena, int longitudMaxima)
{
if (string.IsNullOrEmpty(cadena) || cadena.Length <= longitudMaxima)
{
return cadena;
}
// Ajustar longitud máxima para caracteres multi-byte si es necesario.
// Para simplicidad, se asume longitudCaracter = 1 para este método en el contexto de C# Substring.
string cadenaRecortada = cadena.Substring(0, longitudMaxima);
// Si la longitud original es mayor, añadir puntos suspensivos.
return cadenaRecortada + (cadena.Length > longitudMaxima ? "…" : "");
}
/// <summary>
/// Elimina los espacios en blanco del principio y el final de una cadena.
/// </summary>
/// <param name="cadena">La cadena a limpiar.</param>
/// <returns>La cadena sin espacios en los extremos.</returns>>
public static string RecortarEspacios(this string cadena)
{
return cadena.Trim();
}
}
}