Skip to content

HTTP

HTTP requests are basically a must in every app. We didn’t want people to have to generate their API client, contracts, and then have to global add the boilerplate mediator contract and subsequent request handler, so… we found some very convenient ways to build this in. Imagine having to do something like this for every HTTP call?

public class MyHttpRequest : IRequest<MyResponse>
{
public string SomeArg { get; set; }
}
public class MyHttpRequestHandler<MyHttpRequest, MyResponse>(IConfiguration config) : IRequestHandler<MyHttpRequest, MyResponse>
{
public async Task<MyResponse> Handle(MyHttpRequest request, IMediatorContext context, CancellationToken cancellationToken)
{
var baseUri = config.GetValue<string>("HttpUri");
var httpClient = new HttpClient();
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json);
var message = new HttpRequestMessage(HttpMethod.Post, baseUri + "/somerequest");
// what about headers and auth? OMG... too much
var response = await httpClient.PostAsync(message);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync(cancellationToken);
var result = JsonSerializer.Deserialize<MyResponse>(responseJson);
return result;
}
}
  1. Let’s start with our mediator request contract - Notice that we have some attributes & interfaces that are specific to HTTP requests

    using Shiny.Mediator;
    using Shiny.Mediator.Http;
    [Http(HttpVerb.Post, "/route/{Parameter}")]
    public class MyRequest : IHttpRequest<MyResponse>
    {
    // name of the route parameter
    [HttpParameter(HttpParameterType.Path)]
    public string Parameter { get; set; }
    // query string parameter appended to uri
    [HttpParameter(HttpParameterType.Query)]
    public string QueryValue { get; set; }
    // added to headers
    [HttpParameter(HttpParameterType.Header)]
    public string SomeHeaderValue { get; set; }
    // there can only be one body - serialized to json
    [HttpBody]
    public SomeOtherClass Body { get; set; }
    }
  2. Next, let’s create our configuration for the base URI. This assumes you use Microsoft.Extensions.Configuration JSON, but any other configuration provider with the same structure will work.

    {
    "Mediator": {
    "Http": {
    "Your.Namespace.Contract": "https://yourapi.com",
    // OR
    "Your.Namespace.*": "https://yourapi.com"
    // OR
    "*": "https://yourapi.com"
    }
    }
    }
  3. Now - let’s make the request with mediator

    var response = await mediator.Request(new MyRequest
    {
    Parameter = "someValue",
    QueryValue = "someQuery",
    SomeHeaderValue = "someHeader",
    Body = new SomeOtherClass
    {
    SomeProperty = "someValue"
    }
    });

A common scenario is having to refresh an access token, add it to the next HTTP request, and maybe add some additional header while you’re there. Shiny Mediator allows you to register a different type of middleware of HTTP Requests called IHttpRequestDecorator.

Here’s an example that already exists in Shiny.Mediator.Maui that adds some headers to the request.

Decorators apply to all HTTP requests

public class MauiHttpRequestDecorator(
IConfiguration configuration,
IAppInfo appInfo,
IDeviceInfo deviceInfo,
IGeolocation geolocation
) : IHttpRequestDecorator
{
public async Task Decorate(HttpRequestMessage httpMessage, IMediatorContext context)
{
httpMessage.Headers.Add("AppId", appInfo.PackageName);
httpMessage.Headers.Add("AppVersion", appInfo.Version.ToString());
httpMessage.Headers.Add("DeviceManufacturer", deviceInfo.Manufacturer);
httpMessage.Headers.Add("DeviceModel", deviceInfo.Model);
httpMessage.Headers.Add("DevicePlatform", deviceInfo.Platform.ToString());
httpMessage.Headers.Add("DeviceVersion", deviceInfo.Version.ToString());
httpMessage.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(CultureInfo.CurrentCulture.Name));
if (configuration["Mediator:Http:GpsHeader"] == "true")
{
var gps = await geolocation.GetLastKnownLocationAsync();
if (gps != null)
httpMessage.Headers.Add("GpsCoords", $"{gps.Latitude},{gps.Longitude}");
}
// added to show authentication - does not exist in real MauiHttpRequestDecorator
if (someAuthService.TokenExpiry > DateTimeOffset.UtcNow)
await someAuthService.RefreshToken();
httpMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", someAuthService.AccessToken);
}
}

Let’s reduce the boilerplate and mapping even further. You’ve already done all that work once in your APIs. Some WebAPIs can have a large API surface with many routes, parameters, and types. Generating OpenAPI files is pretty standard these days with things like NSwag, Swashbuckle, & Refitter. Shiny Mediator can help with this as well. You can generate all the contracts and responses from an OpenAPI file or URI to an OpenAPI document.

Simply edit your csproj file that has Shiny.Mediator installed and add the following to it.

If there is no OperationId defined for the OpenAPI method, we cannot generate a contract.

<ItemGroup>
<MediatorHttp Include="OpenApiDoc.json"
Namespace="My.Namespace"
ContractPrefix="optional"
ContractPostfix="HttpRequest"
Visible="false" />
<MediatorHttp Include="OpenApiRemote"
Uri="https://someurl.com"
Namespace="My.RemoteNamespace"
ContractPrefix="optional"
ContractPostfix="HttpRequest"
Visible="false" />
</ItemGroup>

and now in your C#. The .AddGeneratedOpenApiClient() will add all MediatorHttp elements in that library. This method will only exist if openapi handlers have been generated.

builder.Services.AddShinyMediator(x => x..AddGeneratedOpenApiClient())

The HTTP extension has a few configuration options that you can set in your IConfiguration provider. As with many other modules, you can configure by contract, namespace, handler, or globally. The following is a sample configuration that you can use in your appsettings.json or other configuration provider.

{
"Mediator": {
"Http": {
"Your.Namespace.Contract": "https://yourapi.com",
// OR
"Your.Namespace.*": "https://yourapi.com"
// OR
"*": "https://yourapi.com",
"Debug: true",
"Timeout": 30 // in seconds
}
}
}
  • Debug - If true, the HTTP request will log the request and response to the console. This is useful for debugging purposes.
  • Timeout - The default timeout for the HTTP request in seconds. This is useful for long-running requests. Defaults to 20 seconds if not set.

In order to deal with AOT scenarios & have this all generated nicely, we built our JSON source generator into our HTTP extension source generator.
We don’t enable this out of the box for several reasons.

  1. Every API is different and it is difficult to source generate serialization picture perfect
  2. If your API is large, the source generation of all the HTTP contracts plus the JSON converters can take a few seconds.
  3. We have made it so you can mark your own types just as easily with [SourceGenerateJsonConverterAttribute] or implementation your own ISerializerService for Mediator to use.

To source generate the JSON converters for your HTTP contracts, do the following in your ItemGroup

Set GenerateJsonConverters to true

<ItemGroup>
<MediatorHttp Include="OpenApiDoc.json"
Namespace="My.Namespace"
GenerateJsonConverters="true"
Visible="false" />
</ItemGroup>

That’s it. Now all your HTTP contract types will have source generated JSON converters for AOT scenarios.