Skip to content
This repository was archived by the owner on Nov 17, 2018. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions HttpClientFactory.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{EBF25046
build\repo.props = build\repo.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Http.Polly", "src\Microsoft.Extensions.Http.Polly\Microsoft.Extensions.Http.Polly.csproj", "{69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Http.Polly.Test", "test\Microsoft.Extensions.Http.Polly.Test\Microsoft.Extensions.Http.Polly.Test.csproj", "{6C61E727-6E0B-402D-8E6E-2FE4E7590822}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -47,6 +51,14 @@ Global
{0A0C4C1A-3A07-43C1-8A74-4DE193D50939}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A0C4C1A-3A07-43C1-8A74-4DE193D50939}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A0C4C1A-3A07-43C1-8A74-4DE193D50939}.Release|Any CPU.Build.0 = Release|Any CPU
{69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5}.Release|Any CPU.Build.0 = Release|Any CPU
{6C61E727-6E0B-402D-8E6E-2FE4E7590822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6C61E727-6E0B-402D-8E6E-2FE4E7590822}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C61E727-6E0B-402D-8E6E-2FE4E7590822}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C61E727-6E0B-402D-8E6E-2FE4E7590822}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -56,6 +68,8 @@ Global
{752F6163-AE48-46D3-8A6D-695BBF3629CB} = {A7C2238C-5C0F-4D33-BE66-4015985CE962}
{42C81623-6316-4C15-9E41-84AF48608C47} = {FF6B150F-C423-41BB-9563-55A0DFEAE21C}
{0A0C4C1A-3A07-43C1-8A74-4DE193D50939} = {BAD8867C-73F7-4DB8-8E79-70287E67987A}
{69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5} = {0ACA47C2-6B67-46B8-A661-C564E4450DE4}
{6C61E727-6E0B-402D-8E6E-2FE4E7590822} = {A7C2238C-5C0F-4D33-BE66-4015985CE962}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {002C7A25-D737-4B87-AFBB-B6E0FB2DB0D3}
Expand Down
1 change: 1 addition & 0 deletions build/dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<MicrosoftNETCoreApp21PackageVersion>2.1.0-preview2-26130-04</MicrosoftNETCoreApp21PackageVersion>
<MicrosoftNETTestSdkPackageVersion>15.3.0</MicrosoftNETTestSdkPackageVersion>
<MoqPackageVersion>4.7.49</MoqPackageVersion>
<PollySignedPackageVersion>5.8.0</PollySignedPackageVersion>
<XunitAnalyzersPackageVersion>0.8.0</XunitAnalyzersPackageVersion>
<XunitPackageVersion>2.3.1</XunitPackageVersion>
<XunitRunnerVisualStudioPackageVersion>2.4.0-beta.1.build3945</XunitRunnerVisualStudioPackageVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Extensions.Http\Microsoft.Extensions.Http.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Extensions.Http.Polly\Microsoft.Extensions.Http.Polly.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
8 changes: 8 additions & 0 deletions samples/HttpClientFactorySample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using Polly;

namespace HttpClientFactorySample
{
Expand Down Expand Up @@ -53,6 +54,13 @@ public static void Configure(IServiceCollection services)
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // Github API versioning
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent
})

// Build a totally custom policy using any criteria
.AddPolicyHandler(Policy.Handle<HttpRequestException>().RetryAsync())

// Build a policy that will handle exceptions and 500s from the remote server
.AddServerErrorPolicyHandler(p => p.RetryAsync())

.AddHttpMessageHandler(() => new RetryHandler()) // Retry requests to github using our retry handler
.AddTypedClient<GithubClient>();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.Http;
using Polly;

namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extensions methods for configuring <see cref="PolicyHttpMessageHandler"/> message handlers as part of
/// and <see cref="HttpClient"/> message handler pipeline.
/// </summary>
public static class PollyHttpClientBuilderExtensions
{
/// <summary>
/// Adds a <see cref="PolicyHttpMessageHandler"/> which will surround request execution with the provided
/// <see cref="IAsyncPolicy"/>.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <param name="policy">The <see cref="IAsyncPolicy"/>.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
/// <remarks>
/// <para>
/// See the remarks on <see cref="PolicyHttpMessageHandler"/> for guidance on configuring policies.
/// </para>
/// </remarks>
public static IHttpClientBuilder AddPolicyHandler(this IHttpClientBuilder builder, IAsyncPolicy policy)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

if (policy == null)
{
throw new ArgumentNullException(nameof(policy));
}

// Important - cache policy instances so that they are singletons per handler.
var innerPolicy = policy.WrapAsync(Policy.NoOpAsync<HttpResponseMessage>());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reisenberger - what do you think of this approach instead of creating two separate code paths?

Having two separate code paths just seems a bit like clutter - and then there's naming to think about 😲..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var innerPolicy = policy.WrapAsync(Policy.NoOpAsync<HttpResponseMessage>());

I like how this simplifies the separate codepaths; that concerned me too.


builder.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(innerPolicy));
return builder;
}

/// <summary>
/// Adds a <see cref="PolicyHttpMessageHandler"/> which will surround request execution with the provided
/// <see cref="IAsyncPolicy{HttpResponseMessage}"/>.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <param name="policy">The <see cref="IAsyncPolicy{HttpResponseMessage}"/>.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
/// <remarks>
/// <para>
/// See the remarks on <see cref="PolicyHttpMessageHandler"/> for guidance on configuring policies.
/// </para>
/// </remarks>
public static IHttpClientBuilder AddPolicyHandler(this IHttpClientBuilder builder, IAsyncPolicy<HttpResponseMessage> policy)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

if (policy == null)
{
throw new ArgumentNullException(nameof(policy));
}

builder.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(policy));
return builder;
}

/// <summary>
/// Adds a <see cref="PolicyHttpMessageHandler"/> which will surround request execution with a <see cref="Policy"/>
/// created by executing the provided configuration delegate. The policy builder will be preconfigured to trigger
/// application of the policy for requests that fail with a connection or server error (5XX status code).
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <param name="configurePolicy">A delegate used to create a <see cref="IAsyncPolicy{HttpResponseMessage}"/>.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
/// <remarks>
/// <para>
/// See the remarks on <see cref="PolicyHttpMessageHandler"/> for guidance on configuring policies.
/// </para>
/// <para>
/// The <see cref="PolicyBuilder{HttpResponseMessage}"/> provided to <paramref name="configurePolicy"/> has been
/// preconfigured to handle connection errors (as <see cref="HttpRequestException"/>) or server errors (as a 5XX HTTP
/// status code). The configuration is similar to the following code sample:
/// <code>
/// Policy.HandleAsync&lt;HttpRequestException&gt;().OrResult&lt;HttpResponseMessage&gt;(response =>
/// {
/// return response.StatusCode >= HttpStatusCode.InternalServerError;
/// }
/// </code>
/// </para>
/// <para>
/// The policy created by <paramref name="configurePolicy"/> will be cached indefinitely per named client. Policies
/// are generally designed to act as singletons, and can be shared when appropriate. To share a policy across multiple
/// named clients, first create the policy and the pass it to multiple calls to
/// <see cref="AddPolicyHandler(IHttpClientBuilder, IAsyncPolicy)"/> or
/// <see cref="AddPolicyHandler(IHttpClientBuilder, IAsyncPolicy{HttpResponseMessage})"/> as desired.
/// </para>
/// </remarks>
public static IHttpClientBuilder AddServerErrorPolicyHandler(
this IHttpClientBuilder builder,
Func<PolicyBuilder<HttpResponseMessage>, IAsyncPolicy<HttpResponseMessage>> configurePolicy)
{

var policyBuilder = Policy.Handle<HttpRequestException>().OrResult<HttpResponseMessage>(response =>
{
return response.StatusCode >= HttpStatusCode.InternalServerError;
});

// Important - cache policy instances so that they are singletons per handler.
var policy = configurePolicy(policyBuilder);

builder.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(policy));
return builder;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>
The HttpClient factory is a pattern for configuring and retrieving named HttpClients in a composable way. This package integrates IHttpClientFactory with the Polly library, to add transient-fault-handling and resiliency through fluent policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback.
</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;httpclient</PackageTags>

<!-- Don't use Microsoft.AspNetCore.Http.Polly as a namespace, that introduces ambiguities with 'Polly' -->
<RootNamespace>Microsoft.AspNetCore.Http</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Extensions.Http\Microsoft.Extensions.Http.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Polly-Signed" Version="$(PollySignedPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.ValueStopwatch.Sources" Version="$(MicrosoftExtensionsValueStopwatchSourcesPackageVersion)" PrivateAssets="All" />
</ItemGroup>

</Project>
83 changes: 83 additions & 0 deletions src/Microsoft.Extensions.Http.Polly/PolicyHttpMessageHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Polly;

namespace Microsoft.Extensions.Http
{
/// <summary>
/// A <see cref="DelegatingHandler"/> implementation that executes request processing surrounded by a <see cref="Policy"/>.
/// </summary>
/// <remarks>
/// <para>
/// This message handler implementation supports the use of policies provided by the Polly library for
/// transient-fault-handling and resiliency.
/// </para>
/// <para>
/// The documentation provided here is focused guidance for using Polly together with the <see cref="IHttpClientFactory"/>.
/// See the Polly project and its documentation (https://github.com/app-vnext/Polly) for authoritative information on Polly.
/// </para>
/// <para>
/// The extension methods on <see cref="PollyHttpClientBuilderExtensions"/> are designed as a convenient and correct
/// to creating a <see cref="PolicyHttpMessageHandler"/>.
/// </para>
/// <para>
/// The <see cref="PollyHttpClientBuilderExtensions.AddPolicyHandler(IHttpClientBuilder, IAsyncPolicy)"/> and
/// <see cref="PollyHttpClientBuilderExtensions.AddPolicyHandler(IHttpClientBuilder, IAsyncPolicy{HttpResponseMessage})"/>
/// methods support the creation of a <see cref="PolicyHttpMessageHandler"/> for any kind of policy. This includes
/// non-reactive policies such as Timeout, or Cache which don't require the underlying request to fail first.
/// </para>
/// <para>
/// The <see cref="PollyHttpClientBuilderExtensions.AddServerErrorPolicyHandler(IHttpClientBuilder, Func{PolicyBuilder{HttpResponseMessage}, IAsyncPolicy{HttpResponseMessage}})"/>
/// method is an opinionated convenience method that supports the application of a policy for requests that fail due
/// to a connection failure or server error (5XX HTTP status code). This kind of method supports only reactive policies
/// such as Retry, Circuit-Breaker or Fallback. This method is only provided for convenience, we recommend creating
/// your own policies as needed if this does not meet your requirements.
/// </para>
/// <para>
/// Take care when using policies such as Retry or Timeout together as HttpClient provides its own timeout via
/// <see cref="HttpClient.Timeout"/>.
/// </para>
/// <para>
/// All policies provided by Polly are designed to efficient when used in a long-lived way. Certain policies such as the
/// Bulkhead and Circuit-Breaker share state and may not have the desired effect when not shared properly. Take care to
/// ensure the correct lifetimes when using policies and message handlers together in custom scenarios. The extension
/// methods provided by <see cref="PollyHttpClientBuilderExtensions"/> are designed to assign a long lifetime to policies
/// and ensure that they can be used when the handler rotation feature is active.
/// </para>
/// </remarks>
public class PolicyHttpMessageHandler : DelegatingHandler
{
private readonly IAsyncPolicy<HttpResponseMessage> _policy;

/// <summary>
/// Creates a new <see cref="PolicyHttpMessageHandler"/>.
/// </summary>
/// <param name="policy">The policy.</param>
public PolicyHttpMessageHandler(IAsyncPolicy<HttpResponseMessage> policy)
{
if (policy == null)
{
throw new ArgumentNullException(nameof(policy));
}

_policy = policy;
}

/// <inheritdoc />
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}

return _policy.ExecuteAsync((ct) => base.SendAsync(request, ct), cancellationToken);
}
}
}
Loading