Estamos escribiendo una aplicación en la pila C#/Habr

¡Hola a todos! Mi nombre es Dmitri Bajtenkovy soy desarrollador de .NET. Hoy realizaremos un experimento: escribiremos una aplicación web completa utilizando soluciones escritas en C# y la plataforma .NET. Más de mis artículos se pueden leer en los medios. ACCESO.

¿Qué quiero decir?

Como sabemos, en general, una aplicación web consta de un backend, un frontend, una base de datos y, en ocasiones, un caché. Con el backend y el frontend todo está claro: tenemos un maravilloso framework ASP.NET Core para el servidor y páginas blazor o razor para el cliente. Sin embargo, las partes de infraestructura de la aplicación (bases de datos, cachés) suelen estar escritas en otros lenguajes de nivel inferior, como C y C++.

Afortunadamente, Microsoft lanzó recientemente una solución de almacenamiento en caché similar a Redis llamada Granate. Puede utilizar una base de datos de documentos como base de datos principal. CuervoDBque está escrito en C#.

Escribimos una aplicación en la pila de C#.

Escribimos una aplicación en la pila de C#.

¿Qué estamos codificando?

Entonces, hemos ordenado la pila. Tendremos:

  • ASP.NET Core para el backend;

  • Blazor para la interfaz;

  • RavenDB como DBMS principal;

  • Granate para caché.

Hello World en el mundo de las aplicaciones web se considera varias hojas de tareas pendientes. Escribiremos una aplicación similar.

Creé una solución vacía en el IDE y luego le agregué un proyecto ASP.NET Core Web API:

Backend: repositorio y puntos finales

RavenDB es una base de datos NoSQL orientada a documentos diseñada para simplificar la gestión de datos y proporcionar un alto rendimiento. Admite transacciones ACID, series temporales, búsqueda de texto completo y mucho más. Se pueden encontrar más detalles en el sitio web del proyecto.

Para comunicarse con RavenDB, se utiliza la biblioteca RavenDB.Client. Se puede agregar en la interfaz o usando el comando:

dotnet add package RavenDB.Client

A continuación, agregue la carpeta DataAccess y la clase ToDoItem; este será nuestro modelo:

public class ToDoItem
{
    public string Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public DateTime Deadline { get; set; }
}

Allí también agregaremos la clase ToDoRepository, que se comunicará con la base de datos mediante una abstracción especial IDocumentStore. El repositorio es responsable de las operaciones CRUD: crear, leer, actualizar y eliminar. La abstracción IDocumentStore le permite crear objetos de sesión con los que puede interactuar con los datos.

Registraremos nuestro repositorio en el archivo Program.cs y también inicializaremos IDocumentStore con una conexión a nuestra base de datos. En general, interactuar con RavenDB es muy similar a trabajar con DBContext en Entity Framework.

Crear método:

    public async Task Create(ToDoItem item)
    {
        using var session = store.OpenAsyncSession();
        await session.StoreAsync(item);
        await session.SaveChangesAsync();
    }

Método GetById:

    public async Task<ToDoItem> GetById(string id)
    {
        using var session = store.OpenAsyncSession();
        return await session.LoadAsync<ToDoItem>(id);
    }

Hay dos formas de actualizar una entidad:

  • Obtenga una entidad por identificador, actualice el conjunto de campos de la entidad y llame a SaveChanges.

  • Llame al método Patch, en el que debe especificar la entidad y los enlaces a los campos que contiene.

    public async Task Update(ToDoItem item)
    {
        using var session = store.OpenAsyncSession();
        session.Advanced.Patch(item, x => x.Deadline, item.Deadline);
        session.Advanced.Patch(item, x => x.Title, item.Title);
        session.Advanced.Patch(item, x => x.Description, item.Description);
        await session.SaveChangesAsync();
    }

También necesitamos registrar nuestro repositorio en el contenedor DI, en el archivo Program.cs:

var store = new DocumentStore
{
    Urls = new() { " },
    Database = "Todos"
};
store.Initialize();
builder.Services.AddSingleton<IDocumentStore>(store);

Servidor: caché

Usamos Garnet como caché. Este es un almacén de caché remoto de Microsoft Research. Esta solución está básicamente escrita en C#. Este caché es compatible con el protocolo. RESP.para que podamos usar la biblioteca StackExchange.Redis como cliente.

Instalemos la biblioteca:

dotnet add package StackExchange.Redis

Agreguemos la clase CacheService e implementemos el primer método GetOrAdd:

public async Task<T> GetOrAdd<T>(string key, Func<Task<T>> itemFactory, int expirationInSecond)
    {
        // если такой элемент уже есть, возвращаем его
        var existingItem = await _database.StringGetAsync(key);
        if (existingItem.HasValue)
        {
            return JsonSerializer.Deserialize<T>(existingItem);
        }
 
        // забираем новый элемент
        var newItem = await itemFactory();
 
        // добавляем элемент в кеш

        await _database.StringSetAsync(key, JsonSerializer.Serialize(newItem), TimeSpan.FromSeconds(expirationInSecond));
        return newItem;
    

Invalidar método para borrar el caché:

    public async Task Invalidate(string key)
    {
        await _database.KeyDeleteAsync(key);
    }

Backend: poniéndolo todo junto

Ahora agreguemos una nueva clase ToDoService, que combina la lógica del repositorio y el caché. Cuando recibamos datos los agregaremos al caché, y al actualizarlos los invalidaremos.

public class ToDoService(ToDoRepository repository, CacheService cacheService)
{
    public async Task<IEnumerable<ToDoItem>> GetAllAsync()
    {
        return await cacheService.GetOrAdd($"ToDoItem:all", 
            async () => await repository.GetAll(), 30);
    }
 
    public async Task<ToDoItem> GetByIdAsync(string id)
    {
        return await cacheService.GetOrAdd($"ToDoItem:{id}", 
            async () => await repository.GetById(id), 30);
    }
 
    public async Task CreateAsync(ToDoItem item)
    {
        await repository.Create(item);
        await cacheService.Invalidate($"ToDoItem:all");
    }
 
    public async Task UpdateAsync(ToDoItem item)
    {
        await repository.Update(item);
        await cacheService.Invalidate($"ToDoItem:{item.Id}");
        await cacheService.Invalidate($"ToDoItem:all");
    }
 
    public async Task DeleteAsync(string id)
    {
        await repository.Delete(id);
        await cacheService.Invalidate($"ToDoItem:{id}");
        await cacheService.Invalidate($"ToDoItem:all");
    }
}

Registremos todo lo que necesitamos en Program.cs:

var store = new DocumentStore
{
    Urls = new() { " },
    Database = "Todos"
};
store.Initialize();
builder.Services.AddSingleton<IDocumentStore>(store);
builder.Services.AddScoped<ToDoRepository>();
builder.Services.AddScoped<ToDoService>();
builder.Services.AddScoped<CacheService>();
builder.Services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect("localhost"));

Y agregue puntos finales utilizando el enfoque de API mínima en el mismo Program.cs:

app.MapGet("api/todo", async ((FromServices) ToDoService toDoService) 
    => await toDoService.GetAllAsync());
    
app.MapPost("api/todo", async ((FromBody) ToDoItem item, (FromServices) ToDoService toDoService) 
    => await toDoService.CreateAsync(item));
 
app.MapPut("api/todo", async ((FromBody) ToDoItem item, (FromServices) ToDoService toDoService) 
    => await toDoService.UpdateAsync(item));
 
app.MapGet("api/todo/{id}", async (string id, (FromServices) ToDoService toDoService) 
    => await toDoService.GetByIdAsync(id));
 
app.MapDelete("api/todo/{id}", async (string id, (FromServices) ToDoService toDoService)
    => await toDoService.DeleteAsync(id));

Infraestructura

Para probar nuestra aplicación, necesitamos generar RavenDB y Garnet. Esto se puede hacer usando Docker Compose.

Agreguemos la carpeta Launcher y el archivo docker-compose.yml al proyecto:

version: '3.8'
 
services:
  ravendb:
    image: ravendb/ravendb:latest
    environment:
      RAVEN_DB_URL: "
      RAVEN_DB_PUBLIC_URL: "
      RAVEN_DB_TCP_URL: "tcp://0.0.0.0:38888"
    ports:
      - "8080:8080"
 
  garnet:
    image: 'ghcr.io/microsoft/garnet'
    ulimits:
      memlock: -1
    ports:
      - "6379:6379"
    volumes:
      - garnetdata:/data
 
volumes:
  ravendb_data:
  garnetdata:

Ejecutemos el comando docker compose up -d. RavenDB ahora está disponible en localhost:8080 y Garnet está disponible en localhost:6379.

Debe ir a la dirección de RavenDB y realizar la configuración inicial, y luego ir a la sección Bases de datos y crear una base de datos de Todos:

Ahora podemos ejecutar nuestra API y comprobar su funcionalidad. Iniciemos la aplicación, abramos Swagger y hagamos una solicitud POST para crear una tarea:

La solicitud se completó con éxito. Podemos ir a la base de datos y ver la tarea:

La forma más sencilla de comprobar el caché es en el depurador: la primera vez que ejecutamos una solicitud GET debemos ir a la base de datos, y la segunda vez vamos al caché:

Interfaz

Ahora escribamos una interfaz de usuario para nuestro rastreador de tareas. Usaremos el framework Blazor para que la aplicación esté escrita completamente en .NET 🙂

Agreguemos el proyecto Blazor a nuestra solución:

Por analogía con el backend, agregaremos las clases ToDoItem para describir el objeto de tarea y ToDoService para interactuar con el Backend.
Tarea pendiente:

public class ToDoItem
{
    public string Id { get; set; }
    (Required)
    public string Title { get; set; }
    (Required)
    public string Description { get; set; }
    public DateTime Deadline { get; set; }
}

Servicio de tareas pendientes:

public class ToDoService(HttpClient httpClient)
{
    public async Task<List<ToDoItem>> GetToDoItemsAsync() 
        => await httpClient.GetFromJsonAsync<List<ToDoItem>>("todo");
    
    public async Task<ToDoItem> GetToDoItemByIdAsync(string id)
        => await httpClient.GetFromJsonAsync<ToDoItem>($"todo/{id}");
 
    public async Task CreateToDoItemAsync(ToDoItem item)
        => await httpClient.PostAsJsonAsync("todo", item);
 
    public async Task UpdateToDoItemAsync(ToDoItem item)
        => await httpClient.PutAsJsonAsync($"todo/{item.Id}", item);
 
    public async Task DeleteToDoItemAsync(string id)
        =>  await httpClient.DeleteAsync($"todo/{id}");
}

En el archivo Program.cs registramos el servicio y HttpCilent:

builder.Services.AddScoped(_ => 
new HttpClient { BaseAddress = new Uri(" });

builder.Services.AddScoped<ToDoService>();

Ahora visual. Toda la lógica estará contenida en archivos. CrearItem.razor, EditItem.razor Y Lista de tareas pendientes.razor. El código completo se puede ver en GitHub, pero aquí nos centraremos en los puntos clave.

Para reenviar varios servicios en una página, puede utilizar el asistente @inject:

@inject ToDoService ToDoService
@inject NavigationManager NavigationManager

Para formularios, se utiliza la etiqueta EditForm:

<EditForm EditContext="@editContext" Model="newItem" FormName="Create New Task" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div>
        <label for="title">Title: </label>
        <InputText id="title" class="form-control" @bind-Value="newItem.Title" />
    </div>
    <div>
        <label for="description">Description: </label>
        <InputText id="description" class="form-control" @bind-Value="newItem.Description" />
    </div>
    <div>
        <label for="deadline">Deadline: </label>
        <InputDate  id="deadline" class="form-control" @bind-Value="newItem.Deadline" />
    </div>
 
    <button type="submit" class="btn btn-primary">Save</button>
</EditForm>

Si está utilizando .NET8, debe especificar explícitamente el parámetro rendermode en los archivos de su página para que los métodos funcionen correctamente:

@rendermode InteractiveServer

Lanzamos las aplicaciones: primero docker-compose para la infraestructura, luego nuestra API y frontend:

Conclusión

En este artículo, intentamos utilizar todo lo escrito en la plataforma .NET para crear una aplicación web, desde el marco de front-end hasta la base de datos. Por supuesto, estas no son las únicas aplicaciones escritas en C#. También está YARP, que es muy conveniente para usar como proxy para microservicios, o LiteDB, una base de datos en memoria conveniente para realizar pruebas.

código completo en GitHub


Otros artículos relacionados

Modelado de negocios: cómo y por qué usarlo en el desarrollo
Veamos cómo los diagramas de procesos ayudan a las empresas y cuándo es necesario optimizar los procesos.

Métricas de empresas de TI: qué números necesita saber para lograr el éxito empresarial
Doce métricas que pueden ayudarte a evaluar el desempeño empresarial

Cómo construí la gestión del conocimiento corporativo para un producto de TI
Compartimos nuestra experiencia en la organización y construcción de una base de conocimiento corporativa para el desarrollo de productos de TI.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *