Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Download as pdf or txt
Download as pdf or txt
You are on page 1of 63

ASP.

NET Core
Web API Best Practices
STEVE SMITH
ARDALIS.COM | @ARDALIS | STEVE@ARDALIS.COM
WEEKLYDEVTIPS.COM (PODCAST)

Web API Best Practices - @ardalis


Learn More After Today
1) DevIQ
◦ ASP.NET Core Quick Start http://aspnetcorequickstart.com

2) Microsoft FREE eBook/Sample App


◦ eShopOnWeb eCommerce Sample https://ardalis.com/architecture-ebook

3) Weekly Dev Tips Podcast / Newsletter


◦ http://ardalis.com/tips

4) Contact me for mentoring/training for your company/team


◦ http://ardalis.com

Web API Best Practices - @ardalis


Web API Design

Web API Best Practices - @ardalis


Representational State Transfer (REST)
"An architectural style for building distributed systems based on hypermedia"

Open standards-based

Technology-agnostic

Client issues a request to a URI that represents a resource;


◦ Request verb that indicates the operation to perform on the resource.
◦ Request body includes the data required for the operation.

REST-based APIs are stateless; each request may be handled by a different server-side resource

https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design

Web API Best Practices - @ardalis


URI Design Considerations
URI values should correspond to nouns
◦ E.g. /customers, /authors, /orders

URI values should typically be plural (when referring to collections)


◦ Again, /customers, /authors, /orders

Requests for individual resources should append an identifier:


◦ Example: /customers/1, /orders/00234

Web API Best Practices - @ardalis


Principle of Least Astonishment
Try not to surprise your client with how your API works!

Keep it SIMPLE

Keep it CONSISTENT

Web API Best Practices - @ardalis


Don’t Expose Business/Data Model
Avoid coupling your Web API directly to your data/domain model

API design, including URIs, may not map 1:1 to domain objects or database tables.

Example:
POST /orders
May map to a NewOrderRequest on the server that triggers processing payment, checking
inventory, sending notifications, etc.
Or it could just insert a record in the Orders table.
It should be able to do either without the API changing.

Web API Best Practices - @ardalis


Use a standard URI structure for
subcollections
For performance or other reasons, API might not return full object tree with root-level request:
GET /customers/1
{
"id":"1",
"name": "Steve Smith"
}
To get the customer’s orders:
GET /customers/1/orders
[{"id":"123","customerId":"1", …}, {"id":"234","customerId":"1", …}]

Web API Best Practices - @ardalis


Avoid Deeply Nested URI Structures
OK
/customers
/customers/1
/customers/1/orders

TOO MUCH
/customers/1/orders/123 (instead: /orders/123)
/customers/1/orders/123/items/1/products/2 (instead: /products/2)

Web API Best Practices - @ardalis


Hypermedia as the Engine of Application
State (HATEOAS)
Less commonly implemented aspect of REST approach
Currently no widely used standard
Basic idea: Each response includes links defining available requests on a given resource

Example:
GET /customers/1
Response includes customer data, as well as links to:
Update the customer Delete the customer List customer orders
List customer addresses Add an address Add an order

Web API Best Practices - @ardalis


HATEOAS Example - RFC 5988 (web linking)
GET /customers/1
RESPONSE:
{
"id":"1",
"name": "Steve Smith",
"links" : [
"href" : "1/orders",
"rel" : "orders",
"type" : "GET"
]
} https://restfulapi.net/hateoas/

Web API Best Practices - @ardalis


Standard Verbs and Behaviors
GET Fetch a resource (or collection of resources)
PUT Update a resource.
POST Create a new resource.
DELETE Delete a resource.

Web API Best Practices - @ardalis


Safe and Idempotent API Requests
Safe requests are requests that do not change resources, and which can be made repeatedly
without impact. Think of safe requests as read-only operations.

An idempotent HTTP method can be called multiple times without changing the expected
response.

Are these the same?

Web API Best Practices - @ardalis


HTTP Verb Idempotency/Safety
VERB Idempotent? Safe?
GET Yes Yes
PUT Yes No
POST No No
DELETE Yes* No

*Decide if a DELETE for a missing id should return a 404 or not. If it returns 404, it won’t be Idempotent.

http://restcookbook.com/HTTP%20Methods/idempotency/

Web API Best Practices - @ardalis


Web API
Implementation

Web API Best Practices - @ardalis


Use Model Validation (up to 2.0)
Always check if Model.IsValid before performing unsafe operations

Web API Best Practices - @ardalis


Use Filters To Represent Policies
Validate Model State using a filter (globally, per-controller, or per-action)

Web API Best Practices - @ardalis


Use [ApiController] in 2.1+

Performs model validation automatically


Requires attribute routing

Recommendation: Add to a base API controller class


Alternately in 2.2 can be applied to Startup for whole project

Web API Best Practices - @ardalis


Base Class
Inherit from ControllerBase, not Controller

Create your own BaseApiController like this one (consider also adding Authorize
filter):

Web API Best Practices - @ardalis


Use Proper HTTP Status Codes as Results
200 OK Request was successful; body has response.
201 OK POST or PUT was successful; body has latest representation.
204 OK DELETE was successful; resource was deleted.
400 BAD REQUEST The request was invalid or cannot otherwise be served.
401 UNAUTHORIZED Authorization failed or authentication details not supplied.
404 NOT FOUND The URI requested or the resource requested doesn’t exist.
500 Internal Server Error Something very bad happened. Unhandled exceptions lead to this.

Web API Best Practices - @ardalis


Web API Conventions
Scenario Expected Response

Get Object with ID – Object Found 200 OK

Get Object with ID – No such object 404 Not Found

Create Object – Object is valid 200 OK

Create Object – Object is invalid 400 Bad Request

Update Object – Object exists and new object is valid 200 OK

Update Object – No such object 404 Not Found

Update Object – New object is invalid 400 Bad Request

Delete Object with ID – Deletion successful 200 OK

Delete Object with ID – No such object 404 Not Found or 200 OK

Web API Best Practices - @ardalis


Prefer NotFound to
NullReferenceException

Web API Best Practices - @ardalis


Prefer NotFound to
NullReferenceException

Web API Best Practices - @ardalis


Use a filter to confirm existence

Web API Best Practices - @ardalis


Avoid Duplicating Data within Requests
Don’t ask for an ID in the route and also in the BindingModel
◦ Unless you’re going to allow updates to a resource’s ID!

Which value should you use? How do you decide?


◦ Best to use a model type that doesn’t include the ID if it’s redundant

Web API Best Practices - @ardalis


Use DTOs Appropriately
Avoid using domain objects or data entities as your API inputs or outputs.
◦ Doing so exposes your app’s internal state and can be a security risk

Avoid creating DTO types that inadvertently reference non-DTO types.


◦ Tip: Look for using statements in your DTO files that shouldn’t be there

If specifying ID on DTOs, may not make sense to use for new object requests (POSTs)
◦ Consider having separate NewResourceDTO and ResourceDTO types
◦ ResourceDTO can inherit from NewResourceDTO and simply add the Id property

Web API Best Practices - @ardalis


Entity Class – Used in POST Action
public class GuestbookEntry : BaseEntity
{
public string EmailAddress { get; set; }
public string Message { get; set; }
public DateTimeOffset DateTimeCreated { get; set; } = DateTime.UtcNow;
}

Intended behavior is only EmailAddress and Message are sent by post.

Web API Best Practices - @ardalis


Security Issue: Data Object for API

Web API Best Practices - @ardalis


Example: Create/Update DTOs
public class NewCustomerDTO
{
public string Name { get; set; }
public string EmailAddress { get; set; }
}
public class CustomerDTO : NewCustomerDTO
{
public int Id { get; set; }
}

Web API Best Practices - @ardalis


Non-DTOs May Expose Sensitive Data

Web API Best Practices - @ardalis


Post-Redirect-Get (PRG) Pattern
Overview
◦ Client POSTs to Server
◦ Server performs requested operation and returns a Redirect (302) to new URI
◦ Client GETs new URI

This pattern is most appropriate to MVC non-API apps.


◦ One of its primary benefits is that it eliminates browser refreshes from reissuing POST commands.
◦ Not generally an issue with Web APIs

REST services should (typically) return the resource in the body of POST commands, not
redirect

Web API Best Practices - @ardalis


What to Return?
Object
◦ Author, Customer, or void
◦ Automatically wrapped in a result (or

Encoding-Specific
◦ return Json(model); // JsonResult

IActionResult (2.0 and earlier) or ActionResult<T> (v2.1+)


◦ return Ok(model);
◦ return NotFound();
◦ return BadRequest();

Web API Best Practices - @ardalis


Prefer IActionResult or ActionResult<T>
Support Content Negotiation
Requests can include Accept header specifying content they want/support

Web API will attempt to comply with specified content format

Support JSON (default) and XML:


Add XML Serializers when adding MVC in ConfigureServices:
services.AddMvc()
.AddXmlSerializerFormatters();

Web API Best Practices - @ardalis


Content Negotiation In Action

Web API Best Practices - @ardalis


Content Negotiation In Action

Web API Best Practices - @ardalis


Documentation / Discoverability
Swagger http://swagger.io
Now the OpenAPI Specification

Provide live, runtime documentation of your APIs

Ability to generate client libraries to assist in consuming your API


◦ NSwag - https://github.com/RSuter/NSwag

Web API Best Practices - @ardalis


Adding Swagger to your Web API
Add Nuget package Swashbuckle.AspNetCore
Add Services in ConfigureServices:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
});

Add Middleware to Configure() (next slide)

Web API Best Practices - @ardalis


Adding Swagger to your Web API (cont.)
public void Configure(IApplicationBuilder app)
{
app.UseSwagger(); // Enable middleware to serve generated Swagger as a JSON
endpoint.

// Enable middleware to serve swagger-ui specifying the Swagger JSON


endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseMvc();
}

Web API Best Practices - @ardalis


Response Types Produced

Web API Best Practices - @ardalis


Analyzers

Web API Best Practices - @ardalis


NSwag Studio

Web API Best Practices - @ardalis


Demo
WORKING WITH SWAGGER

Web API Best Practices - @ardalis


Testing Web APIs

Web API Best Practices - @ardalis


Kinds of Tests
Unit Tests
◦ Test a single unit – typically a method
◦ Only test your code, not infrastructure
◦ Limited usefulness for testing APIs

Integration Tests
◦ Test several methods and/or classes working together
◦ Useful for verifying infrastructure code works correctly

Functional Tests
◦ Test full application stack
◦ Slowest, often most brittle, but provide greatest confidence a particular user scenario works fully

Web API Best Practices - @ardalis


Web API Best Practices - @ardalis
Web API Best Practices - @ardalis
Test APIs with TestServer
Install Microsoft.AspNetCore.TestHost Nuget Package
Configure with WebHostBuilder; use HttpClient to make requests to TestServer instance

Learn more:
https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.2

Web API Best Practices - @ardalis


Example Web API Test

Web API Best Practices - @ardalis


Demo
VALIDATING FILTERS PRODUCE SAME RESULTS AS INLINE CODE

Web API Best Practices - @ardalis


Versioning Web APIs

Web API Best Practices - @ardalis


No Versioning
Limit updates to non-destructive wherever possible

Coordinate with clients on breaking changes

Works best with internal APIs

Web API Best Practices - @ardalis


URI Versioning
Example: api.domain.com/v2/customers/1
Previous versions work as before
Results in multiple URIs corresponding to same resource
Can complicate HATEOAS links
Can be unwieldy if API evolves quickly/frequently

Web API Best Practices - @ardalis


Querystring Versioning
Example: api.domain.com/customers/1?ver=2
Previous versions work as before (default to 1 if omitted)
Can complicate HATEOAS links
Can be unwieldy if API evolves quickly/frequently

Web API Best Practices - @ardalis


Header Versioning
Example: GET api.domain.com/customers/1
Version-Header: 2
Previous versions work as before (default to 1 if omitted)
HATEOAS links must use same header
Can be unwieldy if API evolves quickly/frequently

Web API Best Practices - @ardalis


Media Type Versioning
Example: GET api.domain.com/customers/1
Accept: vnd.domain.v2+json
Response includes header indicating version provided
Previous versions work as before (default to 1 if omitted)
Works well with HATEOAS links (can include MIME types)

Web API Best Practices - @ardalis


Versioning Considerations
Consider performance impact, especially for web server and proxy server caching.
◦ Header and Media Type versioning is less cache friendly than other techniques

Consider whether you will version your entire API (simplest) or resource by resource (generally
not recommended).

Avoid making breaking changes to your API as much as possible. No versioning option is
without its problems.

Web API Best Practices - @ardalis


Securing Web APIs

Web API Best Practices - @ardalis


Use HTTPS
(seriously, just use it)

Web API Best Practices - @ardalis


Windows Auth
Simplest

Well-known

Only works on Windows and within an intranet

Web API Best Practices - @ardalis


IdentityServer 4
An OpenID Connect and OAuth 2.0 framework for ASP.NET Core 2.x
Separate Authentication Service
Single Sign-On Support
Access Control for APIs, including tokens for:
◦ Server-to-Server clients
◦ Web clients and SPAs
◦ Native/Mobile apps

Free, Open Source, Part of .NET Foundation


Learn more: http://docs.identityserver.io/en/release/

Web API Best Practices - @ardalis


Web Tokens (JWT)
Roll your own using available packages:
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.1.4" />

Great article on this topic


◦ http://www.blinkingcaret.com/2017/09/06/secure-web-api-in-asp-net-core/

Steps
◦ Authenticate user and issue token. Store in client (local storage for browser).
◦ Add token in header on subsequent requests
◦ Validate token on server using middleware; return 401 if not valid

Web API Best Practices - @ardalis


JWT Demo

Web API Best Practices - @ardalis


Resources
Blog and Podcast
◦ Ardalis.com
◦ WeeklyDevTips.com

Online Courses (Pluralsight and DevIQ)


• SOLID Principles of OO Design https://www.pluralsight.com/authors/steve-smith
• N-Tier Architecture in C#
• DDD Fundamentals
• ASP.NET Core Quick Start http://aspnetcorequickstart.com/

Other Resources
• Microsoft Architecture eBook/sample http://aka.ms/WebAppArchitecture
• Securing Web API in ASP.NET Core
• http://www.blinkingcaret.com/2017/09/06/secure-web-api-in-asp-net-core/

Web API Best Practices - @ardalis

You might also like