├── .gitattributes ├── .gitignore ├── README.md ├── RestClientSample.sln └── RestClientSample ├── HomeController.cs ├── IHttpClientBuilder.cs ├── IRestClient.cs ├── MyMessageHandler.cs ├── Program.cs ├── RestClient.cs ├── RestClientOptions.cs ├── RestClientSample.csproj ├── RestClientServiceCollectionExtensions.cs └── Startup.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text=auto diff=csharp 17 | *.vb text=auto 18 | *.resx text=auto 19 | *.c text=auto 20 | *.cpp text=auto 21 | *.cxx text=auto 22 | *.h text=auto 23 | *.hxx text=auto 24 | *.py text=auto 25 | *.rb text=auto 26 | *.java text=auto 27 | *.html text=auto 28 | *.htm text=auto 29 | *.css text=auto 30 | *.scss text=auto 31 | *.sass text=auto 32 | *.less text=auto 33 | *.js text=auto 34 | *.lisp text=auto 35 | *.clj text=auto 36 | *.sql text=auto 37 | *.php text=auto 38 | *.lua text=auto 39 | *.m text=auto 40 | *.asm text=auto 41 | *.erl text=auto 42 | *.fs text=auto 43 | *.fsx text=auto 44 | *.hs text=auto 45 | 46 | *.csproj text=auto 47 | *.vbproj text=auto 48 | *.fsproj text=auto 49 | *.dbproj text=auto 50 | *.sln text=auto eol=crlf 51 | 52 | *.sh eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | *.sln.ide/ 6 | _ReSharper.*/ 7 | .idea/ 8 | packages/ 9 | artifacts/ 10 | PublishProfiles/ 11 | .vs/ 12 | *.user 13 | *.suo 14 | *.cache 15 | *.docstates 16 | _ReSharper.* 17 | nuget.exe 18 | *net45.csproj 19 | *net451.csproj 20 | *k10.csproj 21 | *.psess 22 | *.vsp 23 | *.pidb 24 | *.userprefs 25 | *DS_Store 26 | *.ncrunchsolution 27 | *.*sdf 28 | *.ipch 29 | *.swp 30 | *~ 31 | .build/ 32 | .testPublish/ 33 | launchSettings.json 34 | BenchmarkDotNet.Artifacts/ 35 | BDN.Generated/ 36 | binaries/ 37 | global.json 38 | korebuild-lock.txt 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## RestClient sample 2 | 3 | This is a sample of an end to end client experience that we could build with ASP.NET Core using [refit](https://github.com/paulcbetts/refit) as as implementation detail. 4 | The idea here is to make it easy to declare and inject an autogenerated proxy for an HTTP service. That is acheived by registering an 5 | open generic RestClient\ that acts like a factory for a TClient. Each TClient must be configured in the ConfigureServices with the url (or http client). Other more advanced settings can also be set there. 6 | 7 | ### Fundamentals 8 | 9 | 1. Define an interface for your rest service. 10 | ````csharp 11 | public interface IConferencePlannerApi 12 | { 13 | [Get("/api/sessions")] 14 | Task> GetSessionsAsync(); 15 | } 16 | ```` 17 | 18 | 1. Configure the URL for your service. 19 | ```csharp 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | services.AddRestClient(o => 23 | { 24 | // Configure the URL for your service 25 | o.Url = "https://conferenceplanner-api.azurewebsites.net/"; 26 | }); 27 | } 28 | ``` 29 | 1. Consume the interface via RestClient\. 30 | ```csharp 31 | public class HomeController : Controller 32 | { 33 | private readonly IConferencePlannerApi _client; 34 | 35 | public HomeController(RestClient api) 36 | { 37 | _client = api.Client; 38 | } 39 | 40 | [HttpGet("/")] 41 | public async Task Get() 42 | { 43 | var sessions = await _client.GetSessionsAsync(); 44 | 45 | return Ok(sessions); 46 | } 47 | } 48 | ``` 49 | 50 | ### Future Ideas 51 | - Integrate service discovery. Add the ability to name the service that a client represents via an attribute to avoid having to specify a URL in ConfigureServices. 52 | 53 | ```csharp 54 | [ServiceName("conference-api")] 55 | public interface IConferencePlannerApi 56 | { 57 | [Get("/api/sessions")] 58 | Task> GetSessionsAsync(); 59 | } 60 | ``` 61 | - Support the MVC attributes for consistency between client and server. 62 | - Support returning ActionResult\. It would unify the story between the client and server even more. It could also bring efficiency gains by piping responses directly to the output when making outgoing calls. 63 | - Integrate circuit breakers and other policy into the configuration. 64 | -------------------------------------------------------------------------------- /RestClientSample.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestClientSample", "RestClientSample\RestClientSample.csproj", "{0FDC1736-BC17-41AC-8BF2-A1734BF3C309}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {0FDC1736-BC17-41AC-8BF2-A1734BF3C309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {0FDC1736-BC17-41AC-8BF2-A1734BF3C309}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {0FDC1736-BC17-41AC-8BF2-A1734BF3C309}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {0FDC1736-BC17-41AC-8BF2-A1734BF3C309}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {CECAA1F5-24E3-409C-A267-E8FFE6AC3F1E} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /RestClientSample/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace RestClientSample 8 | { 9 | public class HomeController : Controller 10 | { 11 | private readonly IConferencePlannerApi _client; 12 | 13 | public HomeController(IRestClient api) 14 | { 15 | _client = api.Client; 16 | } 17 | 18 | [HttpGet("/")] 19 | public async Task Get() 20 | { 21 | var sessions = await _client.GetSessionsAsync(); 22 | 23 | return Ok(sessions); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RestClientSample/IHttpClientBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | namespace RestClientSample 8 | { 9 | public interface IHttpClientBuilder 10 | { 11 | IHttpClientBuilder Use(DelegatingHandler handler); 12 | 13 | HttpMessageHandler Build(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /RestClientSample/IRestClient.cs: -------------------------------------------------------------------------------- 1 | namespace RestClientSample 2 | { 3 | public interface IRestClient 4 | { 5 | TClient Client { get; set; } 6 | 7 | TClient GetClient(string url); 8 | } 9 | } -------------------------------------------------------------------------------- /RestClientSample/MyMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace RestClientSample 9 | { 10 | public class MyMessageHandler : DelegatingHandler 11 | { 12 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 13 | { 14 | return base.SendAsync(request, cancellationToken); 15 | } 16 | } 17 | 18 | public static class MyMessageHandlerHttpClientBuilderExtensions 19 | { 20 | public static IHttpClientBuilder UseMyMessageHandler(this IHttpClientBuilder clientBuilder) 21 | { 22 | return clientBuilder.Use(new MyMessageHandler()); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /RestClientSample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace RestClientSample 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /RestClientSample/RestClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Options; 7 | using Refit; 8 | 9 | namespace RestClientSample 10 | { 11 | public class RestClient : IRestClient 12 | { 13 | private RefitSettings _settings; 14 | 15 | public TClient Client { get; set; } 16 | 17 | public RestClient(IOptions> options) 18 | { 19 | _settings = new RefitSettings(); 20 | var handler = options.Value.Build(); 21 | _settings.HttpMessageHandlerFactory = () => handler; 22 | 23 | if (options.Value.HttpClient != null) 24 | { 25 | Client = RestService.For(options.Value.HttpClient, _settings); 26 | } 27 | else 28 | { 29 | Client = RestService.For(options.Value.Url, _settings); 30 | } 31 | } 32 | 33 | public TClient GetClient(string url) 34 | { 35 | return RestService.For(url, _settings); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /RestClientSample/RestClientOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | 4 | namespace RestClientSample 5 | { 6 | public class RestClientOptions : IHttpClientBuilder 7 | { 8 | private List _handlers = new List(); 9 | 10 | public string Url { get; set; } 11 | public HttpClient HttpClient { get; set; } 12 | 13 | public HttpMessageHandler Build() 14 | { 15 | HttpMessageHandler current = new HttpClientHandler(); 16 | 17 | for (int i = _handlers.Count - 1; i >= 0; i--) 18 | { 19 | var handler = _handlers[i]; 20 | handler.InnerHandler = current; 21 | current = handler; 22 | } 23 | 24 | return current; 25 | } 26 | 27 | public IHttpClientBuilder Use(DelegatingHandler handler) 28 | { 29 | _handlers.Add(handler); 30 | return this; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /RestClientSample/RestClientSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /RestClientSample/RestClientServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.DependencyInjection.Extensions; 7 | 8 | namespace RestClientSample 9 | { 10 | public static class RestClientServiceCollectionExtensions 11 | { 12 | public static IServiceCollection AddRestClient(this IServiceCollection services, Action> action) 13 | { 14 | services.TryAddSingleton(typeof(IRestClient<>), typeof(RestClient<>)); 15 | services.Configure(action); 16 | return services; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /RestClientSample/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Newtonsoft.Json.Linq; 9 | using Refit; 10 | 11 | namespace RestClientSample 12 | { 13 | public class Startup 14 | { 15 | public void ConfigureServices(IServiceCollection services) 16 | { 17 | services.AddRestClient(o => 18 | { 19 | o.Url = "https://conferenceplanner-api.azurewebsites.net/"; 20 | o.UseMyMessageHandler(); 21 | }); 22 | 23 | services.AddMvc(); 24 | } 25 | 26 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 27 | { 28 | if (env.IsDevelopment()) 29 | { 30 | app.UseDeveloperExceptionPage(); 31 | } 32 | 33 | app.UseMvc(); 34 | } 35 | } 36 | 37 | public interface IConferencePlannerApi 38 | { 39 | [Get("/api/sessions")] 40 | Task> GetSessionsAsync(); 41 | } 42 | } 43 | --------------------------------------------------------------------------------