2020/04/25

Mis indispensables de C#. Parte 2: Extensiones

Esta es una serie de 3 artículos cortos sobre características de C# que uso en todas mis aplicaciones.

Extensiones… Muchas extensiones


Me gusta usar extension methods para validaciones. Aquí hay algunas básicas.

public static class ValueExtensions
{
    public static string ThrowIfEmpty(this string value)
    {
        return string.IsNullOrWhiteSpace(value) ?
            throw new ArgumentNullException() :
            value.Trim();
    }

    public static string TrimOrThrow(this string value)
    {
        return value?.Trim() ??
            throw new ArgumentNullException();
    }

    public static Guid ThrowIfEmpty(this Guid value)
    {
        return value == Guid.Empty ?
            throw new ArgumentNullException() :
            value;
    }

    public static DateTime ThrowIfInPast(this DateTime value)
    {
        return value <= DateTime.Now ?
            throw new ArgumentOutOfRangeException() :
            value;
    }

    public static DateTime NowIfDefault(this DateTime value)
    {
        return value == default(DateTime) ?
            DateTime.Now :
            value;
    }
}

Es muy sencillo validar parámetros usando estas extensiones.

public async Task<TransferObject> DoSomething(
    string userId,
    Guid roleId,
    string name,
    DateTime meetingDate,
    DateTime date)
{
    userId.ThrowIfEmpty();
    roleId.ThrowIfEmpty();
    name = name.TrimOrThrow();
    meetingDate.ThrowIfInPast();
    date = date.NowIfDefault();

    return await _store.SaveSomething(userId, roleId, name, meetingDate, date);
}

También las uso para interactuar con la base de datos, si por algún motivo no estoy usando un ORM.

public static class SqlExtensions
{
    public static string GetStringOrNull(this SqlDataReader reader, int index)
    {
        return reader.IsDBNull(index) ?
            null :
            reader.GetString(index);
    }

    public static DateTime GetDateTimeOrNow(this SqlDataReader reader, int index)
    {
        return reader.IsDBNull(index) ?
            DateTime.Now :
            reader.GetDateTime(index);
    }

    public static DateTime? GetDateTimeOrNull(this SqlDataReader reader, int index)
    {
        return reader.IsDBNull(index) ?
            null :
            reader.GetDateTime(index);
    }

    public static Guid GetGuidOrEmpty(this SqlDataReader reader, int index)
    {
        return reader.IsDBNull(index) ?
            Guid.Empty :
            reader.GetGuid(index);
    }

    public static Guid? GetGuidOrNull(this SqlDataReader reader, int index)
    {
        return reader.IsDBNull(index) ?
            null :
            reader.GetGuid(index);
    }

    public static void SetSqlDBNullParameters(this SqlCommand command)
    {
        foreach (IDataParameter param in command.Parameters)
        {
            if (param.Value == null)
            {
                param.Value = DBNull.Value;
            }
        }
    }
}

Así es muy fácil manejar las columnas opcionales de un insert.

string itemKey = "blog.lvbernal.com";
string itemValue = null;

string nonQuery = @"
    INSERT INTO [some].[table] ([key], [value])
    VALUES (@key, @value)";

using SqlConnection conn = new SqlConnection(_connectionString);
conn.Open();

using SqlCommand cmd = new SqlCommand(nonQuery, conn);
cmd.Parameters.AddWithValue("@key", itemKey);
cmd.Parameters.AddWithValue("@value", itemValue);  // Esto genera un error

cmd.SetSqlDBNullParameters();  // Aquí reemplazamos los null por DBNull.Value

Y leer datos desde un SqlDataReader.

using SqlDataReader reader = await cmd.ExecuteReaderAsync();

if (reader.HasRows)
{
    while (reader.Read())
    {
        var item = new ItemDTO(
            id: reader.GetGuid(0),
            name: reader.GetStringOrNull(1),
            owner: reader.GetGuidOrNull(2)
        );

        list.Add(item);
    }
}

2020/04/23

Mis indispensables de C#. Parte 1

Esta es una serie de 3 artículos cortos sobre características de C# que uso en todas mis aplicaciones... aunque este en particular empieza con una nueva.

Switch expressions


Las switch expressions son mi característica favorita de C# 8. Ayudan a asignar valores rápidamente y las uso mucho para renderizar la interfaz gráfica.

Supongamos una Partial View que muestra el nombre de una empresa, consultor o institución, y un botón de color distinto para cada caso.

@model MyViewModel

@{
    var (title, color) = Model.RoleType switch
    {
        RoleType.Company => ("Empresa", "info"),
        RoleType.Institution => ("Institución", "primary"),
        RoleType.Mentor => ("Consultor", "warning"),
        _ => ("?", "danger")
    };
}

<h3>
    @Model.Name
</h3>

<button class="btn btn-@color">
    @title
</button>

También funciona con métodos, si no tenemos una partial para cada item.

@model MenuViewModel

@{
    string GetLinkIcon (string linkPage) => linkPage switch
    {
        MenuViewModel.PageIndex => "fe fe-home",
        MenuViewModel.PageTools => "fe fe-box",
        MenuViewModel.PagePrograms => "fe fe-arrow-up-right",
        MenuViewModel.PageConnections => "fe fe-users",
        MenuViewModel.PageRequests => "fe fe-bell",
        _ => ""
    };

    string GetLinkCls (string linkPage) => linkPage == Model.Current ? "active" : "";
}

<ul class="nav flex-column d-none d-md-block">
    @foreach (var p in Model.Pages)
    {
        <li class="nav-item @GetLinkCls(p)">
            <a class="nav-link" asp-action="@p">
                <span class="@GetLinkIcon(p)"></span>
            </a>
        </li>
    }
</ul>

Propiedades de sólo lectura


Nos acostumbramos tanto a las propiedades con get y set públicos, que las escribimos de forma automática y se nos olvida que las clases son el primer filtro para evitar errores en capas inferiores.

Podemos usar un set privado si la propiedad es de sólo lectura.

public string Name { get; private set; }

Pero yo prefiero quitar el set para que se asigne únicamente desde el constructor y forzar la regla de negocio.

public string Name { get; }

public MyClass(string name)
{
    Name = name;
}

2020/04/19

Aprovechando las Partial Views en ASP.NET Core (Parte 1)

Si trabajan con ASP.NET Core, seguramente han separado los componentes de su interfaz gráfica usando Partial Views o Partials. Son una característica básica del framework junto a las Sections y los Layouts.

Cuando se empezaron a popularizar los frameworks de front-end como Angular, Ractive, React o Vue (y recientemente el estándar WebAssembly), las partials quedaron en un segundo plano porque al ser renderizadas desde el servidor, "no son dinámicas". Ese dinamismo lo están agregando con los Razor Components y Blazor, pero esa es otra historia.

Y aquí viene la opinión impopular: muy pocas aplicaciones son tan complejas como para justificar un framework de front-end. A eso agregaría: muy pocas aplicaciones son tan complejas como para justificar una conexión permanente con el servidor, que es como funcionan los Razor Components. Y una más: que un elemento aparezca o desaparezca de una lista, sin recargar la página, no justifica ninguna complejidad accidental.

Con un poco de javascript se pueden aprovechar las partials para construir aplicaciones web suficientemente dinámicas.

Supongamos una aplicación que permite asignar objetivos a una empresa. Cada usuario puede tener múltiples empresas, pero las administra de forma independiente. El controlador responde a la URL:

/{controller=Company}/{companyId:guid}/{action=Index}/{id?}

El método (action) Index se ve más o menos así:

public async Task<IActionResult> Index(Guid companyId)
{
    var objectives = await _objectiveService.GetObjectives(UserId, companyId);
    var model = objectives.Select(o => ObjectiveViewModel.FromObjective(o));
    return View(model);
}

La view usa un @foreach para renderizar la lista desde el servidor y cada vez que agregamos un nuevo objetivo, se recarga la página. El sitio se demora en cargar y no hay ningún spinner. ¡Horror!. ¿Justifica usar un framework de front-end? Yo creo que no. ¿Justifica hacer todo con javascript y web APIs? Tampoco. Sigamos usando las herramientas que tiene el framework.

Lo primero es crear un método que retorna la lista de objetivos ya renderizada:

public async Task<IActionResult> ObjectivesComponent(Guid companyId)
{
    var objectives = await _objectiveService.GetObjectives(UserId, companyId);
    var model = objectives.Select(o => ObjectiveViewModel.FromObjective(o));
    return PartialView("_ObjectivesPartial", model);
}

Podemos simplificar el Index:

public IActionResult Index(Guid companyId)
{
    var model = new IndexViewModel(companyId);
    return View(model);
}

La view tiene un contenedor que muestra el spinner y carga otras dos partials, una con el script y otra con el modal que se despliega para agregar un nuevo objetivo:

@model IndexViewModel

<div id="objectivesHolder">
    <div class="spinner-border text-primary" role="status">
        <span class="sr-only">Cargando...</span>
    </div>
</div>

<button data-toggle="modal" data-target="#addObjectiveModal" class="btn btn-primary">
    Agregar objetivo
</button>

@section Scripts
{
    @{ await Html.RenderPartialAsync("_IndexScripts", Model.Id); }
}

@section EndOfPage
{
    @{ await Html.RenderPartialAsync("_AddObjectiveModal"); }
}

La partial _IndexScripts tiene un manejador de interfaz muy pequeño:

@model Guid

<script text="text/javascript">
    $(() => {
        // No me gusta inyectar valores del modelo directamente al script,
        // así que los paso como parámetros y desde un solo sitio
        var mrg = new IndexUIMgr("@Html.Raw(Model)");
    });

    class IndexUIMgr {
        constructor(id) {
            this.companyId = id;
            this.configureModal();
            this.loadObjectives();
        }

        configureModal() { }

        async loadObjectives() { }
    }
</script>

El método que carga la partial es loadObjectives:

async loadObjectives() {
    let response = await fetch(`/company/${this.companyId}/objectivesComponent`);

    if(response.ok) {
        let content = await response.text();
        $("#objectivesHolder").html(content);
        // Habilitar otros botones, por ejemplo,
        // los de editar o completar objetivo
    } else {
        $("#objectivesHolder").html("No fue posible cargar el contenido");
    }
}

¡Listo! Ya cargamos la partial de forma dinámica. Ahora sigue el botón de agregar. Yo uso un método genérico para habilitar los formularios:

enableModalForm(modalSelector, submitBtnSelector, formSelector, callback) {
    let modal = $(modalSelector);
    let btn = $(submitBtnSelector);
    let form = $(formSelector);

    btn.on("click", () => {

        // jquery-validation
        if (!form.valid())
        {
            return false;
        }

        form.submit((ev)=>{
            // Evitar múltiples clicks
            btn.prop("disabled", true);

            // Submit sin recargar la página
            $.ajax({
                type: form.attr("method"),
                url: form.attr("action"),
                data: form.serialize(),
                success: (data) => {
                    modal.modal("hide");
                    form.trigger("reset");
                    form.off("submit");
                    callback();
                    btn.prop("disabled", false);
                },
                error: (err) => {
                    form.off("submit");
                    btn.prop("disabled", false);
                }
            });

            ev.preventDefault();
        });
    });
}

El método configureModal llama a enableModalForm:

let context = this;
context.enableModalForm(
    "#addObjectiveModal",
    "#addObjectiveBtn",
    "#addObjectiveForm",
    () => { context.loadObjectives(); });

Y por último, el endpoint para recibir el formulario:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<bool> AddObjective(Guid companyId, AddObjectiveViewModel model)
{
    var created = await _objectiveService.AddObjective(
        userId: UserId,
        companyId: companyId,
        title: model.Title
    );

    return created;
}

Para el botón de editar, basta con llenar el formulario (ej. #editObjectiveForm) antes de mostrar el modal; el resto es igual. Si los formularios son muy grandes, se puede integrar un template engine que ayude a mapear los valores, pero creo que en ese caso es mejor usar una página separada.

Cuando se agrega un nuevo objetivo, el componente se actualiza completamente. La página no se recarga y el scroll no se va hacia arriba. No hay que preocuparse por renderizar un elemento falso mientras se crea "por detrás" el definitivo.

Esta idea no es nueva y hay herramientas muy interesantes como turbolinks que ayudan en el proceso. Pero me parece que en ASP.NET no está suficientemente documentada.

¿Cuándo usar todo un framework? En mi opinión, cuando hay un builder como el de Lucidchart, para herramientas colaborativas como Google Docs, o para aplicaciones en donde hay muchos parámetros e interacciones, como en los buscadores. ¿Qué tanta interacción se puede manejar sin framework? Puedo decir que la suficiente. Todo este sitio está hecho así:


2020/04/14

¿Y qué pasó con CaliSharp?

¡Por supuesto que seguimos! Solo que le perdí el interés a publicar fotos. Creo que el ejercicio consiste en invitar a quienes más podamos a los eventos y compartir con los que lleguen. Crear comunidad.

Llegó un momento, que según mi Instagram fue por allá en septiembre de 2019, en el que ese último paso de publicar fotos y artículos de agradecimiento me empezó a sonar a “mira de lo que te perdiste por no asistir”, o peor, “mira lo que hice este mes”. Así que paré.

Desde noviembre tuvimos charlas como “Desarrollo de plataformas”, “Introducción a radio definida por software”, “Azure Functions: Experiencias de un principiante”, “Súper tendencias de arquitectura y desarrollo de software para 2020”, “¿Debería dar una charla?”, “Blazor: C# en el browser”, “Tecnología para Industria 4.0” y “REST, y por qué lo estás haciendo mal”. Y esperamos seguir organizando muchas más.

Recuerden que pueden inscribirse al meetup CaliSharp para conocer los próximos eventos. Y si les interesa dar una charla, sólo deben llenar este formulario, que tiene un aire vintage pero funciona muy bien.

PD: Vienen artículos técnicos sobre .NET Core.