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>

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.

Direct HTTP requests have a little less “magic” to them. You may ask “well why do I want them then”? Same reason as other things that seem obvious in Mediator, the middleware, the caching, the decorating, without the code layers to get there.

Direct HTTP request have 1 object and only return an object that is deserialized to whatever is configured in ResultType.

AppSettings.json

{
"Mediator": {
"Http": {
"Direct": {
"BaseUrl": "https://myapi.com",
"MyRoute":{
"Url": "https://myapi.com/route/1234",
"Method": "POST" // GET is default
}
}
}
}
}
var request = new HttpDirectRequest
{
ConfigNameOrRoute = "RouteName",
ResultType = typeof(MyResponse), // leave null if there is no response
};
IMediator mediator = ... // get your mediator instance
var response = await mediator.Request(request);
// NOTE: unlike other strong typed requests, the response is always an object - you have to cast it to the expected type
var result = (MyResponse)response.Result;