Skip to content
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

Merged
merged 44 commits into from
May 2, 2018
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1b897ac
Starting work on HttpClientFactory documentation (WIP)
stevejgordon Feb 15, 2018
37a56fb
Fixing some minor issues
stevejgordon Feb 15, 2018
0523d99
Updating per initial feedback
stevejgordon Feb 16, 2018
ca0cc90
Adding refit example
stevejgordon Feb 16, 2018
7026d16
Further feedback adjustments
stevejgordon Feb 16, 2018
ed1b3dd
Adding delegating handler middleware section
stevejgordon Feb 16, 2018
bfed490
Minor typo
stevejgordon Feb 16, 2018
812cf28
Initial Polly example
stevejgordon Feb 16, 2018
86e239d
Updating with latest feedback
stevejgordon Feb 19, 2018
ec14f81
Pass to remove you/your + improve readability and clarity
stevejgordon Feb 19, 2018
d527081
Additional tidy-up
stevejgordon Feb 19, 2018
ad34a62
Edits for latest feedback
stevejgordon Feb 21, 2018
37a5deb
UE pass
scottaddie Feb 21, 2018
b5aa44b
Snippet fixes
stevejgordon Feb 22, 2018
135a6e1
Add ms.custom metadata
scottaddie Feb 22, 2018
b62b134
Adding WIP sample project
stevejgordon Feb 23, 2018
c71f194
Minor edits to Fundamentals index page
scottaddie Feb 23, 2018
a7e1c51
Relocate master TOC link
scottaddie Feb 23, 2018
590150d
Feedback and sample update to 2.1 preview
stevejgordon Feb 27, 2018
567afbb
Fixing some missed HttpClientFactory items
stevejgordon Mar 1, 2018
5d18655
Initial snippets from sample
stevejgordon Mar 1, 2018
2251cb9
Fixing a copy/paste mistake
stevejgordon Mar 1, 2018
c8b6438
fixing code link
stevejgordon Mar 1, 2018
aaf26d3
Remove gerund from title
scottaddie Mar 1, 2018
c9d0daf
Fix wording in the description metadata value
scottaddie Mar 1, 2018
7825296
Minor verbiage tweaks
scottaddie Mar 2, 2018
b077c4b
WIP
stevejgordon Mar 20, 2018
44be1a5
Working on Polly sample and documentation
stevejgordon Apr 4, 2018
f95bd56
Using code from sample in docs
stevejgordon Apr 4, 2018
d231feb
UE pass
scottaddie Apr 5, 2018
957f760
Convert usage H2 headings to H3
scottaddie Apr 5, 2018
f715142
Adding initial logging information
stevejgordon Apr 6, 2018
05e139f
Verbiage tweaks
scottaddie Apr 10, 2018
af9c32f
Updating to 2.1 preview 2
stevejgordon Apr 16, 2018
aaf3240
Removing HTTPS redirection since launchSettings does not default to c…
stevejgordon Apr 17, 2018
fdf6d7c
Add monikerRange metadata for 2.1+
scottaddie Apr 23, 2018
c7d9e6d
Major sample and doc content update
stevejgordon Apr 26, 2018
b2c7d60
Adding section about configuring HttpMessageHandler
stevejgordon Apr 27, 2018
699e07d
Updating with feedback from Ryan
stevejgordon Apr 27, 2018
e4bd7f0
Feedback and updates
stevejgordon Apr 28, 2018
d7a5b8c
Incorporate Acrolinx feedback
scottaddie Apr 30, 2018
4c76827
Updating sample with preferred idioms and minor doc tweaks
stevejgordon May 2, 2018
8a37fc7
Fixing grammar and spelling
stevejgordon May 2, 2018
b34cda6
Add 2.1 Preview include
scottaddie May 2, 2018
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
242 changes: 242 additions & 0 deletions aspnetcore/fundamentals/http-requests.md

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))
Copy link
Contributor Author

@stevejgordon stevejgordon Mar 1, 2018

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

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
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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 ReadAsAsync.

Copy link
Contributor Author

@stevejgordon stevejgordon Apr 30, 2018

Choose a reason for hiding this comment

The 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)
{
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
23 changes: 23 additions & 0 deletions aspnetcore/fundamentals/http-requests/samples/Pages/Error.cshtml
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>
Loading