Categoría: Programación

Apuntes de programación, algoritmos, pruebas, ejemplos o ejercicios resueltos para practicar o resolución de problemas relacionados con la programación

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.

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.

.NET Fiddle – C# sin Visual Studio

En algunas ocasiones se necesita probar algún concepto o ver si es posible hacer algo de forma rápida.

Actualmente trabajo bastante con C# y dado que es molesto tener que crear un proyecto solo para probar algo sencillo y ejecutar el mastodonte que es Visual Studio solo para algo tan pequeño, encontré una solucion que se adapta este caso en especifico.

Se trata de dotnetfiddle.net, es un compilador online de C#, permite crear proyectos sencillos de Consola, MVC, Script y Nancy; entre sus opciones también permite seleccionar la versión de C# que queremos usar.

Esta herramienta me gusta bastante porque es bastante sencilla, permite crear snippets de código y probarlos desde el navegador, también puedes generar un enlace para compartirlo o incluso puedes trabajar al mismo tiempo sobre el mismo código con alguien más(esto último no lo he probado aún).

Para muestra un botón, sabia que existian los Extension Methods pero jamás los habia usado y me puse a revisar, ya que necesito formatear objetos DateTime a string pero quiero evitar el tener que escribir ToString(@”yyyy-MM-dd\THH:mm:ss”) cada vez que lo necesito, así que se me ocurrió intentar crear un Extension Method , así aprendia a usarlas y ademas me servia, ya que si en algun momento el formato de fecha que debo usar cambia, solo cambio el formato es la Extension Method.

Así que este fue el resultado: