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
- Agregar el paquete
AutoWasmApiGeneratoral proyecto Server: ``` <PackageReference Include="AutoWasmApiGenerator" Version="0.0.*" /> - 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);
}
- En cualquier archivo del proyecto Server, marcar el ensamblado para generar el controlador: ```
[assembly: AutoWasmApiGenerator.WebControllerAssembly]
Se generará `AuthServiceController`. - En el proyecto Client (o donde se requiera el invocador), marcar el ensamblado: ```
[assembly: AutoWasmApiGenerator.ApiInvokerAssembly]
Se generará `AuthServiceApiInvoker`. - Registrar en el proyecto Client: ```
builder.Services.AddScoped<IAuthService, AuthServiceApiInvoker>();
- 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.