Share backend contract with Blazor client

February 24th 2023 Blazor

In general, you can create client-side types for invoking HTTP web services from Blazor using the OpenAPI code generator just as you would for any SPA framework or other client. However, if the web service is also developed in .NET and you have control over it, you can share the types and libraries through a common class library instead.

If you create a new project from the Blazor WebAssembly App template and enable the ASP.NET Core Hosted option, you will already see an example of this. There is a Shared class library in the generated solution and a WeatherForecast class inside it:

public class WeatherForecast
{
    public DateOnly Date { get; set; }

    public int TemperatureC { get; set; }

    public string? Summary { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

which is used both in the Web API Server project in the WeatherForecastController:

private static readonly string[] Summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild",
    "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    })
    .ToArray();
}

and in the Blazor WebAssembly Client project on the FetchData page:

private WeatherForecast[]? forecasts;

protected override async Task OnInitializedAsync()
{
    forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}

Sharing model classes in this way is a good start. However, in my opinion, you can go a step further and also share the endpoint contracts via a common service interface for the server and the client:

public interface IWeatherForecastService
{
    Task<IEnumerable<WeatherForecast>> Get();
}

This requires you to move the business logic to retrieve the data (or in this case to generate the data) from the controller to a service class, but this is definitely a good idea as it allows you to separate the concerns and makes unit testing easier:

public class WeatherForecastService : IWeatherForecastService
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    public Task<IEnumerable<WeatherForecast>> Get()
    {
        return Task.FromResult<IEnumerable<WeatherForecast>>(
            Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray());
    }
}

In the controller, you then only need to call the injected service's method (and possibly validate the input and build the response, if necessary):

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly IWeatherForecastService weatherForecastService;

    public WeatherForecastController(IWeatherForecastService weatherForecastService)
    {
        this.weatherForecastService = weatherForecastService;
    }

    [HttpGet]
    public async Task<IEnumerable<WeatherForecast>> Get()
    {
        return await this.weatherForecastService.Get();
    }
}

Similarly, in the Blazor application, you need to move the fetching code from the page component to the service:

public class WeatherForecastService : IWeatherForecastService
{
    private readonly HttpClient http;

    public WeatherForecastService(HttpClient http)
    {
        this.http = http;
    }

    public async Task<IEnumerable<WeatherForecast>> Get()
    {
        return await http.GetFromJsonAsync<IEnumerable<WeatherForecast>>(
            "WeatherForecast"
        ) ?? Array.Empty<WeatherForecast>();
    }
}

And then call only the method of the injected service in the page:

private IEnumerable<WeatherForecast> forecasts = Array.Empty<WeatherForecast>();

protected override async Task OnInitializedAsync()
{
    forecasts = await WeatherForecastService.Get();
}

Again, this is generally a good idea because it allows you to move all the details about the URL and response type into the service so that your code in the page component can be fully strongly typed.

Also, do not forget to register the service with dependency injection on both the client and the server, otherwise your controller or page component will not be able to be instantiated:

builder.Services.AddScoped<IWeatherForecastService, WeatherForecastService>();

You can find the full source code for this example in my GitHub repository. You can also compare the code between the last commit and the previous one to see the changes when a common interface was introduced.

This post describes how you can take advantage of using the same technology for the Blazor client and the backend. You can share a strongly typed contract between the two without the intermediate OpenAPI definition. This approach loses some information compared to OpenAPI code generation (especially the URLs of each endpoint), but in return you get the ability to share any business logic in the model classes.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License