Generador incremental que simplifica la compatibilidad de Blazor Server con el modo Blazor Auto

Blazor, dentro del ecosistema .NET, ofrece varios modos de desarrollo. Inicialmente se contaba con Blazor Server y Blazor WebAssembly, cada uno con sus pros y contras evidentes. Con la llegada de .NET 8, surgió el modo Blazor Auto, que combina ventajas de ambos. Sin embargo, para quienes están habituados a la escritura en Server, migrar un proyecto a Auto resulta tedioso y repetitivo. ¿Existe una forma de seguir escribiendo como en Server y que la adaptación a Auto sea transparente? Aunque es una simplificación, en muchos casos es viable, siempre considerando qué tan factible es en WebAssembly.

La solución: AutoWasmApiGenerator

AutoWasmApiGenerator es un generador incremental que construye Web APIs para proyectos Blazor Server, permitiendo su uso en modo Blazor Auto.

Por ejemplo, dado un componente Razor en un proyecto Server:

// UserProfile.razor
@code {
    [Inject, NotNull] IUserService? Service { get; set; }
}

Si la implementación de IUserService no puede ejecutarse en WebAssembly (por ejemplo, por acceso a base de datos o archivos del servidor), se necesita que el servidor exponga una Web API y que el cliente implemente las llamadas a esa interfaz. Este generador se encarga de crear automáticamente tanto el controlador del servidor como el invocador del cliente.

Uso

Estructura del proyecto

  • Server: proyecto del lado servidor
  • Client: proyecto del lado cliente
  • Shared: proyecto compartido (páginas, modelos, etc.)

Pasos

  1. Agregar el paquete AutoWasmApiGenerator al proyecto Server: ``` <PackageReference Include="AutoWasmApiGenerator" Version="0.0.*" />
  2. En el proyecto Shared o Client, definir la interfaz: ``` [WebController] public interface IAuthService { [WebMethod(Method = WebMethod.Post)] Task LoginAsync(string user, string password, CancellationToken token); }
  3. En cualquier archivo del proyecto Server, marcar el ensamblado para generar el controlador: ``` [assembly: AutoWasmApiGenerator.WebControllerAssembly]
    
    Se generará `AuthServiceController`.
    
  4. En el proyecto Client (o donde se requiera el invocador), marcar el ensamblado: ``` [assembly: AutoWasmApiGenerator.ApiInvokerAssembly]
    
    Se generará `AuthServiceApiInvoker`.
    
  5. Registrar en el proyecto Client: ``` builder.Services.AddScoped<IAuthService, AuthServiceApiInvoker>();
  6. Usar la interfaz inyectada: ``` @code { [Inject] public IAuthService AuthService { get; set; } private async Task LoginAsync() { await AuthService.LoginAsync("user", "pass", CancellationToken.None); } }
    
    

Atributos clave

WebControllerAttribute

Marca una interfaz de servicio para generar el controlador y la clase invocadora.

WebMethodNotSupportedAttribute

Indica que un método no debe generar su correspondiente Web API.

ApiInvokeNotSupportedAttribute

Marca una clase o método para que no se genere el invocador (el método lanzará NotSupportedException).

WebMethodAttribute

Especifica el método HTTP para una acción. Por defecto es Post.

[WebMethod(Method = WebMethod.Get)]
Task<bool> CheckStatusAsync(string id);

Propiedades

Nombre Tipo Descripción
Method WebMethod Método HTTP (Post, Get, Put, Delete)
Route string? Ruta personalizada para la acción; si es null, se usa el nombre del método
AllowAnonymous bool Anula la configuración de autorización
Authorize bool Requiere autenticación

WebMethodParameterBindingAttribute

Define cómo se vincula un parámetro (FromQuery, FromBody, FromHeader, FromRoute, FromForm, Ignore, FromServices).

[WebMethod(Method = WebMethod.Post)]
Task<bool> LogAsync(
    [WebMethodParameterBinding(BindingType.FromBody)] string message,
    [WebMethodParameterBinding(BindingType.FromQuery)] string path,
    [WebMethodParameterBinding(BindingType.Ignore)] CancellationToken token);

Inyección de servicios para interceptación

IGeneratedApiInvokeDelegatingHandler

Permite agregar lógica antes/después de cada invocación y manejar excepciones.

  • Task BeforeSendAsync(SendContext context)
  • Task AfterSendAsync(SendContext context)
  • Task OnExceptionAsync(ExceptionContext context)

Ejemplo de implementación:

[AutoInject(Group = "WASM", ServiceType = typeof(IGeneratedApiInvokeDelegatingHandler))]
public class CustomHandler(ILogger<CustomHandler> logger, IUIService ui) : GeneratedApiInvokeDelegatingHandler
{
    public override Task BeforeSendAsync(SendContext context)
    {
        logger.LogDebug("Antes de enviar {Método}", context.TargetMethod);
        return base.BeforeSendAsync(context);
    }
    public override Task OnExceptionAsync(ExceptionContext context)
    {
        logger.LogDebug("Excepción: {Msg}", context.Exception.Message);
        if (context.SendContext.Response?.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            context.Handled = true;
            ui.Error("Sesión expirada, inicie sesión nuevamente");
        }
        return Task.CompletedTask;
    }
}

SendContext

Nombre Tipo Descripción
TargetType Type Interfaz del servicio
TargetMethod string Nombre del método
Parameters object?[]? Parámetros de la llamada
ReturnType Type? Tipo de retorno
Request HttpRequestMessage Mensaje de solicitud
Response HttpResponseMessage? Respuesta recibida

ExceptionContext

Nombre Tipo Descripción
SendContext SendContext Contexto de la invocación
Exception Expection Excepción original
Handled bool Indica si la excepción fue manejada; si es false, se relanza

Manejo de valores de retorno en caso de error

Se puede configurar un productor de resultados de error usando AddAutoWasmErrorResultHandler:

builder.Services.AddAutoWasmErrorResultHandler(config =>
{
    config.CreateErrorResult<MyResult>(context =>
        new MyResult() { Success = false, Message = context.Exception.Message });
});

El generador intenta construir automáticamente un objeto de resultado si el tipo tiene una propiedad booleana llamada Success (o IsSuccess) y una propiedad de cadena Message (o Msg). Se puede personalizar con los parámetros SuccessFlag y MessageFlag en ApiInvokerAssemblyAttribute.

Ejemplo de código generado

Interfaz original

[WebController(Route = "api/auth")]
public interface IAuthService
{
    Task<string> LoginAsync(string user, string pass);
    [WebMethod(Method = WebMethod.Get)]
    Task<UserInfo> GetProfileAsync(int userId);
    void Logout();
}

Controlador generado (AuthServiceController)

// <auto-generated/>
[ApiController]
[Route("api/auth")]
public class AuthServiceController : ControllerBase
{
    private readonly IAuthService _service;

    public AuthServiceController(IAuthService service) => _service = service;

    [HttpPost("Login")]
    public Task<string> Login([FromQuery] string user, [FromQuery] string pass)
        => _service.LoginAsync(user, pass);

    [HttpGet("GetProfile")]
    public Task<UserInfo> GetProfile([FromQuery] int userId)
        => _service.GetProfileAsync(userId);

    [HttpPost("Logout")]
    public void Logout() => _service.Logout();
}

Invocador generado (AuthServiceApiInvoker)

// <auto-generated/>
public partial class AuthServiceApiInvoker : IAuthService
{
    private readonly IHttpClientFactory _factory;
    private readonly IGeneratedApiInvokeDelegatingHandler _handler;

    public AuthServiceApiInvoker(IHttpClientFactory factory, IServiceProvider services)
    {
        _factory = factory;
        _handler = services.GetService<IGeneratedApiInvokeDelegatingHandler>()
                   ?? GeneratedApiInvokeDelegatingHandler.Default;
    }

    public async Task<string> LoginAsync(string user, string pass)
    {
        var client = _factory.CreateClient("AuthService");
        var request = new HttpRequestMessage(HttpMethod.Post, "api/auth/Login?user=" + user + "&pass=" + pass);
        var ctx = new SendContext(typeof(IAuthService), nameof(LoginAsync), request);
        ctx.Parameters = new object[] { user, pass };
        ctx.ReturnType = typeof(string);
        try
        {
            await _handler.BeforeSendAsync(ctx);
            var response = await client.SendAsync(request);
            ctx.Response = response;
            response.EnsureSuccessStatusCode();
            await _handler.AfterSendAsync(ctx);
            return await response.Content.ReadAsStringAsync();
        }
        catch (Exception ex)
        {
            var exCtx = new ExceptionContext(ctx, ex);
            await _handler.OnExceptionAsync(exCtx);
            if (!exCtx.Handled) throw;
            // Intenta devolver un valor por defecto
            return default;
        }
    }

    // ... otros métodos
}

El código fuente completo del generador se encuentra disponible en el repositorio.

Etiquetas: Blazor Blazor Server Blazor WebAssembly Blazor Auto generación de código incremental

Publicado el 7-3 18:00