Simmy Chaos Engine for .NET – Part 2, Resilience and Injected Faults

Full source code here.

I wrote a blog on using a Simmy Fault policy a few days ago, it is very simple to use by itself, but it is far more useful when used in combination with a resilience policy.

Take the scenario where I have added retry logic to my requests, but the requests never fail, how do I know the retry logic works the way I expect. This is where the fault policy shines.

I’m going to build on the previous post where I injected a fault 50 percent of the time when making http requests. In the previous post I had Simmy throw a HttpRequestException but this time I’m going to have Simmy return a HttpResponseMessage containing an InternalServerError.

Along with the fault policy, I am going add a simple retry policy, retrying up to three times.

Here’s how it will work –

The fault policy executes –

If the fault policy is active and a fault is to be returned (remember this happens 50% of the time) a InternalServerError is returned. The HttpClient does NOT make a request to the remote service

The retry policy executes. The handles clause sees the InternalServerError and the behavior clause of the retry kicks in and performs the retry

The fault policy executes again

The preceding two steps repeat until a request succeeds or the retry policy reaches is limit of 3 retries

 

If the fault policy is not active, or a fault is NOT to be returned

the HTTP client executes the request to the remote service

the handles clause of the retry determines whether a retry should be performed

 

This is what it looks like –

At the center is the HttpReqeust, around it is the fault policy and around everything is the retry policy.

To achieve all this in code is very simple, in ConfigureServices add the fault policy.

public void ConfigureServices(IServiceCollection services)
{
	HttpResponseMessage faultHttpResponseMessage = new HttpResponseMessage(HttpStatusCode.InternalServerError)
	{
		Content = new StringContent("Simmy swapped the Ok for an Internal Server Error")
	};
	AsyncInjectOutcomePolicy<HttpResponseMessage> faultPolicy = MonkeyPolicy.InjectFaultAsync(
		faultHttpResponseMessage,
		injectionRate: .5,
		enabled: () => true 
	);

When the fault policy executes, 50% of the time it will return an InternalServerError response, preempting the real request to the remote service.

Next is the retry policy, its behavior clause triggers if the HttpResponseMessage is not a success.

IAsyncPolicy<HttpResponseMessage> retryPolicy = Policy
	.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
	.RetryAsync(3, onRetry: (message, retryCount) =>
	{
		Console.WriteLine($"Retry: {retryCount}");
	});

Wrap the retry policy around the fault policy.

AsyncPolicyWrap<HttpResponseMessage> faultAndRetryWrap = Policy.WrapAsync(retryPolicy, faultPolicy);

Finally, create the HttpClientFactory, passing in the policy wrap. If you are not familiar with how the HttpClientFactory works read Steve Gordon’s series or my blog posts on it. If you want know how it works with Polly, take a look at my posts on that topic.

services.AddHttpClient("OpenBreweryDb", client =>
{
	client.BaseAddress = new Uri("https://api.openbrewerydb.org/breweries/");
	client.DefaultRequestHeaders.Add("Accept", "application/json");
}).AddPolicyHandler(faultAndRetryWrap);

To fire of the HttpClient request inside the BreweryController remains the same –

string requestEndpoint = $"?by_state={state}&by_name={name}";
var httpClient = _httpClientFactory.CreateClient("OpenBreweryDb");
var response = await httpClient.GetAsync(requestEndpoint);

Full source code here.

Simmy Chaos Engine for .NET – Part 1, Injecting Faults

Full source code here.

For quite a while I have been writing, presenting and making videos about using Polly to make applications more stable.

With this post I’m starting a series on how break your applications with a chaos engine, the kind you might have heard about from the likes of NetFlix.

The team behind Polly released a chaos engineering module in June 2019. It allows you to inject faults, latency and arbitrary behavior into your application.

There are two general ways of using these policies –
1. Within your own business logic (if you have access to it).
2. Within the calls to business logic (if you can’t or don’t want to change it).

I’m going to deal with the latter case in this post. I want to see how my code responds when failures occur in third party dependencies, since I have no control over those dependencies I have to introduce the errors myself.

Making a good service go bad
In the provided code I have dependency on remote service and I want make it seem that that service suffers from a high percentage of faults.
This example will make GET requests to an API that looks up breweries, from what I can tell it is a highly reliable API, but I will make look as though 50% of requests to it fail. The point of doing this is to see how my application reacts to these errors – does it do so gracefully, or does it fall on its face.

If you made requests to the API without interfering with them you should see a 100% success rate. But I want to see failures, so I’ll use the Fault Policy to inject exceptions.

First, I added two NuGet packages to my project – Polly.Contrib.Simmy and Microsoft.Extensions.Http.Polly to the project.

In Startup.cs I add the fault policy.

AsyncInjectOutcomePolicy<HttpResponseMessage> faultPolicy = MonkeyPolicy.InjectFaultAsync<HttpResponseMessage>(
	new HttpRequestException("Simmy threw an exception"), 
		injectionRate: .5,
		enabled: () => true
	);

The parameters specify the exception to throw, the percentage of requests that throw the exception, and whether the policy is enabled. You can use set the injection rate and whether the policy is enabled via configuration, but I’m not going to show that here.
In this case, I am throwing an HttpRequestException 50% of the time, and the policy is enabled.

Then create a HttpClientFactory for the OpenBreweryDb

services.AddHttpClient("OpenBreweryDb", client =>
{
	client.BaseAddress = new Uri("https://api.openbrewerydb.org/breweries/");
	client.DefaultRequestHeaders.Add("Accept", "application/json");
}).AddPolicyHandler(faultPolicy);

If you are not yet using the HttpClientFactory, the source code has an example for you too, check the BreweryNoHttpClientFactoryController.

Start up the application, it should open on port 5000 and make a request to look up Night Shift Brewing in Massachusetts. There is a 50% chance of an exception occurring with each request.

Try it out. You’ll see that my application does not handle these failures very well, it simply returns the error to original caller.

That’s it, you are now starting down the road of mixing in some chaos into your application.

What then?
Great, my application fails, but we always knew it could. There’s not much point introducing failures unless you plan to handle them in some way, and this is where you might add a retry policy to improve you likelihood of a successful request.

Of course, the Polly retry, or wait and retry policies would be ideal. I’ll show how to do just that in the next post.

Full source code here.

Selectively Caching Values Inside HttpResponseMessage with Polly – caching series 3/3

Full source code here.

This is the last of three posts on caching with Polly. The first showed how to selectively cache HttpResponseMessages based on the status code of the response.

The second showed how to cache values inside the HttpResponseMessage rather than the whole of the response.

This final post will show how to selectively cache values inside the response based on the status of the response.

If you have read the previous posts the content of this will feel familiar.

Changes to Startup
Like the previous post, add a memory cache, a policy registry and a HttpClientFactory to the service collection.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache();
    services.AddSingleton<IAsyncCacheProvider, MemoryCacheProvider>();

    IPolicyRegistry<string> registry = services.AddPolicyRegistry();

    services.AddHttpClient("RemoteServer", client =>
    {
        client.BaseAddress = new Uri("http://localhost:5000/api/");
        client.DefaultRequestHeaders.Add("Accept", "application/json");
    });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

What’s new is in the Configure method, fist step is to add the registry to the method parameters, DI will take care of this for us.

public void Configure(IApplicationBuilder app, IHostingEnvironment env,
    IAsyncCacheProvider cacheProvider, IPolicyRegistry<string> registry)
{

Next is the cache filter, this one examines the tuple of int and HttpStatusCode and, if the HttpStatusCode is an OK the tuple will be cached for the duration specified. If the respones is not OK the filter will set the cache time to 0 which is interpreted as do not cache by the cache policy.

Func<Context, (int value, HttpStatusCode StatusCode), Ttl> cacheOnly200OkFilter =
	(context, result) => new Ttl(
		timeSpan: result.StatusCode == HttpStatusCode.OK ? TimeSpan.FromSeconds(5) : TimeSpan.Zero,
		slidingExpiration: true
	);

And this is the cache policy that uses the filter above to store the tuple.

IAsyncPolicy<(int, HttpStatusCode)> cachePolicy =
	Policy.CacheAsync<(int, HttpStatusCode)>(
		cacheProvider.AsyncFor<(int, HttpStatusCode)>(), 
		new ResultTtl<(int, HttpStatusCode)>(cacheOnly200OkFilter),
		onCacheError: null);

Lastly, add the policy to the registry.

registry.Add("cacheOnly200OkFilter", cachePolicy);

Changes to the Controller
Over in the CatalogController there many changes.

At the top I take in the HttpClientFactory and policy registry.

private readonly IHttpClientFactory _httpClientFactory;
private readonly IPolicyRegistry<string> _policyRegistry;

public CatalogController(IHttpClientFactory httpClientFactory, IPolicyRegistry<string> policyRegistry)
{
	_httpClientFactory = httpClientFactory;
	_policyRegistry = policyRegistry;
}

Inside the Get method, I setup grab the HttpClient from the HttpClientFactory, set the endpoint of the remote service, take the cache policy from the registry and setup the context for the cache policy to cache based on the incoming request. All of this is similar to normal usage of the Polly cache.

[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
    string requestEndpoint = $"inventory/{id}";
    var httpClient = _httpClientFactory.CreateClient("RemoteServer");

    var cachePolicy = _policyRegistry.Get<CachePolicy<(int, HttpStatusCode)>>("cacheOnly200Ok");
    Context policyExecutionContext = new Context($"GetInventoryById-{id}");	

As in the previous post I don’t call the HttpClient from inside the cache policy, instead I need to call something that returns the int and the HttpStatusCode. I use a local function that makes the request to the remote service, checks the response, deserializes the value and returns a tuple.

(int ItemsInStock, HttpStatusCode StatusCode) itemsInStockAndStatusCode =
    await cachePolicy.ExecuteAsync(context => MakeRequest(), policyExecutionContext);

// local function
async Task<(int count, HttpStatusCode statusCode)> MakeRequest()
{
    HttpResponseMessage response = await httpClient.GetAsync(requestEndpoint);

    if (response.IsSuccessStatusCode)
    {
        int itemsInStock = JsonConvert.DeserializeObject<int>(await response.Content.ReadAsStringAsync());
        return (itemsInStock, response.StatusCode);
    }

    return (0, response.StatusCode);
}

When the cachePolicy is executed it first checks if there is value already in the cache, if there is, that will be returned and the local function will not be called. If there is no value in the cache, the local function is called, if the response of the local function is in the 200 range, the value is stored in the cache.

At this point, the action method has not returned anything to its caller.

Below the local function I check the tuple returned from the call to the cache policy and return a response.

if (itemsInStockAndStatusCode.StatusCode != HttpStatusCode.OK)
{
     return StatusCode((int)itemsInStockAndStatusCode.StatusCode);
}
return Ok(itemsInStockAndStatusCode.ItemsInStock);

For clarity, here is the full Get method.

[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
    string requestEndpoint = $"inventory/{id}";
    var httpClient = _httpClientFactory.CreateClient("RemoteServer");

    var cachePolicy = _policyRegistry.Get<CachePolicy<(int, HttpStatusCode)>>("cacheOnly200Ok");
    Context policyExecutionContext = new Context($"GetInventoryById-{id}");

    (int ItemsInStock, HttpStatusCode StatusCode) itemsInStockAndStatusCode =
        await cachePolicy.ExecuteAsync(context => MakeRequest(), policyExecutionContext);

    async Task<(int count, HttpStatusCode statusCode)> MakeRequest()
    {
        HttpResponseMessage response = await httpClient.GetAsync(requestEndpoint);

        if (response.IsSuccessStatusCode)
        {
            int itemsInStock = JsonConvert.DeserializeObject<int>(await response.Content.ReadAsSt
            return (itemsInStock, response.StatusCode);
        }

        // it doesn't matter what int you use here, it won't be cached as the StatusCode is not a
        return (0, response.StatusCode);
    }

    if (itemsInStockAndStatusCode.StatusCode != HttpStatusCode.OK)
    {
         return StatusCode((int)itemsInStockAndStatusCode.StatusCode);
    }
    return Ok(itemsInStockAndStatusCode.ItemsInStock);
}

Here endeth the series on caching.

Full source code here.

Using the HttpClientInterception to Test Methods That Use a HttpClient

Full source code available here.

In my previous post I showed a way of testing a controller that uses a HttpClient.

I had to mock the HttpMessageHandler pass that to the HttpClient and set a bunch of properties. It works well, but is a bit long winded.

I received a comment from a reader who suggested that I try the JustEat.HttpClientInterception library. It allows you to setup responses to specified requests, and pass these to a HttpClient. Then the HttpClient is passed to the controller.

Here is how the test method looks –

[Fact]
public async Task GetTest()
{
    //Arrange
    List<int> myList = new List<int>() {1, 2, 3, 4, 5};
    
    // setup the interceptor
    HttpRequestInterceptionBuilder builder = new HttpRequestInterceptionBuilder()
        .ForHost("localhost.something.com")
        .ForPath("/v1/numbers")
        .WithJsonContent(myList);
    
    // create the HttpClient from the builder
    // and setup the HttpClientBaseAddress
    HttpClient client = new HttpClientInterceptorOptions()
        .Register(builder).CreateHttpClient("http://localhost.something.com/v1/");

    ValuesController controller = new ValuesController(client);

    //Act
    IActionResult result = await controller.Get();

    //Assert
    OkObjectResult resultObject = result as OkObjectResult;
    Assert.NotNull(resultObject);

    List<int> numbers = resultObject.Value as List<int>;
    Assert.Equal(5, numbers.Count);
}

Briefly, here is the constructor of the values controller. It takes the HttpClient as a parameter, usually passed by dependency injection.

public class ValuesController : Controller
{
    readonly IAsyncPolicy<HttpResponseMessage> _httpRetryPolicy;
    private readonly HttpClient _httpClient;

    public ValuesController(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
	//snip..

Full source code available here.

HttpContent ReadAsAsync with .NET Core 2

Full source code available here.

If you are used to using HttpContent.ReadAsAsync you might be surprised to learn that it is missing from .NET Core 2. You can try adding Microsoft.AspNet.WebApi.Client but you might get warnings or errors.

At some point Microsoft will come out with an updated NuGet package, but in the meantime here is a work around.

At this extension method to your code.

using Newtonsoft.Json;
using System.Net.Http;
using System.Threading.Tasks;

namespace ReadAsAsyncCore
{
    public static class HttpContentExtensions
    {
        public static async Task<T> ReadAsJsonAsync<T>(this HttpContent content)
        {
            string json = await content.ReadAsStringAsync();
            T value = JsonConvert.DeserializeObject<T>(json);
            return value;
        }
    }
}

And use like this.

HttpResponseMessage httpResponse = await httpClient.GetAsync(requestEndpoint);
List<Product> products = await httpResponse.Content.ReadAsJsonAsync<List<Product>>();

Full source code available here.

.Net Core Multiple Get Methods with the Action Method Selector Attribute

Full source code available here.

In .Net Core Web Api it is not possible for the routing mechanism to distinguish between the following action methods.

public string GetSomething(int id, int something)
and
public string GetAnother(int id, int another)

But with the help of an ActionMethodSelectorAttribute we can tell the methods apart.

Step 1
Add a new class called QueryStringConstraint and inherit from ActionMethodSelectorAttribute.

public class QueryStringConstraint : ActionMethodSelectorAttribute
{
	public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
	{
		IList<ParameterDescriptor> methodParameters = action.Parameters;

		ICollection<string> queryStringKeys = routeContext.HttpContext.Request.Query.Keys.Select(q => q.ToLower()).ToList();
		IList<string> methodParamNames = methodParameters.Select(mp => mp.Name.ToLower()).ToList();

		foreach (var methodParameter in methodParameters)
		{
			if (methodParameter.ParameterType.Name.Contains("Nullable")) 
			{
				//check if the query string has a parameter that is not in the method params
				foreach(var q in queryStringKeys)
				{
					if (methodParamNames.All(mp => mp != q))
					{
						return false;
					}
				}

				if(queryStringKeys.All(q => q != methodParameter.Name.ToLower()))
				{
					continue;
				}
			}
			else if (queryStringKeys.All(q => q != methodParameter.Name.ToLower()))
			{
				return false;
			}
		}
		return true;
	}
}

Step 2
Add the QueryStringConstraint to the action methods that need to be distinguished by query string parameters.

[QueryStringConstraint] 
public string GetAnother(int id, int another)
{
	return $"GetAnother {id} {another}";
}

// http://localhost:62922/api/values/?id=1&something=22
[QueryStringConstraint] 
public string GetSomething(int id, int something)
{
	return $"GetSomething {id} {something}";
} 

Full source code available here.

.NET Core Web Api Routing

Full source code available here.

Routing in .NET Core Web Api (1.1 and 2) is a little different than in earlier versions.

I’ve written about this a few times, you can find those posts here.

In this post I’m going to show some examples of routing that might help you if you are having problems, especially if you want to have multiple GET methods. In a later article I will show how to use the action method selector to choose between actions methods with the same signature.

The code and comments are fairly self explanatory.

[HttpGet("api/Person")]
public class PersonController : Controller
{
	// GET: api/Person
	[HttpGet]
	public IEnumerable<string> Get()
	{
		return new string[] { "Dave Smith", "Edward Temple" };
	}

	// http://localhost:27624/api/person/1
	[HttpGet("{id}")]
	public string Get(int id)
	{
		return $"Get by id:{id} - Dave Smith";
	}

	// http://localhost:62689/api/person/ByNumber/5
	[HttpGet("ByNumber/{number}")]
	public string GetByNumber(int number)
	{
		return $"Get by number:{number} - Tom Davis";
	}
	// http://localhost:62689/api/person/ByFirstName/Steve
	[HttpGet("ByFirstName")]
	public string GetByFristName(string firstName)
	{
		return $"Get by first name - {firstName} Smith";
	}

	// http://localhost:62689/api/person/ByLastName?lastname=Smith
	[HttpGet("ByLastName")]
	public string GetByLastName(string lastName)
	{
		return $"Get by last name - Dave {lastName}";
	}

	// http://localhost:62689/api/person/ByFirstAndLastName?firstname=Steve&lastname=Smith
	[HttpGet("ByFirstAndLastName")]
	public string GetByFirstAndLastName(string firstName, string lastName)
	{
		return $"Get by first and last name - {firstName} {lastName}";
	}
}

Here are some more examples with attribute routing and a query model.

[HttpGet("api/accounts/{AccountId}")]
public class AccountsController : Controller
{
	// http://localhost:62689/api/accounts/11
	public IActionResult Get(int accountId)
	{
		return Ok(accountId);
	}

	// http://localhost:62689/api/accounts/?accountId=11
	// http://localhost:62689/api/accounts/?accountId=11&AccountName=dave
	// http://localhost:62689/api/accounts/?accountId=11&AccountName=dave&managerid=15
	[HttpGet("/api/accounts/")]
	public IActionResult Get(QueryModel queryModel)
	{
		return Ok(queryModel);
	}

	// http://localhost:62689/api/accounts/11/manager/22
	[HttpGet("Manager/{ManagerId}")]
	public IActionResult Get(int accountId, int managerId)
	{
		return Ok($"accountId:{accountId}, managerId:{managerId}");
	}

	// http://localhost:62689/api/accounts/11/manager/22/role/33
	[HttpGet("/api/accounts/{AccountId}/Manager/{ManagerId}/Role/{RoleId}")]
	public IActionResult Get(int accountId, int managerId, int roleId)
	{
		return Ok($"accountId:{accountId}, managerId:{managerId}, roleId:{roleId}");
	}
}

public class QueryModel
{
	public int AccountId { get; set; }
	public string AccountName { get; set; }
	public int ManagerId { get; set; }
}

Full source code available here.

Returning default values from a failed web request with Polly Fallbacks

Full source code available here.

In previous posts on Polly I showed how to use a simple retry, and a retry with a delegate that is called before the request is retried.

In this post I will show how use a Polly fallback policy, this allows a “fallback” or default value to be returned when both the request and retries fail.

The retry policy is similar to ones I have used before.

_httpRetrytPolicy = Policy.HandleResult<HttpResponseMessage>(
         r => r.StatusCode == HttpStatusCode.InternalServerError)
        .WaitAndRetryAsync(2, retryAttempt => TimeSpan.FromSeconds(retryAttempt));

The fallback policy returns a HttpResponseMessage with some cached values.

_httpRequestFallbackPolicy = Policy.HandleResult<HttpResponseMessage>(
         r => r.StatusCode == HttpStatusCode.InternalServerError)
         .FallbackAsync(new HttpResponseMessage(HttpStatusCode.OK)
         {
             Content = new ObjectContent(_cachedList.GetType(), _cachedList, new JsonMediaTypeFormatter())
         });

Using the polices is almost the same as in my earlier posts, the httpClient.GetAsync is executed by the retry policy. The retry policy is executed by the fallback policy. The Polly documentation refers to this as a “‘Russian-Doll’ or ‘onion-skin-layers’ model”.

HttpResponseMessage httpResponse =  
   await _httpRequestFallbackPolicy.ExecuteAsync( () 
   =>  _httpRetrytPolicy.ExecuteAsync(()
   =>  httpClient.GetAsync(requestEndpoint)));

The above call inside a call can also be done with a Polly policy wrap, I will demonstrate that in later post.

If the request succeeds the first time, the response is assigned to httpResponse. If the request fails, the retry policy kicks in and retries up to three times. If all of those fail, the fallback policy returns a httpResponse containing the cached list.

Full source code available here.

Re-authorization and onRetry with Polly

Full source code available here.

In a previous post I showed how to use Polly to make a simple call, retrying in the event of failure. In another I showed how to let a request fail and process the response.

In this post I will show a retry where a delegate is called as part of the retry. A common use case for this is reauthorizing after an Unauthorized response.

The difference between this policy and the ones in my previous posts is very small.

public class ValuesController : ApiController
{
    readonly RetryPolicy<HttpResponseMessage> _httpRequestPolicy;
    private HttpClient _httpClient;
    public ValuesController()
    {
        _httpRequestPolicy = Policy.HandleResult<HttpResponseMessage>(
                r => r.StatusCode == HttpStatusCode.InternalServerError ||
                     r.StatusCode == HttpStatusCode.Unauthorized)
            .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(retryAttempt), onRetry: (response, timespan) =>
            {
                if (response.Result.StatusCode == HttpStatusCode.Unauthorized)
                {
                    PerformReauthorization();
                }
            });
    }

The onRetry delegate is the new part –

onRetry: (response, timespan) =>
{
	if (response.Result.StatusCode == HttpStatusCode.Unauthorized)
	{
		PerformReauthorization();
	}
}

In my PerformReauthorization I create a new HttpClient and pass it the new authorization code. For simplicity I am passing a code in the cookie of my request, you could of course use Basic Authentication or any another technique.

private void PerformReauthorization()
{
    // here you would get a new token, call some other service, etc.
    _httpClient = GetHttpClient("GoodAuthCode"); // pass in the good auth token
}

The important thing to note is that my HttpClient instance is class level, meaning it can be accessed from PerformReauthorization.

The call to the web service remains the same.

HttpResponseMessage httpResponse = await _httpRequestPolicy.ExecuteAsync(() => _httpClient.GetAsync(requestEndpoint))

I’ll post of fallbacks soon, they allow a default value to be returned in the event of repeated failures.

For completeness here is GetHttpClient.

private HttpClient GetHttpClient(string authCookieValue)
{
    var cookieContainer = new CookieContainer();
    var handler = new HttpClientHandler() {CookieContainer = cookieContainer};
    cookieContainer.Add(new Uri("http://localhost"),new Cookie("Auth", authCookieValue));
    _httpClient = new HttpClient(handler);
    _httpClient.BaseAddress = new Uri(@"http://localhost:2629/api/");

    _httpClient.DefaultRequestHeaders.Accept.Clear();
    _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    return _httpClient;
}

Full source code available here.

Letting a request fail with Polly

Polly is fantastic for transparently retrying a request but there is always a point at which you must give up. Here you have a choice, let the request fail and inform the original caller or use the Fallback feature of Polly. In this post I’m going to show how to let the request fail. I’ll post another time on the Polly Fallback feature.

With Polly, failing is done in much the same was as you would do without Polly. Make the request, examine the response, return the appropriate status to the caller.

The example code below shows this when calling a Web Api 2 endpoint. The hoped for response is OK (200), any other is considered a failure.

HttpResponseMessage httpResponse = await _policies.RequestTimeoutPolicy.ExecuteAsync(() => httpClient.GetAsync(requestEndpoint));

if (httpResponse.IsSuccessStatusCode)
{
    int number = await httpResponse.Content.ReadAsAsync<int>();
    return Ok(number);
}

string errorMessage = await httpResponse.Content.ReadAsStringAsync();
return Content(httpResponse.StatusCode, errorMessage);

Fallback would allow me to return some default number and OK even if the web request failed. Coming soon…