Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ASP.NET Core hidden gems

ASP.NET Core hidden gems

Microsoft’s ASP.NET Core is a brand new web framework, completely open sourced and cross-platform; for building scalable- & cloud-ready web applications. This talk covers some of the features in the framework, that is not that obvious, in my opinion, that we can utilise when building modern and practical APIs. Think REST, dependency injection, application startup, startup filters, custom servers, clients, hosted services, conventions, model binding, application parts, HATEOAS, and more.

Fanie Reynders

May 31, 2018
Tweet

More Decks by Fanie Reynders

Other Decks in Programming

Transcript

  1. Inline configuration public static void Main(string[] args) => WebHost .CreateDefaultBuilder(args)

    .ConfigureServices(services => { }) //optional .Configure(app => { }) //required .Build() .Run();
  2. Configuration as class public static void Main(string[] args) => WebHost

    .CreateDefaultBuilder<Startup>(args) .Build() .Run();
  3. Configuration as class public class Startup { // Optional public

    void ConfigureServices(IServiceCollection services) { } // Required public void Configure(IApplicationBuilder app) { } }
  4. public class AwesomeStartupFilter : IStartupFilter { public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)

    { return builder => { builder.Use(async (context, _next) => { if (context.Request.Query["key"] != "1234") { await context.Response.WriteAsync("You had one job!"); } else { await _next(); } }); next(builder); }; } }
  5. Startup filters public class Startup { public void ConfigureServices(IServiceCollection services)

    { services.AddTransient<IStartupFilter, AwesomeStartupFilter>(); } // ... }
  6. Configuration as class public class Startup { // Optional public

    void ConfigureServices(IServiceCollection services) { } // Required public void Configure(IApplicationBuilder app) { } }
  7. Configuration as class (per environment) public class Startup[Environment] { //

    Optional public void Configure[Environment]Services(IServiceCollection services) { } // Required public void Configure[Environment](IApplicationBuilder app) { } }
  8. public class Program { public static void Main(string[] args) =>

    WebHost .CreateDefaultBuilder(args) .UseStartup(Assembly.GetExecutingAssembly().FullName); .Build() .Run(); } public class StartupFoo { // Optional public void ConfigureServices(IServiceCollection services) { } // Required public void Configure(IApplicationBuilder app) { } }
  9. public class Startup { // Optional public void ConfigureFooServices(IServiceCollection services)

    { } // Required public void ConfigureFoo(IApplicationBuilder app) { } // Optional public void ConfigureBarServices(IServiceCollection services) { services.AddSingleton<IAwesome, Awesome>(); } // Required public void ConfigureBar(IApplicationBuilder app, IAwesome awesome) { } }
  10. Server definition public interface IServer : IDisposable { IFeatureCollection Features

    { get; } Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); }
  11. HttpClient • IDisposable - object is gone but connection is

    not • Socket exhaustion • Use singleton, but connections remain open – DNS TTL • Hard* to test
  12. HttpClientFactory • Factory for creating HTTP clients – ASP.NET Core

    2.1 • Easy to test • Consumption patterns • Direct • Named • Typed • Generated
  13. // Usage - direct public class MyController : Controller {

    private readonly IHttpClientFactory _httpClientFactory; public MyController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public async IActionResult Get() { var client = _httpClientFactory.CreateClient(); client.DefaultRequestHeaders.Add("Accept", "application/json"); client.DefaultRequestHeaders.Add("User-Agent", "Awesome"); var result = await client.GetStringAsync("https://api.awesome.io/foo"); return Ok(result); } }
  14. HttpClientFactory – Named client // Registration public void ConfigureServices(IServiceCollection services)

    { services.AddHttpClient("awesome", c => { c.BaseAddress = new Uri("https://api.awesome.io"); c.DefaultRequestHeaders.Add("Accept", "application/json"); c.DefaultRequestHeaders.Add("User-Agent", "Awesome"); }); // ... }
  15. // Usage – named client public class MyController : Controller

    { private readonly IHttpClientFactory _httpClientFactory; public MyController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public async IActionResult Get() { var client = _httpClientFactory.CreateClient("awesome"); var result = await client.GetStringAsync("foo"); return Ok(result); } }
  16. public class AwesomeService { public HttpClient _client { get; private

    set; } public AwesomeService(HttpClient client) { client.BaseAddress = new Uri("https://api.awesome.io"); client.DefaultRequestHeaders.Add("Accept", "application/json"); client.DefaultRequestHeaders.Add("User-Agent", "Awesome"); _client = client; } public async string Foo() { return await _client.GetStringAsync("foo"); } }
  17. // Usage – typed client public class MyController : Controller

    { private readonly AwesomeService _awesomeService; public MyController(AwesomeService awesomeService) { _awesomeService = awesomeService; } public async IActionResult Get() { var result = await awesomeService.Foo(); return Ok(result); } }
  18. HttpClientFactory – Generated client public interface IAwesomeService { [Get("/foo")] Task<string>

    Foo(); } // Registration public void ConfigureServices(IServiceCollection services) { services.AddTypedClient(c => Refit.RestService.For<IAwesomeService>(c)); // ... }
  19. // Usage – generated client public class MyController : Controller

    { private readonly IAwesomeService _awesomeService; public MyController(IAwesomeService awesomeService) { _awesomeService = awesomeService; } public async IActionResult Get() { var result = await awesomeService.Foo(); return Ok(result); } }
  20. Hosted Service public interface IHostedService { Task StartAsync(CancellationToken cancellationToken); Task

    StopAsync(CancellationToken cancellationToken); } // Registration public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IHostedService, AwesomeHostedService>(); // ... }
  21. public class AwesomeHostedService : IHostedService { private readonly ITwitterApi twitterApi;

    private readonly IMemoryCache cache; public AwesomeHostedService(ITwitterApi twitterApi, IMemoryCache cache) { this.twitterApi = twitterApi; this.cache = cache; } public Task StartAsync(CancellationToken cancellationToken) => PullTweets(); public Task StopAsync(CancellationToken cancellationToken) => Task.FromResult(0); // ...
  22. // ... private async Task PullTweets() { var sinceId =

    cache.Get<long>("sinceId"); var tweets = await twitterApi.Search("#RigaDevDays", sinceId); var tweetStrings = new StringBuilder(); foreach (var tweet in tweets) { tweetStrings.AppendLine($"{tweet.Date} {tweet.Text} {tweet.User}"); } await File.AppendAllTextAsync("tweets.csv", tweetStrings.ToString()); } }
  23. ApiController attribute • Automagically responds with 400 for validation errors

    • Smarter bindings • [FromBody]  Complex types • [FromRoute]  Route values • [FromQuery]  Query params • Requires Attribute Routing
  24. public class Numbers { [Required] public int? Number1 { get;

    set; } [Required, Range(1,100)] public int? Number2 { get; set; } } [Route("api/[controller]")] [ApiController] public class CalculatorController : ControllerBase { public ActionResult<int> Post([Required]Numbers numbers, string key) { if (key != "SecretKey") return Unauthorized(); return (numbers.Number1 * numbers.Number2); } }
  25. { "errors": { "Number1": [ "The Number1 field is required.“

    ], "Number2": [ "The field Number2 must be between 1 and 100.“ ] }, "type": "https://api.awesome.io", "title": "One or more validation errors occurred.", "status": 400, "detail": "Go read the docs, duh!", "instance": "/api/calculator" }
  26. public void ConfigureServices(IServiceCollection services) { services.Configure<ApiBehaviorOptions>(options => { options.InvalidModelStateResponseFactory =

    context => { var problemDetails = new ValidationProblemDetails(context.ModelState) { Instance = context.HttpContext.Request.Path, Status = StatusCodes.Status400BadRequest, Type = "https://api.awesome.io", Detail = "Go read the docs, duh!" }; return new BadRequestObjectResult(problemDetails) { ContentTypes = { "application/json+problem" } }; }; }); }K
  27. // GET api/people/names namespace Api.People { [Route("api/people/names")] public class NamesController

    { public string[] Get([FromServices]IPeopleRepository people) { return people.All; } } }
  28. Convention definitions public interface IApplicationModelConvention { void Apply(ApplicationModel application); }

    public interface IControllerModelConvention { void Apply(ControllerModel controller); } public interface IActionModelConvention { void Apply(ActionModel action); }
  29. // GET api/people/names namespace Api.People { public class NamesApi {

    public string[] Get([FromServices]IPeopleRepository people) { return people.All; } } }
  30. Load external MVC during runtime // Registration public void ConfigureServices(IServiceCollection

    services) { var assembly = Assembly.LoadFile(@"c:\random\awesome.dll"); services .AddMvc() .AddApplicationPart(assembly); // ... }
  31. HATEOAS Client Server application/json+hateoas [ { "_self" : "OPTIONS /"

    } { "list-foo" : "GET /api/foo" }, { "list-bar" : "GET /api/bar" }, { "list-baz" : "GET /api/baz" } ]
  32. HATEOAS Client Server [ { "_self" : "GET /api/bar" }

    { "create-bar" : "POST /api/bar" }, { "data" : { ... } } ] application/json+hateoas
  33. HATEOAS Client Server [ { "_self" : "GET /api/bar/1" },

    { "remove-bar" : "DELETE /api/bar/1" }, { "update-bar" : "PUT /api/bar/1" }, { "list-bar" : "GET /api/bar" }, { "data" : { ... } } ] application/json+hateoas
  34. HATEOAS Client Server [ { "_self" : "OPTIONS /api/bar" },

    { "create-bar" : "POST /api/bar" }, { "list-bar" : "GET /api/bar" } ] application/json+hateoas