Autor: irobles

Iterando datos en SQL Server

Cuando he trabajado con procedimientos almacenados en SQL Server, comúnmente me surgía la necesidad de iterar registros y lo hacia con tablas temporales y cursores.

A groso modo lo que tenia que hacer era lo siguiente:

  1. Crear una tabla temporal (#Temp)
  2. Insertar los datos a procesar
  3. Declarar un cursor
  4. Recorrer los registros uno por uno
  5. Cerrar y liberar el cursor
  6. Eliminar la tabla temporal

Por lo general este método no presentaba problemas pero sí me llegó a pasar que en algún procedimiento tenia que aplicar lógica de negocio durante el iterado de los datos y esto me generaba problemas porque no podía terminar la ejecución sin eliminar la tabla temporal ni cerrar el cursor y esto representaba un inconveniente por lo siguiente:

  • Es fácil olvidar eliminar la tabla temporal
  • Existe el riesgo de dejar cursores abiertos
  • El código se vuelve más complejo y difícil de mantener
  • Puede afectar el rendimiento si no se maneja correctamente

Buscando alternativas más limpias, encontré una forma mucho más sencilla y controlada de hacerlo.

Una alternativa más simple: variables tipo tabla + WHILE

En lugar de usar tablas temporales y cursores, podemos utilizar una variable de tipo tabla junto con una columna IDENTITY para simular un índice, y recorrer los registros usando un ciclo WHILE.

Ejemplo

Supongamos que queremos iterar los miembros de un grupo de personas,

SQL
DECLARE @grupoId INT = 28
	, @idxMiembro INT = 0 -- El indice debe comenzar en 0
	, @NumMiembros INT = 0

DECLARE @miembrosDeGrupo TABLE (
	idx INT NOT NULL IDENTITY,
	clienteId BIGINT NOT NULL,
	nombreCliente VARCHAR(500) NOT NULL
)

-- Se obtienen los datos a iterar
INSERT INTO @miembrosDeGrupo
SELECT cl.clienteId, cl.nombreCompleto
FROM MiembrosGrupos mg
INNER JOIN clientes cl
ON mg.clienteId = cl.clienteId
WHERE grupoId = @grupoId

-- Se obtiene el numero de datos a iterar
SELECT @NumMiembros = COUNT(*)
FROM @miembrosDeGrupo

WHILE @idxMiembro <= @NumMiembros
BEGIN
	-- Obtenemos los elementos por indice
	SELECT * FROM @miembrosDeGrupo WHERE idx = @idxMiembro

	-- No olvidar incrementar el indice
	SET @idxMiembro += 1
END

¿Por qué este enfoque es mejor?

Este método tiene varias ventajas importantes:

1. No necesitas limpiar nada

Las variables de tipo tabla viven únicamente dentro del scope del procedimiento y no tienes que preocuparte por eliminarlas.

2. Evitas cursores

Los cursores son poderosos, pero también propensos a errores y problemas de rendimiento si no se usan correctamente.

3. Código más limpio y fácil de mantener

El uso de WHILE con un índice hace que la lógica sea más clara y controlada.

4. Menor riesgo de errores

No hay cursores abiertos ni recursos pendientes por liberar.

Consideraciones importantes

Aunque este enfoque es muy útil, hay algunos puntos que debes tener en cuenta:

  • Asegúrate de inicializar correctamente el índice (@idxMiembro)
  • Controla bien el límite del ciclo (@NumMiembros)
  • Este patrón es ideal para conjuntos de datos pequeños o medianos

⚠️ Para grandes volúmenes de datos, siempre vale la pena evaluar si puedes resolver el problema con operaciones basadas en conjuntos (set-based), que suelen ser más eficientes en SQL Server.

Conclusión

El uso de variables tipo tabla con IDENTITY y ciclos WHILE es una alternativa sencilla y efectiva para iterar datos sin recurrir a cursores ni tablas temporales.

No reemplaza todos los escenarios, pero definitivamente es una herramienta más que vale la pena tener en tu arsenal como desarrollador.

Unit of Work con Dapper

Como mencioné en el post anterior, recientemente comencé a utilizar Dapper para algunos proyectos en mi trabajo y a causa de ellos tuve la necesidad de migrar algunas operaciones al otro sistema y para ello se deben sincronizar datos entre los dos sistemas.

Contexto

Estos datos que necesitaba sincronizar son listas pero existen algunas que dependen de otra lista, por ejemplo, si se tratara de un punto de venta es probable tengamos entidades como Caja y ésta dependería de Sucursal.

Bajo éste escenario el procedimiento a seguir es el siguiente:

  1. Se tienen dos sistemas a sincronizar (S1 y S2)
  2. Se creará una aplicación de consola que recabará las listas de S1 y las enviará a S2
  3. S2 contará con un WebApi para recibir las listas enviadas por S1

Bien ahora, desde el punto de vista de S2 tenemos los siguientes escenarios:

  1. El elemento recibido es nuevo y hay que registrarlo
  2. El elemento ya existe y se debe actualizar en S2

Desglosando el Problema

Como mencioné anteriormente, hay listas que dependen de otra y esto nos genera un caso “especial” ya que si se agregan elementos nuevos a una lista que tiene dependencias y estas mismas aun no se han registrado esto genera un conflicto en S2 al tratar de registrar el elemento recibido.

Ejemplo: Supongamos que en S1 existen las entidades Sucursal y Caja, se entiende que la caja pertenece a alguna sucursal, dicho en otras palabras, la caja depende de la sucursal, esto implica que para poder registrar una caja ya debe estar registrada la sucursal a la que pertenece, entonces: ¿Qué pasaría si recibimos una caja de una sucursal nueva y aun no se registra la sucursal?, obviamente esto es un error que se soluciona de dos formas:

  1. Asegurarnos de que la sincronización dé prioridad a las listas que tengan dependencias.
  2. Si el elemento recibido tiene una dependencia que no existe, la dependencia se registra “en blanco” y al recibir la lista de la que depende se actualizaría la dependencia “en blanco”.

Para éste caso donde la sincronización será automática no creo conveniente seguir el camino de la opción número 1 ya que si el sistema crece podríamos toparnos con referencias circulares (cosa que ya me ha pasado anteriormente) y esto provocaría otro problema, así que opté por seguir la opción número 2.

Patrón Unit of Work

Ahora que he decidido registrar las dependencias “en blanco” necesito hacer las inserciones de las dependencias en una transacción y para ello utilizaré el patrón UnitOfWork y para ello usare la siguiente clase:

C#
public class UnitOfWork : IDisposable
{
    public IDbConnection Connection { get; }
    public IDbTransaction Transaction { get; }

    public UnitOfWork()
    {
        Connection = Repository.GetConnection();
        Connection.Open();
        Transaction = Connection.BeginTransaction();
    }

    public void Commit() => Transaction.Commit();
    public void Rollback() => Transaction.Rollback();

    public void Dispose()
    {
        Transaction?.Dispose();
        Connection?.Dispose();
    }
}

Esta clase será la encargada crear la conexión a la base de datos y de hacer commit o rollback a la transacción.

A continuación les muestro como usar la clase UnitOfWork:

C#
public static Result<long?> InsertCaja(CajaRequest request)
{
    using (var unit = new UnitOfWork())
    {
        try
        {
            Result<long?> result = InsertCajaInterna(request, unit.Connection, unit.Transaction);
            unit.Commit();
            return result;
        }
        catch (Exception ex)
        {
            unit.Rollback();
            log.Error($"Message: {ex.Message} | InnerException: {ex.InnerException} | StackTrace: {ex.StackTrace}");
            return Result<long?>.Fail("Ocurrio un error al registrar la caja");
        }
    }
}

Como se puede ver creé una función “Interna” pasando como parámetros tanto la conexión como la transacción, de esta manera todas las operaciones de base de datos se harán en conjunto.

NOTA: Estoy utilizando la clase Result para manejar las respuestas de las operaciones, su código es el siguiente:

C#
public class Result
{
    protected Result() { }

    public bool Success { get; protected set; }
    public string Message { get; protected set; }

    public static Result Ok(string message = "") =>
        new Result { Success = true, Message = message };

    public static Result Fail(string message) =>
        new Result { Success = false, Message = message };
}

public class Result<T> : Result
{
    public T Data { get; set; } = default;

    public static Result<T> Ok(T data, string message = "")
        => new Result<T> { Success = true, Message = message, Data = data };

    public new static Result<T> Fail(string message)
        => new Result<T> { Success = false, Message = message, Data = default };
}

A continuación te muestro el código de la función InsertCajaInterna:

C#
private static Result<long?> InsertCajaInterna(CajaRequest request, IDbConnection con, IDbTransaction trx)
{
    long? sucursalId = null;

    // Se obtiene la sucursal
    sucursalId = con.ExecuteScalar<long?>(
        "SELECT SucursalId FROM Sucursal WHERE IdExterno = @IdExternoSucursal"
        , new { IdExternoSucursal = request.sucursalId }, trx);

    // Si no existe la sucursal se registra en blanco
    if (!sucursalId.HasValue)
    {
        Result<long?> resultSucursal = InsertSucursalInterna(new SucursalRequest
            { idExterno = request.sucursalId, IsEmptiInsert = true }, con, trx);

        sucursalId = resultSucursal.Data;

        if (!sucursalId.HasValue)
            return Result<long?>.Fail("No se pudo identificar la sucursal");
    }

    // Se valida si existe una caja con el mismo IdExterno
    Result<long?> cajaId = con.ExecuteScalar<long?>("SELECT CajaId FROM Caja WHERE IdExterno = @IdExterno",
        new { IdExterno = request.idExterno }, trx);

    if (!cajaId.HasValue)
    {
        // Se inserta la caja nueva
        cajaId = con.ExecuteScalar<long?>(
            "INSERT INTO Caja (IdExterno, NombreCaja, SucursalId) " +
            " VALUES (@IdExterno, UPPER(@NombreCaja), @SucursalId); " +
            "SELECT SCOPE_IDENTITY();"
            , new
            {
                IdExterno = request.idExterno,
                NombreCaja = request.nombreCaja,
                SucursalId = sucursalId.Value
            }, trx);
    }
    else
    {
        // Se actualiza la caja existente
        con.Execute(
            "UPDATE Caja SET NombreCaja = UPPER(@NombreCaja), SucursalId = @SucursalId " +
            "WHERE IdExterno = @IdExterno "
            , new
            {
                IdExterno = request.idExterno,
                NombreCaja = request.nombreCaja,
                SucursalId = sucursalId.Value
            }, trx);
    }

    log.Info($"CajaId: {cajaId} | Nombre: '{request.nombreCaja}' | SucursalId: {sucursalId} | IdExterno: {request.idExterno}");

    return Result<long?>.Ok(cajaId, "Caja registrada correctamente");
}

De esta manera he conseguido insertar la caja y su dependencia en una transacción para mantener la integridad de los datos.

¿Cuándo usar Unit of Work?

  1. Cuando se requieren múltiples operaciones dependientes entre sí.
  2. Procesos de negocio complejos.

Conclusión

Implementar el patrón Unit of Work con Dapper no es obligatorio, pero existen escenarios donde se requiere que múltiples operaciones se ejecuten en conjunto, y es aquí donde se vuelve una pieza clave para mantener la integridad de los datos.

Usando Dapper con C# y SQL Server

Recientemente en mi trabajo iniciamos el desarrollo de una aplicación donde varios desarrolladores íbamos a estar trabajando al mismo tiempo, cada quien en módulos diferentes. Y como suele pasar esto implica cambios constantes en la base de datos.

Si has trabajado con Entity Framework en equipo, probablemente ya te has topado con el problema de que el DbContext empieza a desincronizarse, migraciones que chocan, modelos que ya no representan la realidad de la base de datos y terminas perdiendo tiempo en cosas que no deberían ser problema.

Así que decidí buscar una alternativa más simple, más directa y que no presentara este problema. Ahí fue donde me encontré con Dapper.

¿Qué es Dapper?

Dapper es un micro ORM desarrollado por el equipo de Stack Overflow que diferencia de Entity Framework, aquí no hay magia pesada ni abstracciones complejas, escribes tu SQL y Dapper se encarga de mapear los resultados a tus objetos.

¿Por qué decidí usarlo?

En mi caso, lo que necesitaba era:

  • Evitar problemas de sincronización entre modelo y base de datos
  • Tener control total sobre las consultas
  • Algo rápido de implementar
  • Que funcionara bien trabajando en equipo

Aplicación de Control Gastos

Para probar Dapper decidí hacer una aplicación ultra sencilla para registrar gastos ya que necesitaba aprender a usarlo antes de implementarlo en un proyecto en mi trabajo.

Instalación

Nada complicado, como cualquier paquete de NuGet:

Install-Package Dapper

Y para SQL Server:

dotnet add package Microsoft.Data.SqlClient

Configuración básica

Yo normalmente manejo una clase base para obtener la conexión, algo así:

C#
public class BaseRepository
{
    private readonly IConfiguration _configuration;

    public BaseRepository(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    protected SqlConnection GetConnection()
    {
        return new SqlConnection(_configuration.GetConnectionString("DefaultConnection"));
    }
}

Esto me permite reutilizar la conexión en todos mis repositorios.

Modelo de ejemplo

C#
public class Usuario
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public string Email { get; set; }
}

SELECT

Aquí es donde Dapper brilla por su simplicidad.

C#
public Usuario[] GetUsuarios()
{
    using var con = GetConnection();

    return con.Query<Usuario>("SELECT * FROM Usuarios")
              .ToArray();
}

Con parámetros:

C#
public Usuario? GetUsuarioById(int id)
{
    using var con = GetConnection();

    return con.QueryFirstOrDefault<Usuario>(
        "SELECT * FROM Usuarios WHERE Id = @Id",
        new { Id = id });
}

INSERT

C#
public void InsertUsuario(Usuario usuario)
{
    using var con = GetConnection();

    con.Execute(
        "INSERT INTO Usuarios (Nombre, Email) VALUES (@Nombre, @Email)",
        usuario);
}

UPDATE

C#
public void UpdateUsuario(Usuario usuario)
{
    using var con = GetConnection();

    con.Execute(
        @"UPDATE Usuarios 
          SET Nombre = @Nombre, Email = @Email 
          WHERE Id = @Id",
        usuario);
}

DELETE

C#
public void DeleteUsuario(int id)
{
    using var con = GetConnection();

    con.Execute(
        "DELETE FROM Usuarios WHERE Id = @Id",
        new { Id = id });
}

Transacciones

Aquí es donde ya se pone interesante, porque puedes hacer exactamente lo que necesitas sin pelearte con el ORM.

En uno de mis repositorios, por ejemplo, manejo algo así:

  • Inserto una cuenta
  • Si tiene saldo inicial, inserto también un movimiento
  • Todo dentro de una transacción

Ejemplo simplificado:

C#
using var con = GetConnection();
con.Open();

using var trx = con.BeginTransaction();

try
{
    var cuentaId = con.ExecuteScalar<long>(
        @"INSERT INTO Cuentas (Nombre) 
          VALUES (@Nombre);
          SELECT SCOPE_IDENTITY();",
        new { Nombre = "Cuenta demo" },
        trx);

    con.Execute(
        @"INSERT INTO Movimientos (CuentaId, Importe)
          VALUES (@CuentaId, @Importe)",
        new { CuentaId = cuentaId, Importe = 100 },
        trx);

    trx.Commit();
}
catch
{
    trx.Rollback();
    throw;
}

Este tipo de control es justo lo que estaba buscando.

Ventajas

Después de usarlo en algunos proyectos en mi día a día, estas fueron las principales ventajas:

  • Menos fricción trabajando en equipo
  • No dependes de migraciones
  • Código más predecible
  • Mejor rendimiento
  • Debug mucho más sencillo

¿Reemplaza a Entity Framework?

No necesariamente, Entity Framework sigue siendo muy útil en muchos escenarios, sobre todo cuando:

  • No quieres escribir SQL
  • Tu modelo es muy estable
  • Necesitas rapidez para prototipar

Pero en escenarios como el mío (muchos cambios y varios desarrolladores), creo Dapper se siente mucho más cómodo.

Conclusión

En mi caso, Dapper no solo resolvió un problema que me representaría usar Entity Framework sino que me ayudó a mejorar el flujo de trabajo al no tener que sincronizar mi base de datos con el modelo. Aunque no descarto la idea de usar Entity Framework pero será en una etapa donde los cambios en la base de datos no sean tan recurrentes como ahora.

Arquitectura Multicapa Simplificada para aplicaciones .NET

En mi trabajo surgió la necesidad de migrar una aplicación relativamente grande que estaba construida con ASP.NET MVC 5 + DevExtreme. Yo también había participado en el desarrollo de dicha aplicación y la verdad es que conforme fue creciendo me fui dando cuenta de que tal ves el camino que habíamos elegido no era el correcto, MVC nos estaba generando controladores gigantescos incluso cuando toda la lógica de negocio estaba en la base de datos, ya que aunque usábamos repositorios para el acceso a datos, todo el sistema estaba basado en procedimientos almacenados y los repositorios solo se encargaban de llamarlos.

Entonces, al presentarse la necesidad de crear una nueva versión del mismo sistema vi que Blazor Server era una buena opción, ya que echaba de menos la característica que ofrecía WebForms de poder ordenar las vistas en carpetas y Blazor Server nos permitía hacerlo, ademas otra necesidad principal que teníamos es que el sistema nuevo debía ser responsivo y se optó por usar MudBlazor.

Cómo nació la idea de la arquitectura

Necesitaba una arquitectura que cumpliera con tres requisitos muy concretos:

  • Que fuera ordenada
  • Que fuera fácil de mantener
  • Que no fuera difícil de implementar

Así que me dedique un tiempo a investigar distintas arquitecturas de software como DDD o Core-Driven pero no me terminaban de convencer. Pero si me dieron una idea de como conseguir lo que necesitaba.

La Arquitectura Multicapa Simplificada

Supongamos que la aplicación que vamos a migrar se llama Rocket, la arquitectura se basa en cuatro capas que estarían en proyectos separados:

Rocket.Core     → Modelos, DTOs, Requests/Responses y clases compartidas
Rocket.Data → Repositorios y acceso a base de datos
Rocket.Services → Lógica de negocio y orquestación
Rocket.Web → Capa de presentación (Blazor Server)

La idea es simple, cada capa tiene una responsabilidad clara y la única capa que puede relacionarse con las demás(que de hecho debe estar relacionada) es Core.

Cómo se comunican las capas

La relación es directa y limpia:

UI (Rocket.Web)

Servicios (Rocket.Services)

Repositorios (Rocket.Data)

Base de datos
-----------------------------
Rocket.Core → Consumido por todas las capas

Rocket.Core

Esta capa esta compuesta de al menos los siguientes namespaces

Rocket.Core
- Rocket.Core.Dtos → Clases para transferir datos
- Rocket.Core.IO → Datos de entrada y salida de los servicios

Una característica que me pareció necesaria al momento de diseñar esta arquitectura es que estuviera desacoplada de la capa de presentación, ya que anteriormente tuve la experiencia de que se había desarrollado un sistema con WebForms y después se nos solicito crear WebAPIs que expusieran algunas funcionalidades del sistema.

Por esta razón agregue un Result Wrapper que me ayude separar la capa de servicios de la capa de presentacion y en caso de ser necesario exponer funcionalidades con WebAPIs solo se consumen los servicios y ¡listo!.

Result
--------------
Success:bool → Indica si la operación fue exitosa
Message:string → Mensaje que se mostrará al usuario
Data:T (Opcional) → Objeto con la respuesta que devuelve el servicio

El código de la clase Result es el siguiente:

C#
namespace Rocket.Core.IO;

public class Result
{
    protected Result() { }
    public bool Success { get; protected set; }
    public string Message { get; protected set; }

    public static Result Ok(string message = "") =>
        new() { Success = true, Message = message };

    public static Result Fail(string message) =>
        new() { Success = false, Message = message };
}

public class Result<T>() : Result where T : BaseResponse?
{
    public T? Data { get; set; }

    public static Result<T> Ok(T data, string message = "")
        => new Result<T> { Success = true, Message = message, Data = data };

    public new static Result<T> Fail(string message)
        => new Result<T> { Success = false, Message = message, Data = default };
}

Rocket.Data

Esta capa es la encargada del acceso a datos, aquí se encuentran los repositorios que usarán los servicios junto con el ORM que se deseé utilizar, en mi caso actualmente uso Dapper pero podría ser cualquier otro, y esta capa se compone de al menos el siguiente namespace:

Rocket.Data
- Rocket.Data.Repositories → Aquí estarían todos los repositorios que requiera nuestra aplicación

Rocket.Services

Para mi esta es la capa más importante ya que aquí tendríamos las reglas de negocio, validaciones, cálculos, etc.

Esta capa estaría compuesto por al menos los siguientes namespaces:

Rocket.Services → Aquí estarían los servicios de nuestra aplicación, por ejemplo: ClientesService, PagosService, etc.

La firma típica de los métodos de cada servicio serian algo parecido a lo siguiente:

C#
public Result<TResponse> Metodo(TRequest request);

// Ejemplo
public Result<LoginResponse> Login(LoginRequest request);

De esta forma, en caso de ser necesario agregar un parámetro nuevo a la función simplemente se define una propiedad en la clase Request y se haría lo mismo para agregar un campo más en la respuesta pero en la clase Response correspondiente.

Un punto importante

Aunque ésta arquitectura permite usar interfaces para los servicios y repositorios actualmente solo agregué clases concretas ya que donde trabajo no usamos pruebas unitarias y me topé con un poco de resistencia a incluirlas.

Rocket.Web

Esta es la capa de presentación, en mi caso es una aplicación Blazor Server pero bien podría ser una aplicación MAUI o un WebAPI pero lo importante es que este proyecto no debe contener lógica de negocio, solo se encarga de la interacción del usuario con nuestra aplicación.

Conclusión

Esta arquitectura multicapa simplificada nació de una necesidad de mantener orden en un sistema relativamente grande sin caer en complejidad innecesaria. Actualmente es la forma en que estructuró las aplicaciones .NET porque ofrece un equilibrio entre claridad, simplicidad y capacidad de crecimiento.

Filtro tipo Excel para MudDataGrid (ExcelLikeFilterColumn)

De igual forma como vimos en el artículo pasado tuve la necesidad de crear un filtro booleano para el MudDataGrid, pues de la misma forma necesité un filtroque se comportara igual que los filtros de Excel: con una lista de valores únicos con checkboxes, para que el usuario pudiera filtrar por uno o varios elementos.

MudBlazor no trae esto de forma nativa, así que desarrollé mi propio componente Razor llamado ExcelLikeFilterColumn.

En este artículo te explico cómo funciona y cómo integrarlo fácilmente en tu proyecto.

¿Por qué hacer un filtro tipo Excel?

Cuando presentas datos tabulares ―por ejemplo, catálogos, direcciones, marcas, categorías, usuarios― los filtros tipo Excel son una bendición:

  • Muestran los valores distintos de la columna.
  • Permiten seleccionar varios valores a la vez.
  • Son intuitivos para cualquier usuario.
  • Se despliegan dentro de un popover con scroll.
  • Se integran perfectamente con MudDataGrid.

Así que decidí replicar ese comportamiento con MudBlazor.

¿Cómo funciona internamente?

Mi componente:

  • Recibe los items del grid mediante Items="context.Items".
  • Usa generics para el tipo de item y el tipo de valor (TItem, TValue).
  • Obtiene los valores únicos de la columna usando ValueSelector.
  • Construye un popover con un checkbox de “Todos” y un checkbox para cada valor distinto.
  • Permite personalizar la etiqueta a mostrar mediante LabelSelector.
  • Aplica el filtro usando FilterDefinition<TItem> tal como lo hace MudDataGrid internamente.

La experiencia final es prácticamente idéntica a un filtro de Excel.

[C#] ExcelFilterColumn.razor
@typeparam TItem
@typeparam TValue

<MudIconButton OnClick="@(() => _open = true)" Icon="@_icon" Size="Size.Small" />
<MudOverlay Visible="@_open" OnClick="@(() => _open = false)" />
<MudPopover Open="@_open" AnchorOrigin="Origin.BottomCenter" TransformOrigin="Origin.TopCenter" Class="px-4 py-2">
    <MudStack Spacing="0">
        <MudCheckBox T="bool" Label="Todos" Size="Size.Small"
                     Value="@_selectAll"
                     ValueChanged="@SelectAllChanged" />
        <MudStack Spacing="0" Style="overflow-y:auto;max-height:250px">
            @foreach (var val in _distinctValues)
            {
                <MudCheckBox T="bool" Label="@GetLabel(val)"
                             Value="@_selectedValues.Contains(val)"
                             ValueChanged="@(v => ValueChanged(v, val))"
                             Size="Size.Small" />
            }
        </MudStack>
        <MudStack Row="true">
            <MudButton OnClick="Clear">Limpiar</MudButton>
            <MudSpacer/>
            <MudButton Color="Color.Primary" OnClick="Apply">Aplicar</MudButton>
        </MudStack>
    </MudStack>
</MudPopover>

@code {
    [Parameter] public IEnumerable<TItem> Items { get; set; } = Enumerable.Empty<TItem>();
    [Parameter] public Func<TItem, TValue?> ValueSelector { get; set; } = default!;
    [Parameter] public Func<TItem, string> LabelSelector { get; set; } = default!;
    [Parameter] public FilterContext<TItem> Context { get; set; } = default!;

    private bool _open;
    private bool _selectAll = true;
    private string _icon = Icons.Material.Outlined.FilterAlt;

    private HashSet<TValue> _selectedValues = new();
    private List<TValue> _distinctValues = new();
    private FilterDefinition<TItem>? _filterDefinition;

    protected override void OnParametersSet()
    {
        _distinctValues = Items
            .Select(ValueSelector)
            .Where(v => v is not null)
            .Distinct()!
            .ToList();

        if (_selectAll)
            _selectedValues = _distinctValues.ToHashSet();

        // Crear el FilterDefinition una sola vez (o regenerarlo si cambian Items)
        _filterDefinition ??= new FilterDefinition<TItem>
        {
            FilterFunction = x =>
            {
                var val = ValueSelector(x);
                return val is not null && _selectedValues.Contains(val);
            }
        };
    }

    private string GetLabel(TValue val)
    {
        var item = Items.FirstOrDefault(x => EqualityComparer<TValue>.Default.Equals(ValueSelector(x)!, val));
        return item is null ? val?.ToString() ?? "" : LabelSelector(item);
    }

    private void SelectAllChanged(bool value)
    {
        _selectAll = value;
        if (value)
            _selectedValues = _distinctValues.ToHashSet();
        else
            _selectedValues.Clear();
    }

    private void ValueChanged(bool value, TValue val)
    {
        if (value)
            _selectedValues.Add(val);
        else
            _selectedValues.Remove(val);

        _selectAll = _selectedValues.Count == _distinctValues.Count;
    }

    private async Task Clear()
    {
        _selectedValues.Clear();
        _selectAll = false;
        _icon = Icons.Material.Outlined.FilterAlt;
        if (_filterDefinition is not null)
            await Context.Actions.ClearFilterAsync(_filterDefinition);
        _open = false;
    }

    private async Task Apply()
    {
        _icon = _selectedValues.Count == _distinctValues.Count
            ? Icons.Material.Outlined.FilterAlt
            : Icons.Material.Filled.FilterAlt;

        if (_filterDefinition is not null)
            await Context.Actions.ApplyFilterAsync(_filterDefinition);

        _open = false;
    }
}

Ejemplo de uso: ExcelLikeFilterColumn

Si tienes una propiedad como Marca, que tiene un ID y un nombre, puedes filtrar así:

[C#] Ejemplo de ExcelLikeFilterColumn
<PropertyColumn Property="x => x.MarcaNombre" Title="Marca">
    <FilterTemplate>
        <ExcelFilterColumn TItem="DireccionesViewModel" TValue="short"
                           Items="context.Items"
                           ValueSelector="x => x.MarcaId!.Value"
                           LabelSelector="x => x.MarcaNombre.ToUpperInvariant()"
                           Context="context" />
    </FilterTemplate>
</PropertyColumn>

Con esto:

  • ValueSelector define cuál es el valor real a filtrar.
  • LabelSelector define el texto que verá el usuario.
  • Los valores duplicados se agrupan automáticamente.
  • El filtro soporta múltiples selecciones.

Conclusión

Este filtro tipo Excel me ha sido ayuda para crear filtros con los que los usuarios están más familiarizados los usuarios, esto les brinda una manera familiar y potente de filtrar valores sin tener que escribir texto ni conocer criterios avanzados.

Creando un filtro booleano personalizado para MudDataGrid (BooleanFilterColumn)

En uno de mis proyectos recientes desarrollados con .NET 9 y MudBlazor, me encontré con la necesidad de mejorar los filtros que ofrece MudDataGrid. Si bien el componente ya trae una base sólida, para columnas booleanas quería algo más práctico y visual, algo similar a lo que vemos en sistemas administrativos profesionales.

Por eso construí un componente personalizado llamado BooleanFilterColumn, que me permite seleccionar rápidamente entre Todos, Activo o Inactivo, usando un pequeño popover con checkboxes.

En este artículo te cuento por qué lo hice, cómo funciona y cómo puedes utilizarlo tú también.

¿Por qué crear un filtro booleano personalizado?

MudBlazor permite aplicar filtros personalizados, pero para las columnas booleanas normalmente solo tenemos opciones binarias o expresiones. Yo quería algo más intuitivo:

  • Un botón de filtro con el icono que cambia según si hay filtros aplicados.
  • Un popover con checkboxes para seleccionar:
    • Todos
    • Activos
    • Inactivos
  • Aplicar o limpiar el filtro con un solo clic.
  • Soporte para personalizar textos (por ejemplo: “Activo”, “Inactivo”, “Todos”, etc.)

El resultado fue este componente Razor:

[C#] BooleanFilterColumn.razor
@typeparam T

<MudIconButton OnClick="@(() => _open = true)" Icon="@_icon" Size="Size.Small" />
<MudOverlay Visible="@_open" OnClick="@(() => _open = false)" />
<MudPopover Open="@_open" AnchorOrigin="Origin.BottomCenter" TransformOrigin="Origin.TopCenter" Class="px-4 py-2">
    <MudStack Spacing="0">
        <MudCheckBox T="bool" Label="@AllText" Size="Size.Small"
                     Value="@_selectAll"
                     ValueChanged="@SelectAllChanged" />
        <MudStack Spacing="0">
            <MudCheckBox T="bool" Label="@TrueText"
                         Value="@_selectedValues.Contains(true)"
                         ValueChanged="@(v => ValueChanged(v, true))"
                         Size="Size.Small" />
            <MudCheckBox T="bool" Label="@FalseText"
                         Value="@_selectedValues.Contains(false)"
                         ValueChanged="@(v => ValueChanged(v, false))"
                         Size="Size.Small" />
        </MudStack>
        <MudStack Row="true">
            <MudButton OnClick="Clear">@ClearButtonText</MudButton>
            <MudSpacer/>
            <MudButton Color="Color.Primary" OnClick="Apply">@ApplyButtonText</MudButton>
        </MudStack>
    </MudStack>
</MudPopover>

@code {
    [Parameter] public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
    [Parameter] public Func<T, bool?> ValueSelector { get; set; } = default!;
    [Parameter] public FilterContext<T> Context { get; set; } = default!;
    [Parameter] public string ApplyButtonText { get; set; } = "Apply";
    [Parameter] public string AllText { get; set; } = "All";
    [Parameter] public string ClearButtonText { get; set; } = "Clear";
    [Parameter] public string TrueText { get; set; } = "True";
    [Parameter] public string FalseText { get; set; } = "False";

    private bool _open;
    private bool _selectAll = true;
    private string _icon = Icons.Material.Outlined.FilterAlt;

    private HashSet<bool> _selectedValues = new();
    private FilterDefinition<T>? _filterDefinition;

    protected override void OnParametersSet()
    {
        if (_selectAll)
            _selectedValues = new HashSet<bool> { true, false };

        _filterDefinition ??= new FilterDefinition<T>
        {
            FilterFunction = x =>
            {
                var val = ValueSelector(x);
                return val is not null && _selectedValues.Contains(val.Value);
            }
        };
    }

    private void SelectAllChanged(bool value)
    {
        _selectAll = value;
        if (value)
            _selectedValues = new HashSet<bool> { true, false };
        else
            _selectedValues.Clear();
    }

    private void ValueChanged(bool value, bool val)
    {
        if (value)
            _selectedValues.Add(val);
        else
            _selectedValues.Remove(val);

        _selectAll = _selectedValues.Count == 2;
    }

    private async Task Clear()
    {
        _selectedValues.Clear();
        _selectAll = false;
        _icon = Icons.Material.Outlined.FilterAlt;
        if (_filterDefinition is not null)
            await Context.Actions.ClearFilterAsync(_filterDefinition);
        _open = false;
    }

    private async Task Apply()
    {
        _icon = _selectedValues.Count == 2
            ? Icons.Material.Outlined.FilterAlt
            : Icons.Material.Filled.FilterAlt;

        if (_filterDefinition is not null)
            await Context.Actions.ApplyFilterAsync(_filterDefinition);

        _open = false;
    }
}

¿Cómo funciona internamente?

  • Recibe una lista de items (Items) provenientes del contexto de filtrado de MudDataGrid.
  • Extrae los valores booleanos distintos (true/false) utilizando ValueSelector.
  • Muestra tres opciones:
    • Todos
    • TrueText
    • FalseText
  • Actualiza el ícono dependiendo del estado del filtro.
  • Construye un FilterDefinition<T> personalizado y lo aplica usando:
await Context.Actions.ApplyFilterAsync(_filterDefinition);

Además, todo vive dentro de un MudPopover, usando MudCheckBox para representar las opciones.

Ejemplo de uso: BooleanFilterColumn

Así es como lo estoy usando actualmente en una columna booleana (por ejemplo, “Activo”)

<PropertyColumn Property="x => x.Activo" Title="Activo">
    <CellTemplate>
        <MudCheckBox Value="@context.Item.Activo" ReadOnly="true"/>
    </CellTemplate>
    <FilterTemplate>
        <BooleanFilterColumn T="Rubro"
                             Items="context.Items"
                             ValueSelector="x => x.Activo"
                             Context="context"
                             AllText="Todos"
                             ApplyButtonText="Aplicar"
                             ClearButtonText="Limpiar"
                             TrueText="ACTIVO"
                             FalseText="INACTIVO" />
    </FilterTemplate>
</PropertyColumn>

Con eso, el usuario tiene un filtro booleano más claro y fácil de usar.

Conclusión

Este filtro ha sido un gran agregado a mi MudDataGrid, haciéndolo más amigable y funcional para columnas booleanas. Si también estás creando aplicaciones administrativas con MudBlazor, probablemente te sea muy útil tener un filtro tan visual y práctico.

Listary

Cómo Listary me facilita el trabajo diario

En mi día a día como desarrollador y administrador de sistemas, siempre estoy buscando herramientas que me ayuden a ahorrar tiempo y simplificar tareas repetitivas. Una de esas joyas que descubrí hace un tiempo es Listary

A primera vista, Listary parece solo un buscador rápido de archivos o un launcher, pero en realidad es mucho más que eso. Lo que más me gusta de esta aplicación es su capacidad para crear comandos personalizados y ejecutarlos de forma instantánea, sin tener que abrir terminales o menús innecesarios.

Por ejemplo, en mi caso utilizo Listary para conectarme a la VPN de mi trabajo. Normalmente, esto implicaría abrir un archivo .bat que contiene las reglas necesarias para que mi tráfico personal no pase por la VPN. Gracias a Listary, simplemente escribo un comando en su launcher —por ejemplo, vpn— y en segundos la conexión está activa.

Este pequeño detalle me ha ahorrado muchísimo tiempo y clics a lo largo del día. Además, la ejecución es fluida y puedo mantener mi flujo de trabajo sin interrupciones.

Integración con otras herramientas

Otra ventaja es que Listary se complementa muy bien con otras utilidades. En mi caso, lo uso junto con Remote Desktop Plus, una aplicación que me permite administrar mis credenciales y conexiones a servidores de forma más cómoda. No voy a profundizar mucho en esta herramienta (ya escribí una entrada dedicada a ella), pero basta decir que ambas hacen un excelente equipo para quienes trabajamos conectándonos constantemente a distintos entornos remotos.

Componentes para Blazor

Recientemente, en mi trabajo tomamos la decisión de rehacer un sistema desarrollado con ASP.NET MVC 5 debido a su alta acumulación de deuda técnica. Aunque gran parte de esa deuda se debía a que el desarrollo inicial estuvo orientado a resolver necesidades operativas inmediatas (lo cual fue una buena decisión en su momento), con el tiempo quedó claro que MVC no era la mejor opción para un sistema de ese tamaño.

Como parte del proyecto, se me encomendó buscar alternativas para la nueva versión del sistema. Inicialmente, pensé en usar WebForms para organizar mejor las vistas, pero descubrí que ya no es soportado por las versiones más recientes de .NET. Sin embargo, en mi investigación me topé con Blazor, una tecnología que no solo es moderna, sino que también permite construir componentes reutilizables y organizados, algo similar a lo que buscaba.

El reto con los Grids y Componentes

El sistema original utilizaba componentes de DevExpress y SyncFusion. Aunque DevExpress nos facilitaba un desarrollo ágil, no cumplía con un requerimiento clave: los grids no eran responsivos, algo crítico dado que el sistema debía ser accesible desde dispositivos móviles. Por ello, complementamos con los grids de SyncFusion, que sí ofrecían este soporte.

Con Blazor seleccionado como base para el desarrollo, la siguiente tarea fue buscar una librería de componentes que permitiera construir una aplicación web moderna y responsiva, sin sacrificar funcionalidad ni estética. Este artículo detalla las opciones que evalué y la elección final.

Librerías de componentes para Blazor

Durante mi análisis, encontré varias opciones interesantes. Aquí presento una lista de las principales librerías disponibles, junto con algunas notas relevantes:

DevExpress (De pago): Conocido por su robustez y herramientas avanzadas, aunque con un costo elevado.

Telerik (De pago): Amplia colección de componentes con soporte profesional.

SyncFusion (De pago / Community): Excelente soporte para grids responsivos, además de una licencia gratuita para pequeñas empresas y desarrolladores individuales.

SemanticUI (Gratuita): Diseño limpio y moderno, aunque requiere integración adicional para ciertos casos.

FomaticUI (Gratuita): Un fork de SemanticUI con mejoras, como un componente DatePicker.

jQWidgets (De pago): Basado en jQuery, ideal para quienes ya estén familiarizados con esa tecnología.

Radzen (De pago / Community): Compatible con Blazor Server y WASM, con licencia gratuita limitada.

MudBlazor (Gratuita): Ofrece componentes modernos y responsivos, con una curva de aprendizaje accesible.

Blazorise (De pago / Community): Soporte para múltiples frameworks CSS, ideal para proyectos personalizados.

FluentUI (Gratuita): Basado en los principios de diseño de Microsoft, ideal para aplicaciones con estilo Office.

MudBlazor – La Elección Final

Después de evaluar varias opciones, nos decidimos por MudBlazor. Esta librería destacó por su facilidad para replicar el layout del sistema anterior y por las características responsivas de su grid. Por defecto, el grid de MudBlazor ajusta su comportamiento en pantallas pequeñas, permitiendo un desplazamiento vertical en lugar de horizontal, justo lo que necesitábamos para mejorar la experiencia en dispositivos móviles.

Remote Desktop Plus

Remote Desktop Plus

Algo que posiblemente es muy común para la mayoría de la gente que trabajamos en el área de TI, es conectarnos por escritorio remoto a distintos servidores. Existen herramientas como TeamViewer o AnyDesk entre muchas otras, pero tal ves la más común sea el Escritorio Remoto de Windows que es lo que en mi caso uso con mayor frecuencia.

Al utilizar frecuentemente el escritorio remoto, me surgió el siguiente problema. Necesitaba algún cliente RDP para guardar los datos de acceso (ip, usuario y contraseña) para conectarme de forma fácil, esta característica ya la soporta la aplicación que ya viene por defecto en Windows, pero a su vez necesitaba poder recuperar o editar esos datos, ya que si no los escribo frecuentemente lo más seguro es que termine olvidandolos, ya que mi memoria no es muy buena.

Buscando por internet llegue a encontrarme con Remote Desktop Manager, y aunque me permitía guardar, y exportar las credenciales de acceso, la verdad es que no me gustaba que siempre aparecía un splash screen que aunque no duraba mucho, tener que esperar cada vez que necesitaba usarlo me parecía tedioso, así que empecé a buscar alguna otra alternativa y así encontré Remote Desktop Plus.

¿Porque me gusta RD+?

Remote Desktop Plus cubre las 2 necesidades que tenia que eran, poder conectarme de forma rápida a los servidores que utilizo pero a su vez me permite exportar las credenciales.

Pero algo que me soprendió y que es de bastante utilidad es que soporta linea de comandos, de esta manera puedo crear algún comando para conectarme a cualquier servidor.

Librería para CFDI 4.0

En 2015 haciendo mis prácticas profesionales en un despacho contable, conocí lo que era CFDI 3.2 y en mi afán de aprender, desarrollé una librería que generaba el XML sellado para su timbrado.

Pero justo al terminar mis prácticas, me contrató una empresa local dedicada a la facturación electrónica, donde laboré por más de 3 años dando soporte técnico al SDK para CFDI que comercializaban.

Con esa experiencia decidí volver a crear una librería para facturación electrónica, que coincidió con la llegada del Cfdi 3.3 y pude venderla cuando entró en vigor (ver aquí).

Además, al mismo tiempo en la empresa que trabajaba desarrollé una nueva versión del SDK, que también soportaba Cfdi 3.3, que entre sus características más resaltables eran:

  • Retro compatibilidad con los proyectos que ya usaban el SDK anterior (prácticamente era quitar el viejo e incorporar el nuevo)
  • Su nueva estructura le permitía darle soporte de forma mucho más facil
  • Implementé un sistema de plugins que le permitía al SDK realizar operaciones antes, durante y después de generar y timbrar el Cfdi.

Con esos casi 4 años de experiencia y los casi 7 años de experiencia profesional, decidí crear una librería para CFDI 4.0.

Actualmente estoy desarrollando una librería que soporta las versiones 3.3 (aún vigente) y 4.0 de CFDI. Esta librería podrá incluir cualquier complemento que necesites por medio de librerías extra que iré desarrollando y liberando tan pronto como me sea posible.

¿Cómo consigo la librería?

La librería la puedes conseguir desde la NuGet Gallery o puedes buscarla en Visual Studio, desde nuget package manager, igual que los complementos.

¿De verdad es gratis?

Si, la librería para CFDI junto con las librerías para complementos son totalmente gratuitas. Este es un proyecto personal y deseó poder implementar la mayor cantidad de complementos posibles (a ser posible, todos).

Soporte Técnico

Actualmente no dispongo del tiempo que me gustaría para darle soporte a un proyecto como este y no puedo ofrecer soporte 24/7.

Como dije anteriormente, mi idea es implementar la mayoría de los complementos, por lo que si necesitas alguno en concreto házmelo saber.

Importante

  • Actualmente estoy utilizando el ambiente de pruebas de Solución Factible para timbrar los CFDI y validar que las librerías estén trabajando correctamente.
  • Pronto publicaré un repositorio de GitHub con los ejemplos que timbré para validar las librerías, está atento a este sitio para que puedas descargarlo.

Ejemplos

CFDI 4.0 de Ingreso
C#
// Archivo XSLT necesario para la Cadena Original
string xsltFile = @"cadenaoriginal_4_0.xslt";
 
// Certificados
string cerFile = @"EKU9003173C9.cer";
string keyFile = @"EKU9003173C9.key";
string password = "12345678a";
 
// Se lee el Certificado
var cerInfo = CsdUtils.LeerCertificado(cerFile);
 
// Se crea el Comprobante
IsaRoGaMX.Cfdi.v40.Comprobante cfdi = new IsaRoGaMX.Cfdi.v40.Comprobante();
 
// Datos de la Factura
cfdi["Serie"] = "F";
cfdi["Folio"] = "1234";
cfdi["Fecha"] = DateTime.Now.ToString("s");
cfdi["CondicionesDePago"] = "CondicionesDePago";
cfdi["SubTotal"] = "200";
cfdi["Descuento"] = "1";
cfdi["Moneda"] = "MXN";
cfdi["TipoCambio"] = "1";
cfdi["Total"] = "198.95";
cfdi["TipoDeComprobante"] = "I";
cfdi["Exportacion"] = "01";
cfdi["MetodoPago"] = "PPD";
cfdi["FormaPago"] = "99";
cfdi["LugarExpedicion"] = "20000";
cfdi["Certificado"] = cerInfo.Certificado;
cfdi["NoCertificado"] = cerInfo.NoCertificado;
 
// Emisor
cfdi.Emisor["Rfc"] = "EKU9003173C9";
cfdi.Emisor["Nombre"] = "ESCUELA KEMPER URGATE";
cfdi.Emisor["RegimenFiscal"] = "601";
 
// Receptor
cfdi.Receptor["Rfc"] = "URE180429TM6";
cfdi.Receptor["Nombre"] = "UNIVERSIDAD ROBOTICA ESPAÑOLA";
cfdi.Receptor["DomicilioFiscalReceptor"] = "65000";
cfdi.Receptor["RegimenFiscalReceptor"] = "601";
cfdi.Receptor["UsoCFDI"] = "G01";
 
// Concepto
cfdi.Conceptos[0]["ClaveProdServ"] = "50211503";
cfdi.Conceptos[0]["Cantidad"] = "1";
cfdi.Conceptos[0]["ClaveUnidad"] = "H87";
cfdi.Conceptos[0]["Unidad"] = "Pieza";
cfdi.Conceptos[0]["Descripcion"] = "Cigarros";
cfdi.Conceptos[0]["ValorUnitario"] = "200.00";
cfdi.Conceptos[0]["Descuento"] = "1";
cfdi.Conceptos[0]["Importe"] = "200.00";
cfdi.Conceptos[0]["ObjetoImp"] = "02";
 
cfdi.Conceptos[0].Impuestos.Traslados[0]["Base"] = "1";
cfdi.Conceptos[0].Impuestos.Traslados[0]["Importe"] = "0.16";
cfdi.Conceptos[0].Impuestos.Traslados[0]["Impuesto"] = "002";
cfdi.Conceptos[0].Impuestos.Traslados[0]["TasaOCuota"] = "0.160000";
cfdi.Conceptos[0].Impuestos.Traslados[0]["TipoFactor"] = "Tasa";
 
cfdi.Conceptos[0].Impuestos.Retenciones[0]["Base"] = "1";
cfdi.Conceptos[0].Impuestos.Retenciones[0]["Impuesto"] = "001";
cfdi.Conceptos[0].Impuestos.Retenciones[0]["TipoFactor"] = "Tasa";
cfdi.Conceptos[0].Impuestos.Retenciones[0]["TasaOCuota"] = "0.100000";
cfdi.Conceptos[0].Impuestos.Retenciones[0]["Importe"] = "0.10";
 
cfdi.Conceptos[0].Impuestos.Retenciones[1]["Base"] = "1";
cfdi.Conceptos[0].Impuestos.Retenciones[1]["Impuesto"] = "002";
cfdi.Conceptos[0].Impuestos.Retenciones[1]["TipoFactor"] = "Tasa";
cfdi.Conceptos[0].Impuestos.Retenciones[1]["TasaOCuota"] = "0.106666";
cfdi.Conceptos[0].Impuestos.Retenciones[1]["Importe"] = "0.11";
 
cfdi.Impuestos["TotalImpuestosRetenidos"] = "0.21";
cfdi.Impuestos["TotalImpuestosTrasladados"] = "0.16";
 
cfdi.Impuestos.Retenciones[0]["Impuesto"] = "001";
cfdi.Impuestos.Retenciones[0]["Importe"] = "0.10";
 
cfdi.Impuestos.Retenciones[1]["Impuesto"] = "002";
cfdi.Impuestos.Retenciones[1]["Importe"] = "0.11";
 
cfdi.Impuestos.Traslados[0]["Base"] = "1";
cfdi.Impuestos.Traslados[0]["Importe"] = "0.16";
cfdi.Impuestos.Traslados[0]["Impuesto"] = "002";
cfdi.Impuestos.Traslados[0]["TasaOCuota"] = "0.160000";
cfdi.Impuestos.Traslados[0]["TipoFactor"] = "Tasa";
 
// Genera cadena original
string cadenaOriginal = XsltTransform.GeneraCadenaOriginal(cfdi.Documento, xsltFile);
 
// Genera sello
cfdi["Sello"] = CsdUtils.GeneraSelloFiscalDigital(cadenaOriginal, keyFile, password);
 
// Se guarda el Xml
cfdi.Documento.Save("F1234.xml");

Cfdi 4.0 de Nómina

C#
// Archivo XSLT necesario para la Cadena Original
string xsltFile = @"E:\Descargas\cadenaoriginal_4_0.xslt";
 
// Certificados
string cerFile = @"EKU9003173C9.cer";
string keyFile = @"EKU9003173C9.key";
string password = "12345678a";
 
// Se lee el Certificado
var cerInfo = CsdUtils.LeerCertificado(cerFile);
 
// Se crea el comprobante
IsaRoGaMX.Cfdi.v40.Comprobante cfdi = new IsaRoGaMX.Cfdi.v40.Comprobante();
 
// Datos del Comprobante
cfdi["Serie"] = "NOM";
cfdi["Folio"] = "789";
cfdi["Fecha"] = DateTime.Now.ToString("s");
cfdi["SubTotal"] = "5000.00";
cfdi["Descuento"] = "200";
cfdi["Moneda"] = "MXN";
cfdi["Total"] = "4800";
cfdi["TipoDeComprobante"] = "N";
cfdi["Exportacion"] = "01";
cfdi["MetodoPago"] = "PUE";
cfdi["LugarExpedicion"] = "20000";
cfdi["Certificado"] = cerInfo.Certificado;
cfdi["NoCertificado"] = cerInfo.NoCertificado;
 
// Emisor
cfdi.Emisor["Rfc"] = "URE180429TM6";
cfdi.Emisor["Nombre"] = "UNIVERSIDAD ROBOTICA ESPAÑOLA";
cfdi.Emisor["RegimenFiscal"] = "601";
 
// Receptor
cfdi.Receptor["Rfc"] = "XOJI740919U48";
cfdi.Receptor["Nombre"] = "INGRID XODAR JIMENEZ";
cfdi.Receptor["DomicilioFiscalReceptor"] = "88965";
cfdi.Receptor["RegimenFiscalReceptor"] = "605";
cfdi.Receptor["UsoCFDI"] = "CN01";
 
// Conceptos
cfdi.Conceptos[0]["ClaveProdServ"] = "84111505";
cfdi.Conceptos[0]["Cantidad"] = "1";
cfdi.Conceptos[0]["ClaveUnidad"] = "ACT";
cfdi.Conceptos[0]["Descripcion"] = "Pago de nómina";
cfdi.Conceptos[0]["ValorUnitario"] = "5000.00";
cfdi.Conceptos[0]["Descuento"] = "200";
cfdi.Conceptos[0]["Importe"] = "5000.00";
cfdi.Conceptos[0]["ObjetoImp"] = "01";
 
// Complemento Nomina
var nomina = new IsaRoGaMX.Cfdi.Nomina.v12.Nomina();
nomina["TipoNomina"] = "O";
nomina["FechaPago"] = "2021-12-24";
nomina["FechaInicialPago"] = "2021-12-09";
nomina["FechaFinalPago"] = "2021-12-24";
nomina["NumDiasPagados"] = "15";
nomina["TotalPercepciones"] = "5000.0";
nomina["TotalDeducciones"] = "200";
 
// Emisor
nomina.Emisor["RegistroPatronal"] = "B5510768108";
nomina.Emisor["RfcPatronOrigen"] = "URE180429TM6";
 
// Receptor
nomina.Receptor["Curp"] = "XEXX010101HNEXXXA4";
nomina.Receptor["NumSeguridadSocial"] = "000000";
nomina.Receptor["FechaInicioRelLaboral"] = "2015-01-01";
nomina.Receptor["Antigüedad"] = "P364W";
nomina.Receptor["TipoContrato"] = "01";
nomina.Receptor["TipoJornada"] = "01";
nomina.Receptor["TipoRegimen"] = "03";
nomina.Receptor["NumEmpleado"] = "120";
nomina.Receptor["Departamento"] = "Desarrollo";
nomina.Receptor["Puesto"] = "Ingeniero de Software";
nomina.Receptor["RiesgoPuesto"] = "1";
nomina.Receptor["PeriodicidadPago"] = "04";
nomina.Receptor["CuentaBancaria"] = "1111111111";
nomina.Receptor["Banco"] = "002";
nomina.Receptor["SalarioBaseCotApor"] = "490.22";
nomina.Receptor["SalarioDiarioIntegrado"] = "146.47";
nomina.Receptor["ClaveEntFed"] = "JAL";
 
// Percepciones
nomina.Percepciones["TotalSueldos"] = "5000.0";
nomina.Percepciones["TotalGravado"] = "2808.8";
nomina.Percepciones["TotalExento"] = "2191.2";
 
nomina.Percepciones[0]["TipoPercepcion"] = "001";
nomina.Percepciones[0]["Clave"] = "00500";
nomina.Percepciones[0]["Concepto"] = "Sueldos; Salarios Rayas y Jornales";
nomina.Percepciones[0]["ImporteGravado"] = "2808.8";
nomina.Percepciones[0]["ImporteExento"] = "2191.2";
 
// Deducciones
nomina.Deducciones["TotalOtrasDeducciones"] = "200";
 
nomina.Deducciones[0]["TipoDeduccion"] = "001";
nomina.Deducciones[0]["Clave"] = "00301";
nomina.Deducciones[0]["Concepto"] = "Seguridad Social";
nomina.Deducciones[0]["Importe"] = "200";
 
// Se agrega el complemento
cfdi.AgregaComplemento(nomina);
 
// Genera cadena original
string cadenaOriginal = XsltTransform.GeneraCadenaOriginal(cfdi.Documento, xsltFile);
 
// Genera sello
cfdi["Sello"] = CsdUtils.GeneraSelloFiscalDigital(cadenaOriginal, keyFile, password);
 
cfdi.Documento.Save("NUM789.xml");
Cfdi 4.0 Instituciones Educativas Privadas
C#
// Archivo XSLT necesario para la Cadena Original
string xsltFile = @"cadenaoriginal_4_0.xslt";
 
// Certificados
string cerFile = @"EKU9003173C9.cer";
string keyFile = @"EKU9003173C9.key";
string password = "12345678a";
 
// Se lee el Certificado
var cerInfo = CsdUtils.LeerCertificado(cerFile);
 
// Se crea el Comprobante
IsaRoGaMX.Cfdi.v40.Comprobante cfdi = new IsaRoGaMX.Cfdi.v40.Comprobante();
 
// Datos de la Factura
cfdi["Serie"] = "F";
cfdi["Folio"] = "1234";
cfdi["Fecha"] = DateTime.Now.ToString("s");
cfdi["CondicionesDePago"] = "CondicionesDePago";
cfdi["SubTotal"] = "200";
cfdi["Descuento"] = "1";
cfdi["Moneda"] = "MXN";
cfdi["TipoCambio"] = "1";
cfdi["Total"] = "198.95";
cfdi["TipoDeComprobante"] = "I";
cfdi["Exportacion"] = "01";
cfdi["MetodoPago"] = "PPD";
cfdi["FormaPago"] = "99";
cfdi["LugarExpedicion"] = "20000";
cfdi["Certificado"] = cerInfo.Certificado;
cfdi["NoCertificado"] = cerInfo.NoCertificado;
 
// Emisor
cfdi.Emisor["Rfc"] = "EKU9003173C9";
cfdi.Emisor["Nombre"] = "ESCUELA KEMPER URGATE";
cfdi.Emisor["RegimenFiscal"] = "601";
 
// Receptor
cfdi.Receptor["Rfc"] = "URE180429TM6";
cfdi.Receptor["Nombre"] = "UNIVERSIDAD ROBOTICA ESPAÑOLA";
cfdi.Receptor["DomicilioFiscalReceptor"] = "65000";
cfdi.Receptor["RegimenFiscalReceptor"] = "601";
cfdi.Receptor["UsoCFDI"] = "G01";
 
// Concepto
cfdi.Conceptos[0]["ClaveProdServ"] = "50211503";
cfdi.Conceptos[0]["Cantidad"] = "1";
cfdi.Conceptos[0]["ClaveUnidad"] = "H87";
cfdi.Conceptos[0]["Unidad"] = "Pieza";
cfdi.Conceptos[0]["Descripcion"] = "Cigarros";
cfdi.Conceptos[0]["ValorUnitario"] = "200.00";
cfdi.Conceptos[0]["Descuento"] = "1";
cfdi.Conceptos[0]["Importe"] = "200.00";
cfdi.Conceptos[0]["ObjetoImp"] = "02";
 
cfdi.Conceptos[0].Impuestos.Traslados[0]["Base"] = "1";
cfdi.Conceptos[0].Impuestos.Traslados[0]["Importe"] = "0.16";
cfdi.Conceptos[0].Impuestos.Traslados[0]["Impuesto"] = "002";
cfdi.Conceptos[0].Impuestos.Traslados[0]["TasaOCuota"] = "0.160000";
cfdi.Conceptos[0].Impuestos.Traslados[0]["TipoFactor"] = "Tasa";
 
cfdi.Conceptos[0].Impuestos.Retenciones[0]["Base"] = "1";
cfdi.Conceptos[0].Impuestos.Retenciones[0]["Impuesto"] = "001";
cfdi.Conceptos[0].Impuestos.Retenciones[0]["TipoFactor"] = "Tasa";
cfdi.Conceptos[0].Impuestos.Retenciones[0]["TasaOCuota"] = "0.100000";
cfdi.Conceptos[0].Impuestos.Retenciones[0]["Importe"] = "0.10";
 
cfdi.Conceptos[0].Impuestos.Retenciones[1]["Base"] = "1";
cfdi.Conceptos[0].Impuestos.Retenciones[1]["Impuesto"] = "002";
cfdi.Conceptos[0].Impuestos.Retenciones[1]["TipoFactor"] = "Tasa";
cfdi.Conceptos[0].Impuestos.Retenciones[1]["TasaOCuota"] = "0.106666";
cfdi.Conceptos[0].Impuestos.Retenciones[1]["Importe"] = "0.11";
 
// Complemento de Instituciones Educativas
var iedu = new IsaRoGaMX.Cfdi.Iedu.v10.InstEducativas();
iedu["CURP"] = "AAAA010101HNLRNL09";
iedu["autRVOE"] = "PEFHS821923KSDJ823";
iedu["nivelEducativo"] = "Secundaria";
iedu["nombreAlumno"] = "Nombre Alumno";
iedu["rfcPago"] = "BBB010101AAA";
 
// Se agrega al concepto
cfdi.Conceptos[0].AgregaComplemento(iedu);
 
// Impuestos
cfdi.Impuestos["TotalImpuestosRetenidos"] = "0.21";
cfdi.Impuestos["TotalImpuestosTrasladados"] = "0.16";
 
cfdi.Impuestos.Retenciones[0]["Impuesto"] = "001";
cfdi.Impuestos.Retenciones[0]["Importe"] = "0.10";
 
cfdi.Impuestos.Retenciones[1]["Impuesto"] = "002";
cfdi.Impuestos.Retenciones[1]["Importe"] = "0.11";
 
cfdi.Impuestos.Traslados[0]["Base"] = "1";
cfdi.Impuestos.Traslados[0]["Importe"] = "0.16";
cfdi.Impuestos.Traslados[0]["Impuesto"] = "002";
cfdi.Impuestos.Traslados[0]["TasaOCuota"] = "0.160000";
cfdi.Impuestos.Traslados[0]["TipoFactor"] = "Tasa";
 
// Genera cadena original
string cadenaOriginal = XsltTransform.GeneraCadenaOriginal(cfdi.Documento, xsltFile);
 
// Genera sello
cfdi["Sello"] = CsdUtils.GeneraSelloFiscalDigital(cadenaOriginal, keyFile, password);
 
// Se guarda el XML
cfdi.Documento.Save("F1234.xml");