Interfacing your API for Blazor Webassembly

Subscribe to my newsletter and never miss my upcoming articles

Introduction

Blazor Web Assembly allows you to share many things between your front- and backend. We can share logic, models etc. This reduces the amount of mismatches between the front- and backend a lot. The problem with this is that there is still room for errors when you change the API. To totally solve this problem I created an interface that defines the controller and is used on the frontend to call the API.
Creating an implementation for every interface and every method of the interface seemed like a tedious and error prow task. For this reason, I created the following solution.

Getting Started

We start with a new project. A new project is not necessary but makes the examples cleaner and more readable. If you are using a existing project or want to start on another project type, make sure you have the following projects in your solution:

  • Server An ASP .NET Core web project containing the API.
  • Client A Blazor WASM project but other .Net Core project would work if you want to use the api from that project.
  • Shared An ASP .NET Core shared project that is used by the Server and Client project.

Setting up Shared

After we get the project setup we want to prepare the shared project. We use the shared project to share models and interfaces between the Client and Server. The first thing we want to do here is install a package to make all the Http attributes usable in this project. Http attributes are used to define the endpoint. We need to know the Http attributes as we want to create a http request based on this endpoint. You can install the package with the following command:

Install-Package Microsoft.AspNetCore.Mvc.DataAnnotations -Version 2.2.0

Dto

We also need a model that is shared between the Client and Server. You can reuse, move or create a new model for this. If you have multiple models in your controller, make sure that they are all defined in the shared project.
For this use case we are going to keep it simple and define a dog class.

public class DogDto
{
  public string Name { get; set; }
  public int Age { get; set; }
}

Endpoint interface

The last step on the shared project is creating an interface for the endpoint. The following points are important to notice:

  • We prefix the name with an 'I' and post fix the name with 'Endpoint'.
    This is not necessary but does require a few changes in a later step.
  • Add the Http attribute for every method.
  • Add extra routing to the Http attribute.
  • Add the FromBody attribute for parameters that need to be converted to the JSON body.
public interface IDogEndpoint
{
  [HttpGet]
  Task<List<DogDto>> Get();

  [HttpGet("{name}")]
  Task<DogDto> Get(string name);

  [HttpPost]
  Task<DogDto> Create([FromBody] DogDto body);

  [HttpPost("{name}")]
  Task<DogDto> Edit(string name, [FromBody] DogDto body);

  [HttpDelete("{name}")]
  Task Delete(string name);
}

Creating the API on Server.

Now that we have defined the interface for the controller, we can start creating the controller for the server. You can also use an existing controller as long as the interface matches the controller

API Controller

I defined a really simple controller that uses a dictionary as storage. The important thing here is that you implement the endpoint interface and add the http attributes.

[Route("api/[controller]")]
[ApiController]
public class DogController : ControllerBase, IDogEndpoint
{
  public static readonly IDictionary<string, DogDto> Dogs = 
    new Dictionary<string, DogDto>
  {
    {"Joey", new DogDto {Age = 5, Name = "Joey"}},
    {"Barky", new DogDto {Age = 8, Name = "Barky"}},
    {"Cooper", new DogDto {Age = 2, Name = "Cooper"}},
    {"SnoopDog", new DogDto {Age = 500, Name = "SnoopDog"}}
  };

  [HttpGet]
  public Task<List<DogDto>> Get()
  {
    return Task.FromResult(Dogs.Values.ToList());
  }

  [HttpGet("{name}")]
  public Task<DogDto> Get(string name)
  {
    return Task.FromResult(Dogs[name]);
  }

  [HttpPost]
  public Task<DogDto> Create([FromBody] DogDto body)
  {
    Dogs.Add(body.Name, body);
    return Task.FromResult(body);
  }

  [HttpPost("{name}")]
  public Task<DogDto> Edit(string name, [FromBody] DogDto body)
  {
    Dogs[name] = body;
    return Task.FromResult(body);
  }

  [HttpDelete("{name}")]
  public Task Delete(string name)
  {
    Dogs.Remove(name);
    return Task.CompletedTask;
  }
}

Client

We have created a controller and interface to talk to at the endpoint. We can create an implementation for every interface but that seems like a lot of work for a big project. This also creates more work when the API changes and the ability for bugs to occur.

HttpClientService

The first class we create for the Client is HttpClientService. This class just helps with the requests and makes it a bit simpler down the road.

public class HttpClientService
{
  private readonly HttpClient _httpClient;

  public HttpClientService(HttpClient httpClient)
  {
    _httpClient = httpClient;
  }

  public async Task<T> GetRequestAsync<T>(string path)
  {
    var response = await _httpClient.GetAsync(path);
    return await response.Content.ReadFromJsonAsync<T>();
  }

  public async Task<TReturn> PostRequestAsync<TReturn, TModel>(
    string path, TModel obj)
  {
    var response = await _httpClient.PostAsJsonAsync(path, obj);
    return await response.Content.ReadFromJsonAsync<TReturn>();
  }

  public async Task DeleteRequestAsync(string path)
  {
    var response = await _httpClient.DeleteAsync(path);
  }
}

DynamicHttpEndpoint

In this class we start with the real magic. We created a generic class that implement a DynamicObject. We also made a constructor that requires the previously created HttpClientService.
We are using this service to make calls. We also have a Generic T that we use to determine the endpoint name.
If you did not use the same prefix and postfix for your interface, you may want to change this here.

public class DynamicHttpEndpoint<T> : DynamicObject
{
  private readonly IHttpClientService _httpClientService;
  private readonly string _controllerName;

  public DynamicHttpEndpoint(IHttpClientService httpClientService)
  {
    _httpClientService = httpClientService;
    _controllerName = typeof(T).Name.Remove(0, 1)
      .Replace("Endpoint", string.Empty);
  }

  public override bool TryInvokeMember(InvokeMemberBinder binder, 
    object[] args, out object result)
  {
  }
}

TryInvokeMember

The method TryInvokeMember will be called on every call on the interface. In this method we want to get the correct method that is being called, get all the data from that method and create a request based on that data.
In the code below we try to get the method by its name and parameters.

public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
  var method = GetCurrentMethod(binder, args);
}

private static MethodInfo GetCurrentMethod(InvokeMemberBinder binder, object[] args)
{
  // All methods with the name of the caller
  var methods = typeof(T).GetMethods()
    .Where(x => x.Name == binder.Name && 
      args.Length == x.GetParameters().Length);
  // Get the method with the correct parameters 
  return methods.First(methodInfo => args.All(  
    arg => methodInfo.GetParameters().Any(x =>  
      x.ParameterType.IsInstanceOfType(arg))));

}

HttpAttribute

After we get the method information that is being called, we want to retrieve some data about the method. The first thing we do is retrieve the HttpMethodAttribute. The Http attribute contains the type of request, routing and parameters. After we get the attribute we check if it contains extra template routing and replace the parameters in it. The last step is getting the return type of the method.

public override bool TryInvokeMember(InvokeMemberBinder binder, 
  object[] args, out object result)
{
  var method = GetCurrentMethod(binder, args);
  // New
  // Get the attribute above the method to get the route and type of request 
  var attribute = method.GetCustomAttribute(
    typeof(HttpMethodAttribute), true) as HttpMethodAttribute;

  // If template is null no additional routing is added
  // If the template is not null extra routing and parameters are added to the route
  var route = _controllerName + (attribute.Template != null ? "/" + 
    ReplacePlaceHoldersWithVariables(attribute.Template, method, args) : 
    string.Empty);
  // Get the return type without the task.
  var returnType = method.ReturnType.GenericTypeArguments.FirstOrDefault() 
    ?? method.ReturnType;
}

private string ReplacePlaceHoldersWithVariables(
  string route, MethodBase method, IReadOnlyList<object> args)
{
  while ((route.IndexOf('{') != -1))
  {
    var indexOfStart = route.IndexOf('{');
    var indexofEnd = route.IndexOf('}');

    var propertyName = route.Substring(indexOfStart + 1, indexofEnd - 1);
    var param = method.GetParameters()
      .First(x => x.Name == propertyName);
    route = route.Replace(
      $"{{{propertyName}}}", args[param.Position].ToString());
  }
  return route;
}

Query attribute

A big part of the http request is the query parameters. To support query parameters we check if any of the parameters contain the attribute FromQueryAttribute. When there are parameters with this attribute we convert the parameters to a string in the QueryToString method.

public override bool TryInvokeMember(InvokeMemberBinder binder, 
  object[] args, out object result)
{
  var method = GetCurrentMethod(binder, args);
  // Get the attribute above the method to get the route and type of request 
  var attribute = method.GetCustomAttribute(
    typeof(HttpMethodAttribute), true) as HttpMethodAttribute;

  // If the template is null no additional routing is added
  // If template is not null extra routing and parameters are added to the route
  var route = _controllerName + (attribute.Template != null ? "/" + 
    ReplacePlaceHoldersWithVariables(attribute.Template, method, args) : 
    string.Empty);
  // Get the return type without the task.
  var returnType = method.ReturnType.GenericTypeArguments.FirstOrDefault() 
    ?? method.ReturnType;

  // New
  var fromQueryList = method.GetParameters()
    .Where(x => x.GetCustomAttribute<FromQueryAttribute>() != null).ToList();

  if (fromQueryList.Any())
  {
    route += QueryToString(fromQueryList, args);
  }
}

private string QueryToString(
  IEnumerable<ParameterInfo> fromQueryList, object[] args)
{
  return "?" + string.Join("&", fromQueryList.Select(parameterInfo =>
  {
    var param = parameterInfo.ParameterType;
    var value = args[parameterInfo.Position];
    if (IsSimple(param))
    {
        return $"{parameterInfo.Name}={value}";
    }

    return string.Join("&", value.GetType().GetProperties()
      .Select(prop =>
      {
        var name = prop.Name;
        var propValue = prop.GetValue(value);
        return $"{name}={propValue}";
      }));
  }));
}
private bool IsSimple(Type type)
{
  return type.IsPrimitive
        || type.IsEnum
        || type == typeof(string)
        || type == typeof(decimal);
}

HttpRequest

The last step in this method is calling the correct method on the HttpClientService. We create a switch based on the type of the HttpAttribute and call the HttpAttribute with the correct method and assign the result to the result variable.

public override bool TryInvokeMember(InvokeMemberBinder binder, 
  object[] args, out object result)
{
  var method = GetCurrentMethod(binder, args);
  // Get the attribute above the method to get the route and type of request 
  var attribute = method.GetCustomAttribute(
    typeof(HttpMethodAttribute), true) as HttpMethodAttribute;

  // If template is null no additional routing is added
  // If the tempalte is not null extra routing and parameters are added to the route
  var route = _controllerName + (attribute.Template != null ? "/" + 
    ReplacePlaceHoldersWithVariables(attribute.Template, method, args) : 
    string.Empty);
  // Get the return type without the task.
  var returnType = method.ReturnType.GenericTypeArguments.FirstOrDefault() 
    ?? method.ReturnType;

  var fromQueryList = method.GetParameters()
    .Where(x => x.GetCustomAttribute<FromQueryAttribute>() != null).ToList();

  if (fromQueryList.Any())
  {
    route += QueryToString(fromQueryList, args);
  }
  // New
  // Getting the correct request type and executing the request on the 
     httpclientservice
  switch (attribute)
  {
    case HttpGetAttribute _:
      var httpGetMethod = _httpClientService.GetType()
        .GetMethod("GetRequestAsync");

      result = httpGetMethod.MakeGenericMethod(returnType)
        .Invoke(_httpClientService, new object[] { route });

      return true;

    case HttpPostAttribute _:
      var bodyParam = method.GetParameters()
        .First(x => x.GetCustomAttribute<FromBodyAttribute>() != null);

      var body = args[bodyParam.Position];
      var bodyType = body.GetType();

      var httpPostMethod = 
        _httpClientService.GetType().GetMethod("PostRequestAsync");

      result = httpPostMethod.MakeGenericMethod(returnType, bodyType)
        .Invoke(_httpClientService, new object[] { route, body });

      return true;

    case HttpDeleteAttribute _:
          var httpDeleteMethod = 
            _httpClientService.GetType().GetMethod("DeleteRequestAsync");
          result = httpDeleteMethod
            .Invoke(_httpClientService, new object[] {route});
          return true;

    default:
      result = default;
      return false;
  }
}

DynamicHttpEndpoint -> IDogEndpoint

We created a DynamicHttpEndpoint but we cannot just cast it to IDogEndpoint. And implementing every interface also doesn't work. To make this work we use a package called ImpromptuInterface We can install this package though the following command.

Install-Package ImpromptuInterface -Version 7.0.1

This package allows us to let interface behave like another object. We changed the following in Program.cs

  • Change the base URL of the HttpClient to match our backend
  • Registered the HttpClientService in the dependency container.
  • Registered IDogEndpoint
    As a last point, we created a retrieved HttpClientService and created a new DynamicHttpEndpoint with IDogEndpoint. After this we called the method ActLike on it. This method is from the ImpropmtuInterface and allows a dynamic object to behave as the interface.
public static async Task Main(string[] args)
{
  var builder = WebAssemblyHostBuilder.CreateDefault(args);
  builder.RootComponents.Add<App>("app");

  builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress + "api/") });
  builder.Services.AddScoped<IHttpClientService, HttpClientService>();
  builder.Services.AddScoped(typeof(IDogEndpoint), serviceProvider =>
  {
    var httpService = serviceProvider.GetService<IHttpClientService>();
    return new DynamicHttpEndpoint<IDogEndpoint>(httpService).ActLike<IDogEndpoint>();

  });

  await builder.Build().RunAsync();
}

The last step

The last step is letting the IDogEndpoint be injected into our page and calling a method on it. In the code below i created an example of how this works but it could be used for all the methods we created on the interface. If you want to see more examples check out the Github page containing the entire implementation.

@using BlazorAPI.Shared.Interfaces
@using BlazorAPI.Shared.Models
@page "/dogs"
@inject IDogEndpoint dogEndpoint

<ul>
  @foreach (var dog in _dogs)
  {
    <li>
      My name is @dog.Name and i am @dog.Age years old!
    </li>
  }
</ul>
@code
{
  private IList<DogDto> _dogs = new List<DogDto>(); 

  protected override async Task OnInitializedAsync()
  {
    await base.OnInitializedAsync();
    _dogs = await dogEndpoint.Get();
  }
}

Summary

In this post we created a interface for our API. We can use this interface on the Client to have a strongly typed link between the front- and backend.
Thank you for reading my first blog post. If you have any questions or suggestions, make sure to post them in the comments below. If you want this solution but don't want want to create the DynamicHttpEndpoint make sure to check out my package: APInterface

It is greatly appreciated if you shared this blog if you found the article interesting.

Github

No Comments Yet