-
Notifications
You must be signed in to change notification settings - Fork 25.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Starting work on HttpClientFactory documentation #5483
Changes from 41 commits
1b897ac
37a56fb
0523d99
ca0cc90
7026d16
ed1b3dd
bfed490
812cf28
86e239d
ec14f81
d527081
ad34a62
37a5deb
b5aa44b
135a6e1
b62b134
c71f194
a7e1c51
590150d
567afbb
5d18655
2251cb9
c8b6438
aaf26d3
c9d0daf
7825296
b077c4b
44be1a5
f95bd56
d231feb
957f760
f715142
05e139f
af9c32f
aaf3240
fdf6d7c
c7d9e6d
b2c7d60
699e07d
e4bd7f0
d7a5b8c
4c76827
8a37fc7
b34cda6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
using System; | ||
using System.Net; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace HttpClientFactorySample.Controllers | ||
{ | ||
public class ThirdPartyController : Controller | ||
{ | ||
[Route("unreliable")] | ||
public IActionResult UnreliableEndpoint() | ||
{ | ||
var second = DateTime.UtcNow.Second; | ||
|
||
// about 50% of the time this will fail | ||
return second % 2 != 0 ? Ok() : StatusCode(HttpStatusCode.ServiceUnavailable); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
using System; | ||
using System.Threading.Tasks; | ||
using HttpClientFactorySample.Services; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace HttpClientFactorySample.Controllers | ||
{ | ||
public class UnreliableConsumerController : Controller | ||
{ | ||
private readonly UnreliableEndpointCallerService _unreliableEndpointCallerService; | ||
|
||
public UnreliableConsumerController(UnreliableEndpointCallerService unreliableEndpointCallerService) | ||
{ | ||
_unreliableEndpointCallerService = unreliableEndpointCallerService; | ||
} | ||
|
||
[Route("unreliable-consumer")] | ||
public async Task<IActionResult> UnreliableEndpointConsumer() | ||
{ | ||
// Builds a URI to what we will imagine is an external endpoint that is unreliable. For this sample we are hosting our own unreliable endpoint to demonstrate! | ||
|
||
var url = Url.Action("UnreliableEndpoint", "ThirdParty"); | ||
|
||
var uriBuilder = new UriBuilder | ||
{ | ||
Scheme = HttpContext.Request.Scheme, | ||
Host = HttpContext.Request.Host.Host, | ||
Port = HttpContext.Request.Host.Port ?? 80, | ||
Path = url | ||
}; | ||
|
||
// call the typed client that has been registered in ConfigureServices | ||
|
||
var status = await _unreliableEndpointCallerService.GetDataFromUnreliableEndpoint(uriBuilder.Uri.ToString()); | ||
|
||
return Ok(status); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
namespace HttpClientFactorySample.GitHub | ||
{ | ||
/// <summary> | ||
/// A partial representation of a branch object from the GitHub API | ||
/// </summary> | ||
public class GitHubBranch | ||
{ | ||
public string Name { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using System; | ||
using Newtonsoft.Json; | ||
|
||
namespace HttpClientFactorySample.GitHub | ||
{ | ||
/// <summary> | ||
/// A partial representation of an issue object from the GitHub API | ||
/// </summary> | ||
public class GitHubIssue | ||
{ | ||
[JsonProperty(PropertyName = "html_url")] | ||
public string Url { get; set; } | ||
|
||
public string Title { get; set; } | ||
|
||
[JsonProperty(PropertyName = "created_at")] | ||
public DateTime Created { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
namespace HttpClientFactorySample.GitHub | ||
{ | ||
/// <summary> | ||
/// A partial representation of a pull request object from the GitHub API | ||
/// </summary> | ||
public class GitHubPullRequest | ||
{ | ||
public string Title { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
using System; | ||
using System.IO; | ||
using System.Net.Http; | ||
using System.Threading.Tasks; | ||
using Newtonsoft.Json; | ||
|
||
namespace HttpClientFactorySample.GitHub | ||
{ | ||
/// <summary> | ||
/// Exposes methods to return GitHub API data | ||
/// </summary> | ||
#region snippet1 | ||
public class GitHubService | ||
{ | ||
public HttpClient Client { get; } | ||
|
||
public GitHubService(HttpClient client) | ||
{ | ||
client.BaseAddress = new Uri("https://api.github.com/"); | ||
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // GitHub API versioning | ||
client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // GitHub requires a user-agent | ||
|
||
Client = client; | ||
} | ||
|
||
public async Task<GitHubIssue> GetLatestDocsIssue() | ||
{ | ||
var response = await Client.GetAsync("/repos/aspnet/docs/issues?state=open&sort=created&direction=desc", HttpCompletionOption.ResponseHeadersRead); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
|
||
using (var stream = await response.Content.ReadAsStreamAsync()) | ||
using (var streamReader = new StreamReader(stream)) | ||
using (var jsonReader = new JsonTextReader(streamReader)) | ||
{ | ||
var serializer = new JsonSerializer(); | ||
|
||
while (await jsonReader.ReadAsync()) | ||
{ | ||
if (jsonReader.TokenType == JsonToken.StartObject) | ||
{ | ||
var issue = serializer.Deserialize<GitHubIssue>(jsonReader); | ||
|
||
if (issue != null) | ||
{ | ||
return issue; // we only want the first object | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code should be using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handy! I wasn't aware ReadAsAsync was available. I believe (while more complex) the code I have is slightly more efficient? In this example it's taking only the first item. A bit of a contrived example. With ReadAsAsync I get all items back in the list. A very rough comparison is ~150kb extra allocated between memory snapshots. I spoke to @glennc and @DamianEdwards at Summit as I was concerned about this complexity and if in fact this was a reasonable approach. At that time it seemed like we wanted to include something similar. I'm happy to make the change though? Side query: Am I correct in thinking that https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Net.Http.Formatting/HttpContentExtensions.cs is the source building Microsoft.AspNet.WebApi.Client? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If what you're saying is true then we should choose a different example. The focus of this doc isn't on the minutae of how to implement special case handling of JSON scenarios, so it should use the idiomatic approach - which is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can adjust the sample to return the full list and switch to ReadAsAsync. Not a problem from my side. I'll get that done hopefully later today / tomorrow. |
||
} | ||
} | ||
|
||
return null; | ||
} | ||
} | ||
#endregion | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
using System.Collections.Generic; | ||
using System.Net.Http; | ||
using System.Threading.Tasks; | ||
using Newtonsoft.Json; | ||
|
||
namespace HttpClientFactorySample.GitHub | ||
{ | ||
#region snippet1 | ||
public class RepoService | ||
{ | ||
private readonly HttpClient _httpClient; // not exposed publically | ||
|
||
public RepoService(HttpClient client) | ||
{ | ||
_httpClient = client; | ||
} | ||
|
||
public async Task<IEnumerable<string>> GetRepos() | ||
{ | ||
var response = await _httpClient.GetAsync("aspnet/repos"); | ||
|
||
response.EnsureSuccessStatusCode(); | ||
|
||
var responseContent = await response.Content.ReadAsStringAsync(); | ||
|
||
return JsonConvert.DeserializeObject<IEnumerable<string>>(responseContent); | ||
} | ||
} | ||
#endregion | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
using System; | ||
using System.Net.Http; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace HttpClientFactorySample.Handlers | ||
{ | ||
#region snippet1 | ||
public class RequestDataHandler : DelegatingHandler | ||
{ | ||
private readonly ILogger<RequestDataHandler> _logger; | ||
|
||
private const string RequestSourceHeaderName = "Request-Source"; | ||
private const string RequestSource = "HttpClientFactorySampleApp"; | ||
private const string RequestIdHeaderName = "Request-Identifier"; | ||
|
||
public RequestDataHandler(ILogger<RequestDataHandler> logger) | ||
{ | ||
_logger = logger; | ||
} | ||
|
||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, | ||
CancellationToken cancellationToken) | ||
{ | ||
var identifier = Guid.NewGuid(); // some information we want to generate and add per request | ||
|
||
_logger.LogInformation($"Starting request {identifier}"); | ||
|
||
request.Headers.Add(RequestSourceHeaderName, RequestSource); | ||
request.Headers.Add(RequestIdHeaderName, identifier.ToString()); | ||
|
||
return base.SendAsync(request, cancellationToken); | ||
} | ||
} | ||
#endregion | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using System; | ||
using System.Net.Http; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace HttpClientFactorySample.Handlers | ||
{ | ||
public class SecureRequestHandler : DelegatingHandler | ||
{ | ||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, | ||
CancellationToken cancellationToken) | ||
{ | ||
if (request.RequestUri.Scheme == Uri.UriSchemeHttp) | ||
{ | ||
var builder = new UriBuilder(request.RequestUri) { Scheme = Uri.UriSchemeHttps }; | ||
request.RequestUri = builder.Uri; | ||
} | ||
|
||
return base.SendAsync(request, cancellationToken); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace HttpClientFactorySample.Handlers | ||
{ | ||
#region snippet1 | ||
public class ValidateHeaderHandler : DelegatingHandler | ||
{ | ||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, | ||
CancellationToken cancellationToken) | ||
{ | ||
if (!request.Headers.Contains("X-API-KEY")) | ||
{ | ||
return new HttpResponseMessage(HttpStatusCode.BadRequest) | ||
{ | ||
Content = new StringContent("You must supply an API key header called X-API-KEY") | ||
}; | ||
} | ||
|
||
return await base.SendAsync(request, cancellationToken); | ||
} | ||
} | ||
#endregion | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>netcoreapp2.1</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0-preview2-final" /> | ||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="2.1.0-preview2-final" /> | ||
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.1.0-preview2-final" /> | ||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.0-preview2-final" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.1.0-preview1-final" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
@page | ||
@model BasicUsageModel | ||
@{ | ||
} | ||
|
||
<h1>Branches for Docs Repo</h1> | ||
|
||
<ul> | ||
@foreach (var branch in Model.Branches) | ||
{ | ||
<li>@branch.Name</li> | ||
} | ||
</ul> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Net.Http; | ||
using System.Threading.Tasks; | ||
using HttpClientFactorySample.GitHub; | ||
using Microsoft.AspNetCore.Mvc.RazorPages; | ||
using Newtonsoft.Json; | ||
|
||
namespace HttpClientFactorySample.Pages | ||
{ | ||
#region snippet1 | ||
public class BasicUsageModel : PageModel | ||
{ | ||
private readonly IHttpClientFactory _clientFactory; | ||
|
||
public IEnumerable<GitHubBranch> Branches { get; private set; } | ||
|
||
public BasicUsageModel(IHttpClientFactory clientFactory) | ||
{ | ||
_clientFactory = clientFactory; | ||
} | ||
|
||
public async Task OnGet() | ||
{ | ||
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/aspnet/docs/branches" ); | ||
request.Headers.Add("Accept", "application/vnd.github.v3+json"); | ||
request.Headers.Add("User-Agent", "HttpClientFactory-Sample"); | ||
|
||
var client = _clientFactory.CreateClient(); | ||
|
||
var response = await client.SendAsync(request); | ||
|
||
if (response.IsSuccessStatusCode) | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rynowak Do you think this is a reasonable approach when consuming directly inside a Razor Page? |
||
var data = await response.Content.ReadAsStringAsync(); | ||
Branches = JsonConvert.DeserializeObject<IEnumerable<GitHubBranch>>(data); | ||
} | ||
else | ||
{ | ||
Branches = Array.Empty<GitHubBranch>(); | ||
} | ||
} | ||
} | ||
#endregion | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
@page | ||
@model ErrorModel | ||
@{ | ||
ViewData["Title"] = "Error"; | ||
} | ||
|
||
<h1 class="text-danger">Error.</h1> | ||
<h2 class="text-danger">An error occurred while processing your request.</h2> | ||
|
||
@if (Model.ShowRequestId) | ||
{ | ||
<p> | ||
<strong>Request ID:</strong> <code>@Model.RequestId</code> | ||
</p> | ||
} | ||
|
||
<h3>Development Mode</h3> | ||
<p> | ||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred. | ||
</p> | ||
<p> | ||
<strong>Development environment should not be enabled in deployed applications</strong>, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>, and restarting the application. | ||
</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd welcome thoughts on whether this is a) correct and b) too complicated for a sample?
I've done it this way as per the advice from @DamianEdwards and @davidfowl during NDC, they highlighted that for performance, larger response content should be read as stream, rather than string. @DamianEdwards touched on this again during this week's community stand-up.
This seems to work and I hope is a reasonably valid approach but as per those sources, it's not necessarily easy to do right.
Personally I'd like the sample to try to be real world to some extent, to avoid people copy/pasting demoware code, but I understand this may over-complicate the main topic being shown here.
cc// @scottaddie