Niels Swimburger.NET 🍔
Posted on January 25, 2021
Applications are often split up into a client and a server. The client renders the UI and communicates to the server through an API. But when the client and server are split up into separate projects and hosted at different origins ( origin == scheme + host + port ), it can be a hassle for the client to communicate with the server.
On the web specifically, communicating between different origins can be painful. By default, clients are not allowed to make ajax requests to a different origin. The server has to use Cross Origin Resource Sharing (CORS) headers to allow cross origin ajax requests.
Another way to resolve CORS challenges, is to circumvent it entirely by having the client and server hosted on the same origin. If your technology stack allows for it, you can merge the client and server into a single project and host them on a single origin. If that's not possible, you can use a reverse proxy to consolidate the client origin and the server origin under the proxy's origin.
The diagram above visualizes how the HTTP requests from a web browser are sent to the reverse proxy. If the requested path starts with '/API/', the proxy forwards the request to the API server. Otherwise the request is sent to the Client Web Server which serves the frontend of your application.
You can pick any option for your frontend client, backend server, and reverse proxy server. It does not matter because they all understand the same language: HTTP. In this walkthrough, you'll learn how to communicate between a Blazor WebAssembly (WASM) client and a ASP.NET Web API without CORS. The client and server will be hosted on a single origin by putting Microsoft's new reverse proxy: YARP (which stands for "YARP: A Reverse Proxy")
But why YARP instead of Apache or Nginx? YARP is built on top of ASP.NET Core which gives you some benefits:
- You can configure YARP with any supported external configuration like JSON, environment variables, and many more.
- You can also use .NET code to configure and extend your reverse proxy.
- You can store this piece of infrastructure in source control like any other ASP.NET Core project.
- You can increase consistency between your local environment and server environments.
- It's cross platform, you can develop locally on your OS of choice but deploy it to your server OS of choice. For example, you can develop on Windows but use Linux machines for production.
Prerequisites
To follow along you need the following tech:
- .NET Core 3.1 or up (installation instructions)
- Windows PowerShell or PowerShell Core (installation instructions)
- OS which supports the above tech such as Windows, Mac, or Linux
You can find all the source code for the end result on GitHub.
Create the Blazor WASM client
Before creating the client, create a folder where all projects will reside using the following commands:
mkdir YarpClientServerSingleOrigin
cd YarpClientServerSingleOrigin
Create a solution file using the dotnet command line interface (CLI):
dotnet new sln
Create the Blazor WASM client:
dotnet new blazorwasm -n Client -o Client
-n : specifies the name of the project
-o : specifies the location where the project will be created
Using the command above, you created a Blazor WASM project named 'Client' which is also used as the default namespace. This project is created in a subfolder also named 'Client'.
Add the client project to the solution file:
dotnet sln add Client
When you take a look at the FetchData component at Client\Pages\FetchData.razor, you can see the client is requesting data about the weather from a static JSON file:
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
Update this line of code to fetch the data from api/WeatherForecast
instead:
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("api/WeatherForecast");
This API endpoint does not exist yet, but you will create this soon.
Create the ASP.NET Core Web API server
Create the Web API project using the dotnet CLI:
dotnet new webapi -n Server -o Server
Using the command above, you created a Web API project named 'Server' which is also used as the default namespace. This project is created in a subfolder also named 'Server'.
Add the server project to the solution file:
dotnet sln add Server
When you try to run the client and server at the same time, an error will be shown because they use the same port by default. The ports used by these projects are specified at 'Properties\launchSettings.json'. To prevent port collision, replace ' https://localhost:5001;http://localhost:5000' with ' https://localhost:5003;http://localhost:5002' at 'Server\Properties\launchSettings.json'. Or run these PowerShell commands:
$launchSettingsContents = Get-Content -path ./Server/Properties/launchSettings.json -Raw
$launchSettingsContents = $launchSettingsContents -replace 'https://localhost:5001;http://localhost:5000','https://localhost:5003;http://localhost:5002'
Set-Content -Value $launchSettingsContents -Path ./Server/Properties/launchSettings.json
There is a single controller that comes out of the box with the WebAPI template: WeatherForecastController.cs
This controller generates random weather forecasts and luckily for you matches the data model used in the Blazor WASM client. The WeatherForecast API can be requested at '/WeatherForecast', but the client will request it at '/api/WeatherForecast'. The reverse proxy you will create next will suffix the API with '/api'.
Create the YARP proxy
Unlike some other reverse proxies, you cannot simply install it on your server and configure it. Instead, you have to create an empty ASP.NET Core Web project, add the YARP NuGet package, and configure YARP using configuration or code. Then you need to build, publish, and install it onto your server like any other ASP.NET Core application. This walkthrough will go through building and running this locally for development only.
Create an empty ASP.NET Core Web project using the dotnet CLI:
dotnet new web -n Proxy -o Proxy
Using the command above, you created an empty web project named 'Proxy' which is also used as the default namespace. This project is created in a subfolder also named 'Proxy'. In this case, Proxy is short for Reverse Proxy and not Forward Proxy.
Add the proxy project to the solution file:
dotnet sln add Proxy
To prevent port collision, you need to update the ports specified in 'launchSettings.json' just like the Web API project. Replace ' https://localhost:5001;http://localhost:5000' with ' https://localhost:5005;http://localhost:5004' at 'Proxy\Properties\launchSettings.json'. Or run these PowerShell commands:
$launchSettingsContents = Get-Content -path ./Proxy/Properties/launchSettings.json -Raw $launchSettingsContents = $launchSettingsContents -replace 'https://localhost:5001;http://localhost:5000','https://localhost:5005;http://localhost:5004' Set-Content -Value $launchSettingsContents -Path ./Proxy/Properties/launchSettings.json
Add the prelease NuGet package of YARP to the proxy project:
dotnet add Proxy package Microsoft.ReverseProxy --prerelease
The package 'Microsoft.ReverseProxy' is at version '1.0.0-preview.8.21065.1' at the time of writing this. If any of the upcoming functionality does not work as expected with newer versions of YARP, make sure to check out YARP's documentation.
Update the 'Startup.cs' file to enable YARP's functionality:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Proxy
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddReverseProxy()
.LoadFromConfig(Configuration.GetSection("ReverseProxy"));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapReverseProxy();
});
}
}
}
You can use any external configuration like 'appsettings.json' or environment variables to configure YARP. You can also configure the reverse proxy dynamically using code.
Update the 'Proxy\appsettings.Development.json' file:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ReverseProxy": {
"Clusters": {
"Client": {
"Destinations": {
"Client1": {
"Address": "https://localhost:5001"
}
}
},
"Server": {
"Destinations": {
"Server1": {
"Address": "https://localhost:5005"
}
}
}
},
"Routes": [
{
"RouteId": "ServerRoute",
"ClusterId": "Server",
"Match": {
"Path": "/api/{\*\*catch-all}"
},
"Transforms": [
{
"PathRemovePrefix": "/api"
}
]
},
{
"RouteId": "ServerSwaggerRoute",
"ClusterId": "Server",
"Match": {
"Path": "/swagger/{\*\*catch-all}"
}
},
{
"RouteId": "ClientRoute",
"ClusterId": "Client",
"Match": {
"Path": "{\*\*catch-all}"
}
}
]
}
}
The above JSON configures YARP as follows:
- 2 clusters are defined
- A cluster named " Client"
- Client cluster only has one destination named " Client1" which points to the local development URL for the Blazor WASM client.
A cluster named " Server"
Server cluster only has one destination named " Server1" which points to the local development URL for the Web API server.
3 routes are defined
- A route named " ServerRoute" which forwards HTTP requests to the "Server" cluster any time the requested path starts with '/api/'. Before the HTTP request is forwarded to the Web API, the '/api' prefix is removed by the "PathRemovePrefix" transform.
- A route named " ServerSwaggerRoute" which fixes the Swagger UI for the Web API. The Swagger UI isn't fully aware it is now hosted under the '/api' suffix and this extra route fixes some of those HTTP requests made to the wrong path.
- A route name " ClientRoute" which forwards HTTP requests to the "Client" cluster for all remaining requests.
In this case, there is only one destination per cluster, but YARP can handle multiple destinations and the reverse proxy will load balance the HTTP requests.
Testing the result
You need to open three different terminals. One for every project. Run the following commands:
- Terminal 1:
dotnet run -p Client
- Terminal 2:
dotnet run -p Server
- Terminal 3:
dotnet run -p Proxy
Open the browser and navigate to the Proxy which should be accessible at 'https://localhost:5005'.
The Blazor client should be showing up. Click on the "Fetch Data" navigation link.
The weather forecast data should populate just as you expect it from an out of the box Blazor WASM project. But every time you reload, the data should be different because it is being generated by the Web API project.
You can write a script to bring up and down all three projects instead of manually running them in three different terminals. OR you could use Microsoft's experimental project Tye to tie everything together.
Install Tye as a .NET global tool:
dotnet tool install -g Microsoft.Tye --version "0.5.0-alpha.20555.1"
You can find the most recent version of Tye at Nuget.
Initialize tye using this command:
tye init
This will create a configuration YAML file 'tye.yaml'. Update the YAML file to match the content below:
name: yarpclientserversingleorigin
services:
- name: client
project: Client/Client.csproj
bindings:
- port: 5001
protocol: https
- name: server
project: Server/Server.csproj
bindings:
- port: 5003
protocol: https
- name: proxy
project: Proxy/Proxy.csproj
Tye will automatically use random ports for projects if you do not specify them. The Proxy project expects the client and server project to be run on port 5001 and 5003. Hence you need to explicitly configure which port and protocol these projects use in the YAML file.
Run this command to bring up all projects at once with tye:
tye run
You can find the proxy URL in the output, or you can navigate to tye's dashboard at localhost:8000
In addition to finding the URL for each service, you can also watch the log streams of each service.
Summary
API's need to provide CORS headers to explicitly allow ajax requests from web clients hosted on a different origin. Reverse proxies can merge the API origin and client origin onto a single origin. Using Microsoft's new reverse proxy "YARP", you configured the proxy to forward requests to '/api' to the Web API, and all other requests to the Blazor WASM client.
Using project Tye, you tied all projects together so you can easily run all projects at once instead of starting them in 3 different terminals.
You can learn more about YARP at YARP's Documentation and you can learn more about project Tye on its GitHub repository.
Posted on January 25, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 28, 2022
February 15, 2021