C#: CRUD Blazor WebAssembly con ASP.NET Core

En el día de hoy vamos a aprender a realizar un CRUD muy simple con Blazor WebAssembly y ASP.NET. Generaremos una API REST con ASP.Net Core y la consumiremos con Blazor.

Crud Blazor WebAssembly

Para realizar este CRUD realizaremos una aplicación que nos permitirá gestionar películas, podría servir para un cine o un videoclub. Crearemos las llamadas Api para agregar, editar, listar y eliminar películas y las consumiremos con Blazor WebAssembly.

Requisitos

  • Última versión de Visual Studio 2019
  • SQL Server
  • Nociones básicas en C#

Generando el proyecto Blazor WebAssembly

El primer paso es abrir Visual Studio y generar un nuevo proyecto Blazor WebAssembly.

Crear proyecto Blazor WebAssembly

Es importante no confundir Blazor Server App con Blazor WebAssembly, nosotros ahora trabajaremos con Blazor WebAssembly más adelante ya explicaré que diferencia hay entre uno y otro.

Como nombre de proyecto escribimos «FilmCrud» tanto para el nombre de proyecto como de la solución. Es importante que no cambies el nombre, ya que de hacerlo también te cambiara los Namespaces y es posible que el código de este tutorial no te funcione.

Para la ubicación puedes escoger la que quieras, eso sí, te recomiendo que sea lo más cercana a la raíz del disco duro y que no tenga caracteres raros en el nombre de la carpeta, Visual estudio no se lleva nada bien con ellos.

Después saltamos al siguiente paso pulsando en siguiente.

Crear proyecto Blazor WebAssembly

En el siguiente paso tenemos varias opciones que escoger, para este tutorial escogemos «.Net 5.0» como plataforma de destino, tipo de autentificación ninguno, para acabar marcamos los checks de «Configurar para HTTPS» y «ASP.NET Core».

Crear proyecto Blazor WebAssembly

Para futuros proyectos debes de saber que marcar «ASP.NET Core hospedado» lo que hará será que en el mismo proyecto nos incluirá Blazor WebAssembly y ASP.NET Core, esta opción solo la debes marcar en caso de que aún no dispongas de una API que consumir con Blazor WebAssembly.

Habiendo hecho estos pasos ya hemos generado nuestro proyecto, pasemos a ver la estructura del mismo.

Estructura proyecto Blazor WebAssembly

El proyecto generado tendrá la estructura que puedes ver en la siguiente imagen. En el caso que no la tuviese seguramente es porque no has marcado la opción «ASP.Net Core hospedado» en el último paso.

Estructura proyecto Blazor WebAssembly

En la solución generada puedes apreciar que son 3 proyectos:

  • FilmCrud.Client: es el proyecto de Blazor WebAssembly lo que el usuario verá cuando navegue por nuestra Web, lo que se conoce como el FrontEnd.
  • FilmCrud.Server: es el proyecto de ASP.Net Core, donde generaremos las llamadas de la API, nuestro BackEnd.
  • FilmCrud.Shared: es una biblioteca que los dos proyectos anteriores tienen referenciada, por lo que cualquier clase que pongamos aquí podrá ser utilizada por cualquier proyecto de la solución.

Instalando paquetes Nuget

Para poder utilizar Entity Framework con SQL Server debemos de instalar los paquetes Nuget que ves en la imagen en los proyectos «FilmCrud.Server» y «FilmCrud.Shared». En el cliente no porque las llamadas a la base de datos no se hacen desde el FrontEnd sino desde nuestro BackEnd y luego se consumen por el FrontEnd.

Paquetes Nuget Entity Framework

Para instalar un paquete Nuget tienes que hacer click derecho sobre Solución Film Crud y «Administrar paquetes Nuget para la solución». En la ventana que se abre vamos buscando los paquetes en la pestaña examinar y los vamos instalando en los proyectos comentados anteriormente.

NOTA: La versión 5.0.5 tiene un bug que no te permitirá seguir el tutorial, probablemente cuando tú leas esto ya habrán reparado el problema. Sin embargo, si tienes problemas al realizar el scaffolding del controlador instala la versión 5.0.4.

Creación del modelo

El modelo es lo que nos permite interactuar con la base de datos, básicamente los modelos deben de crearse tal y como los queremos en la base de datos, ya que con Entity Framework serán un reflejo de la tabla.

Para tener todo más organizado vamos a nuestro proyecto «FilmCrud.Shared» le hacemos click derecho -> Agregar -> Nueva carpeta a esta carpeta le ponemos el nombre «Models», nuevamente hacemos click derecho sobre la carpeta que acabamos de crear, Agregar -> Clase, la clase la llamaremos Film.

Blazor WebAssembly shared

El resultado debe de ser como el de la imagen. Si en un futuro quieres agregar más modelos debes de saber que por norma general se ponen en inglés y en singular.

Abrimos la clase Film eliminamos todo el contenido y escribimos el siguiente código:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace FilmCrud.Shared.Models
{
    public class Film
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public int Duration { get; set; }
        public DateTime CreatedAt { get; set; }
        public DateTime UpdateAt { get; set; }
    }
}

Si te fijas con las variables [Key] y [DatabaseGenerated(DatabaseGeneratedOption.Identity)] indicamos a Entity Framework que Id será el campo id en nuestra base de datos.

Creando el BackEnd

Vamos a proceder a crear nuestro BackEnd que será quien realice las peticiones a la base de datos y nos devuelva los resultados. El BackEnd también será donde deberíamos tratar los datos o hacer cálculos si fuese necesario.

En este apartado solo trabajaremos con el proyecto de ASP.NET: FilmCrud.Server.

Vinculando Entity Framework a la base de datos

Para vincular nuestra base de datos con Entity Framework tenemos que hacer una serie de configuraciones, debemos crear lo que se conoce como un DBContext que es una clase en la que «mapeamos» los modelos para después interactuar con ellos mediante Entity Framework.

En el proyecto «FilmCRUD.Server» creamos una nueva carpeta con el nombre Data y dentro una clase llamada AppDbContext, en esta clase borramos todo el contenido y escribimos lo siguiente:

using FilmCrud.Shared.Models;
using Microsoft.EntityFrameworkCore;

namespace FilmCrud.Server.Data
{
    public class AppDbContext : DbContext
    {

        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        {

        }

        //Referenciamos los modelos que vaya a utilizar Entity Framework. 
        public virtual DbSet<Film> Films { get; set; }

    }
}

Mediante esta clase estamos heredando del DbContext del Entity Framework y referenciando al modelo que hemos creado en el paso anterior. Todo modelo que quieras que interactúe con la base de datos debe agregarse en este fichero.

Para que ASP.Net utilice el Context que hemos creado debemos incluirlo en Startup.cs del proyecto que se encuentra en la raiz. En el método «ConfigureServices» agregamos el siguiente código:

public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllersWithViews();

            //Añadimos nuestro DBContext
            services.AddDbContext<AppDbContext>(option => option.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));

            services.AddRazorPages();
        }

Con esto nuestro ASP ya está utilizando el DBContext, sin embargo, si te fijas estamos cogiendo la connection string de «DefaultConnection» pero todavía no hemos configurado la connection string, vamos a ello.

En la raíz del directorio también tenemos el fichero appsettings.json en este es donde deberemos configurar nuestra connection string, esto ya depende de como tengáis configurado vuestro SQL Server en mi caso lo tengo en la máquina local y utilizo la autentificación por usuario de Windows.

Vosotros deberéis agregar la connection todo el código sera igual excepto la connection string que la tendréis que adaptar. En esta página tenéis ejemplos de configuración.

Nota: Entity Framework no crea la base de datos, por lo que la tenéis que crear manualmente mediante SQL Management Tool.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=Torre;Database=Films;Integrated Security=true;"
  }
}

Generar migraciones y ejecutarlas

Llego el momento de ver si hemos realizado la configuración correctamente. Para ejecutar la generación de migraciones debemos ir en Visual Studio a Herramientas-Administrador de Paquetes Nuget-Consola del Administrador de paquetes.

En ella escribimos Add-Migration comando que nos creará las migraciones en la base de datos. Nos preguntará un nombre puedes escribir el que quieras, como recomendación utilizar nombres descriptivos en mi caso escribo «Crear tabla Films».

Entity Framework Add-Migration

Después escribimos Update-Database comando que nos ejecutará las migraciones en nuestra base de datos creando la tabla Films.

Entity Framework Update-Database

Si no has tenido ningún error en tu base de datos ya puedes apreciar que tienes una tabla con exactamente el mismo formato que tu modelo, he aquí la magia de Entity Framework.

Tabla agregada con Entity Framework

Si te acuerdas cuando creábamos el modelo te he comentado de la importancia del nombre ponerlo en singular y en inglés esto es porque después Entity Framework ya te crea el nombre de la tabla en plural y el del controlador también como veremos más adelante.

Generar controller

Para generar el controller simplemente debemos hacer click derecho sobre la carpeta controllers y escoger Agregar-Nuevo controlador.

Se nos abrirá una ventana en la izquierda tenemos un menú en el que deberemos seleccionar API y seleccionamos Controlador de API con acciones que usan Entity Framework.

Scaffolding ASP.NET Core

Después se nos abrirá una ventana en la que deberemos seleccionar Film como clase de modelo, AppDbContext como clase de contexto y en nombre de controlador dejar el que viene por defecto FilmsController (fíjate que nuevamente pluraliza automáticamente).

Scaffolding ASP.NET Core

Este asistente nos generará todo el código para necesario para el funcionamiento de nuestra API REST. Únicamente tienes que agregar el código marcado a FilmsController para que funcionen los timestamps de las tablas.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using FilmCrud.Server.Data;
using FilmCrud.Shared.Models;

namespace FilmCrud.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class FilmsController : ControllerBase
    {
        private readonly AppDbContext _context;

        public FilmsController(AppDbContext context)
        {
            _context = context;
        }

        // GET: api/Films
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Film>>> GetFilms()
        {
            return await _context.Films.ToListAsync();
        }

        // GET: api/Films/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Film>> GetFilm(int id)
        {
            var film = await _context.Films.FindAsync(id);

            if (film == null)
            {
                return NotFound();
            }

            return film;
        }

        // PUT: api/Films/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPut("{id}")]
        public async Task<IActionResult> PutFilm(int id, Film film)
        {
            if (id != film.Id)
            {
                return BadRequest();
            }

            film.UpdateAt = DateTime.Now;
            _context.Entry(film).State = EntityState.Modified;

            try
            {               
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!FilmExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/Films
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPost]
        public async Task<ActionResult<Film>> PostFilm(Film film)
        {
            film.CreatedAt = DateTime.Now;
            film.UpdateAt = DateTime.Now;

            _context.Films.Add(film);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetFilm", new { id = film.Id }, film);
        }

        // DELETE: api/Films/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteFilm(int id)
        {
            var film = await _context.Films.FindAsync(id);
            if (film == null)
            {
                return NotFound();
            }

            _context.Films.Remove(film);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private bool FilmExists(int id)
        {
            return _context.Films.Any(e => e.Id == id);
        }
    }
}

Con esto ya hemos acabado nuestro BackEnd y ya podemos ponernos con el FrontEnd

Creando el FrontEnd

Mi fuerte no es el diseño así que realizaremos una interfaz lo más simple posible y posteriormente cada uno que la adapte como quiera.

En el FrontEnd consumiremos la API que acabamos de crear, es importante que sepas que prácticamente todo el código que escribes en el FrontEnd puede verse por lo que no escribas contraseñas o datos que no quieras que el usuario pueda ver, por ese motivo no se realizan las peticiones a la base de datos directamente desde FrontEnd y hay un API de por medio.

A partir de ahora trabajaremos exclusivamente con el proyecto: FilmCrud.Client.

Conceptos básicos de Blazor

En las páginas de Blazor si queremos que se puedan acceder vía URL debemos de especificar que URL tendrá mediante el @page «ruta», al igual que en las clases de C# debemos hacer los using de los Namespace que utilizaremos esto se hace mediante el @using namepace.

También inyectaremos @inject HttpClient Http que nos servirá para hacer las peticiones a la API y @inject NavigationManager Navigation que se utilizará para poder navegar entre componentes.

Si te fijas el código C# se escribe entre @code { } esto es para indicarle a Blazor que el contenido es código C# y no HTML.

En el HTML podemos hacer bucles y condiciones simplemente poniendo una @ delante y siguiendo la estructura habitual. En caso de que quieras acceder a las variables del bucle pues igual, nombre de variable con la @ delante.

Otro punto importante son los @onclick que utilizamos para hacer referencia a una función o método que se ejecute al pulsar sobre un elemento y los @bind-Value que sirve para vincular el valor de un campo del formulario a una variable en concreto.

Estructura de carpetas

En el proyecto FilmCrud.Client pulsamos sobre Pages y creamos una nueva carpeta llamada Films, en ella pulsamos sobre agregar y componente razor.

Generamos los siguientes:

  • Create.razor: mediante el cual podremos añadir películas.
  • Edit.razor: para editar películas ya creadas.
  • Index.razor: para listar las películas.

Listar películas (Index.razor)

@page "/films"
@using FilmCrud.Shared.Models
@inject HttpClient Http
@inject NavigationManager Navigation

<h3>Películas</h3>

@if (_films == null)
{
    <p><em>Cargando...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Id</th>
                <th>Title</th>
                <th>Description</th>
                <th>Duration</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var film in _films)
            {
                <tr>
                    <td>@film.Id.ToString()</td>
                    <td>@film.Title</td>
                    <td>@film.Description</td>
                    <td>@film.Duration</td>

                    <td>
                        <button class="btn btn-info"
                                @onclick="(() => Edit(film.Id))">
                            Edit
                        </button>
                        <button class="btn btn-danger"
                                @onclick="(() => Delete(film.Id))">
                            Delete
                        </button>
                    </td>
                </tr>
            }
        </tbody>
    </table>
    <div>
        <button class="btn btn-success" @onclick="Create">Añadir película</button>
    </div>
}

@code {
    

    private List<Film> _films;

    protected override async Task OnInitializedAsync()
    {
        _films = await Http.GetFromJsonAsync<List<Film>>("/api/Films");
    }

    private async Task Delete(int id)
    {
        await Http.DeleteAsync($"/api/Films/{id}");
        _films = await Http.GetFromJsonAsync<List<Film>>("/api/Films");
        StateHasChanged();
    }

    private void Edit(int id)
    {
        Navigation.NavigateTo($"/film/edit/{id}");
    }

    private void Create()
    {
        Navigation.NavigateTo("/film/create");
    }
}

En esta página tenemos una lista de la clase Film que es _films, en ella deberemos cargar la lista de las películas que tenemos en la BD. Si te fijas tenemos el método OnInitializedAsync() que lo que hace es llamar a nuestra API para obtener las películas con esto ya tenemos las películas cargadas.

En el código HTML tenemos una condición que es if(_films == null) para mostrar cargando en el caso de que aún no se haya ejecutado el OnInitializedAsync() si no lo hacemos así estaríamos al bucle con un null y tendríamos un error.

Tenemos también el método Delete en el que le pasaríamos el id de la película a eliminar, despues volveríamos a recargar la variable _films con los elementos actuales de la API y por último, ejecutamos el método StateHasChanged() que nos refrescaría la tabla.

Siempre que hagamos un cambio en los elementos, hay que llamar a StateHasChanged(), esta función básicamente nos volverá a ejecutar el bucle @foreach (var film in _films).

Para acabar tenemos los métodos Create y Edit que nos redirigen a las páginas correspondientes, en el caso del edit también le pasamos el id para que la página sepa que película tiene que editar.

Creación de películas (Create.razor)

@page "/film/create"
@using FilmCrud.Shared.Models
@inject HttpClient Http
@inject NavigationManager Navigation

<h3>Añadir película</h3>

<EditForm Model="@_film" OnValidSubmit="Post">
    <div class="form-group">
        <label>Título: </label>
        <InputText @bind-Value="_film.Title" />
    </div>

    <div class="form-group">
        <label>Descripción</label>
        <InputTextArea @bind-Value="_film.Description"/>
    </div>

    <div class="form-group">
        <label>Summary: </label>
        <InputNumber @bind-Value="_film.Duration"/>
    </div>

    <div class="form-group">
        <input type="submit" class="btn btn-success" value="Agregar pelicula" />
    </div>
</EditForm>

@code {

    private Film _film = new();

    private async Task Post()
    {
        await Http.PostAsJsonAsync<Film>("/api/Films/", _film);
        Navigation.NavigateTo("/films");
    }


}

En esta página vemos un nuevo elemento que es el <EditForm> que básicamente cumple la función de generar la etiqueta <FORM> en el HTML. Este elemento necesita que le pasemos el objeto que enviaremos por el formulario mediante Model=»@_film» y el método que ejecutará el form en el momento de que el usuario pulse enviar que se lo indicamos con OnValidSubmit=»Post».

Fíjate que los campos del formulario tienen diferentes tipos, pero todos son <InputTipo>, en esta página tienes todos los tipos que hay, si tienes alguna necesidad especifica debes de saber que hay paquetes Nuget que amplían los campos, pero de eso ya hablaremos en otro momento.

Otra cosa importante es el @bind-Value que nos permite vincular el valor de un formulario a la propiedad del modelo en tiempo real. Por lo que si escribes en cualquier input se actualiza al momento en nuestro objeto _film.

Para acabar tenemos el método post que se encarga de enviar los datos a nuestra API. Para ello realizamos un Post especificándole el modelo Film y la ruta de la API, él solo se encargará de generar el JSON de forma transparente y enviarlo. Una vez enviado redireccionamos otra vez al usuario a la lista de películas.

Edición de películas (Edit.razor)

@page "/film/edit/{id:int}"
@using FilmCrud.Shared.Models
@inject HttpClient Http
@inject NavigationManager Navigation

<h3>Añadir película</h3>

@if (_film == null)
{
    <p><em>Cargando...</em></p>
}
else
{
    <EditForm Model="@_film" OnValidSubmit="Put">
        <div class="form-group">
            <label>Título: </label>
            <InputText @bind-Value="_film.Title" />
        </div>

        <div class="form-group">
            <label>Descripcion</label>
            <InputTextArea @bind-Value="_film.Description" />
        </div>

        <div class="form-group">
            <label>Summary: </label>
            <InputNumber @bind-Value="_film.Duration" />
        </div>

        <div class="form-group">
            <input type="submit" class="btn btn-success" value="Editar pelicula" />
        </div>
    </EditForm>
}


@code {
    [Parameter]
    public int Id { get; set; }

    private Film _film = null;

    protected override async Task OnInitializedAsync()
    {
        _film = await Http.GetFromJsonAsync<Film>($"/api/Films/{Id}");
    }


    private async Task Put()
    {
        await Http.PutAsJsonAsync<Film>($"/api/Films/{_film.Id}", _film);
        Navigation.NavigateTo("/Films");
    }

}

No hay mucho que comentar aquí, ya que es prácticamente idéntico al Create, lo que sí que debes tener en cuenta es recoger el Id que le enviamos desde la lista.

Para recoger el id especificamos @page «/film/edit/{id:int} con esto le estamos diciendo que la ruta es /film/edit/ y que requiere que aparte se le pase un id en formato integer.

Para cazar este valor desde el código de C# debemos escribir:

[Parameter]
public int Id { get; set; }

Esto lo que hace es almacenarnos el Id en esta variable. Esta variable debe de ser pública.

Por último, replicamos el comportamiento de Index, cargamos el objeto en la variable _film y mientras que no se haya cargado mostramos «Cargando» y para el Put similar a Edit solo que esta vez ya no es un Post sino un Put para cumplir los entandares REST.

Incluir películas en el menú de Blazor

Para acabar este CRUD de Blazor WebAssembly simplemente debemos de actualizar el menú para agregar un elemento que nos lleve al listado de películas.

Para esto debemos editar el fichero NavMenu.razor que se encuentra en la carpeta Shared.

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">FilmCrud</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>

        <li class="nav-item px-3">
            <NavLink class="nav-link" href="films">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Películas
            </NavLink>
        </li>
    </ul>
</div>

@code {
    private bool collapseNavMenu = true;

    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

Descargar código fuente

Puedes ver el código fuente de este proyecto en Github.

Deja un comentario