Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Beautiful REST API design with ASP.NET Core and Ion

License

NotificationsYou must be signed in to change notification settings

nbarbettini/BeautifulRestApi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

79 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hello! 👋 This repository contains an example API written in C# and ASP.NET Core 1.1. It uses theIon hypermedia specification as a starting point to model a consistent, clean REST API that embraces HATEOAS.

I use this example in my talkBuilding beautiful RESTful APIs with ASP.NET Core (follow the link to download the slides).

Deep dive video course

If you want afour-hour deep dive on REST, HATEOAS, Ion, API security, ASP.NET Core, and much more, check out my courseBuilding and Securing RESTful APIs in ASP.NET Core on LinkedIn Learning.

It covers everything in this example repository and a lot more. (If you don't have a LinkedIn Learning or Lynda subscription, send me an e-mail and I'll give you a coupon!)

Testing it out

  1. Clone this repository
  2. Build the solution using Visual Studio, or on thecommand line withdotnet build.
  3. Run the project. The API will start up onhttp://localhost:50647, orhttp://localhost:5000 withdotnet run.
  4. Use an HTTP client likePostman orFiddler toGET http://localhost:50647.
  5. HATEOAS
  6. Profit! 💰

Techniques for building RESTful APIs in ASP.NET Core

This example contains a number of tricks and techniques I've learned while building APIs in ASP.NET Core. If you have any suggestions to make it even better, let me know!

Entity Framework Core in-memory for rapid prototyping

Thein-memory provider in Entity Framework Core makes it easy to rapidly prototype without having to worry about setting up a database. You can build and test against a fast in-memory store, and then swap it out for a real database when you're ready.

With theMicrosoft.EntityFrameworkCore.InMemory package installed, create aDbContext:

publicclassApiDbContext:DbContext{publicApiDbContext(DbContextOptions<ApiDbContext>options):base(options){}// DbSets...}

The only difference between this and a "normal"DbContext is the addition of a constructor that takes aDbContextOptions<> parameter. This is required by the in-memory provider.

Then, wire up the in-memory provider inStartup.ConfigureServices:

services.AddDbContext<ApiDbContext>(options=>{// Use an in-memory database with a randomized database name (for testing)options.UseInMemoryDatabase(Guid.NewGuid().ToString());});

The database will be empty when the application starts. To make prototyping and testing easy, you can add test data inStartup.cs:

// In Configure()vardbContext=app.ApplicationServices.GetRequiredService<ApiDbContext>();AddTestData(dbContext);privatestaticvoidAddTestData(ApiDbContextcontext){context.Conversations.Add(newModels.ConversationEntity{Id=Guid.Parse("6f1e369b-29ce-4d43-b027-3756f03899a1"),CreatedAt=DateTimeOffset.UtcNow,Title="Who is the coolest Avenger?"});// Make sure you save changes!context.SaveChanges();}

Model Ion links, resources, and collections

Ion provides a simple framework for describing REST objects in JSON. These Ion objects can be modeled as POCOs in C#. Here's a Link object:

publicclassLink{publicstringHref{get;set;}// Since ASP.NET Core uses JSON.NET by default, serialization can be// fine-tuned with JSON.NET attributes[JsonProperty(NullValueHandling=NullValueHandling.Ignore,DefaultValueHandling=DefaultValueHandling.Ignore)][DefaultValue(GetMethod)]publicstringMethod{get;set;}[JsonProperty(PropertyName="rel",NullValueHandling=NullValueHandling.Ignore)]publicstring[]Relations{get;set;}}

Modeling resources and collections is drop-dead simple:

// Resources are also (self-referential) linkspublicabstractclassResource:Link{// Rewritten using LinkRewritingFilter during the response pipeline[JsonIgnore]publicLinkSelf{get;set;}}// Collections are also resourcespublicclassCollection<T>:Resource{publicconststringCollectionRelation="collection";publicT[]Value{get;set;}}

These base classes make returning responses from the API nice and clean.

Basic API controllers and routing

API controllers in ASP.NET Core inherit from theController class and use attributes to define routes. The common pattern is naming the controller<RouteName>Controller, and using the/[controller] attribute value, which automatically names the route based on the controller name:

// Handles all routes under /comments[Route("/[controller]")]publicclassCommentsController:Controller{// Action methods...}

Methods in the controller handle specific HTTP verbs and sub-routes. ReturningIActionResult gives you the flexibility to return both HTTP status codes and object payloads:

// Handles route:// GET /comments[HttpGet]publicasyncTask<IActionResult>GetCommentsAsync(CancellationTokenct){returnNotFound();// 404returnOk(data);// 200 with JSON payload}// Handles route:// GET /comments/{commentId}// and {commentId} is bound to the argument in the method signature[HttpGet("{commentId}"]public asyncTask<IActionResult>GetCommentByIdAsync(GuidcommentId,CancellationTokenct){// ...}

Named routes pattern

If you need to refer to specific routes later in code, you can use theName property in the route attribute to provide a unique name. I like usingnameof to name the routes with the same descriptive name as the method itself:

[HttpGet(Name=nameof(GetCommentsAsync))]publicasyncTask<IActionResult>GetCommentsAsync(CancellationTokenct){// ...}

This way, the compiler will make sure route names are always correct.

Async/await best practices

ASP.NET Core supports async/await all the way down the stack. Any controllers or services that make network or database calls should beasync. Entity Framework Core provides async versions of database methods likeSingleAsync andToListAsync.

Adding aCancellationToken parameter to your route methods allows ASP.NET Core to notify your asynchronous tasks of a cancellation (if the browser closes a connection, for example).

Keep controllers lean

I like keeping controllers as lean as possible, by only concerning them with:

  • Validating model binding (or not, see below!)
  • Checking for null, returning early
  • Orchestrating requests to services
  • Returning nice results

Notice the lack of business logic! Keeping controllers lean makes them easier to test and maintain. Lean controllers fit nicely into more complex patterns like CQRS or Mediator as well.

Validate model binding with an ActionFilter

Most routes need to make sure the input values are valid before proceeding. This can be done in one line:

if(!ModelState.IsValid)returnBadRequest(ModelState);

Instead of having this line at the top of every route method, you can factor it out toan ActionFilter which can be applied as an attribute:

[HttpGet(Name=nameof(GetCommentsAsync))][ValidateModel]publicasyncTask<IActionResult>GetCommentsAsync(...)

TheModelState dictionary contains descriptive error messages (especially if the models are annotated withvalidation attributes). You could return all of the errors to the user, or traverse the dictionary to pull out the first error:

varfirstErrorIfAny=modelState.FirstOrDefault(x=>x.Value.Errors.Any()).Value?.Errors?.FirstOrDefault()?.ErrorMessage

Provide a root route

It's not HATEOAS unless the API has a clearly-defined entry point. The root document can be defined as a simple resource of links:

publicclassRootResource:Resource{publicLinkConversations{get;set;}publicLinkComments{get;set;}}

And returned from a controller bound to the/ route:

[Route("/")]publicclassRootController:Controller{[HttpGet(Name=nameof(GetRoot))]publicIActionResultGetRoot(){// return Ok(new RootResponse...)}}

Serialize errors as JSON

A JSON API is expected to return exceptions or API errors as JSON. It's possible to write anexception filter that will serialize all MVC errors to JSON, but some errors occur outside of the MVC pipeline and won't be caught by an exception filter.

Instead, exception-handling middleware can be added inStartup.Configure:

varjsonExceptionMiddleware=newJsonExceptionMiddleware(app.ApplicationServices.GetRequiredService<IHostingEnvironment>());app.UseExceptionHandler(newExceptionHandlerOptions{ExceptionHandler=jsonExceptionMiddleware.Invoke});

PassingIHostingEnvironment to the middleware makes it possible to send more detailed information during development, but send a generic error message in production. The middleware is implemented inJsonExceptionMiddleware.cs.

Generate absolute URLs automatically with a filter

TheController base class provides an easy way to generate protocol- and server-aware absolute URLs withUrl.Link(). However, if you need to generate these links outside of a controller (such as in service code), you either need to pass around theIUrlHelper or find another way.

In this project, theLink class represents an absolute link to another resource or collection. The derivedRouteLink class can stand in (temporarily) as a placeholder that contains just a route name, and at the very end of the response pipeline theLinkRewritingFilter enerates the absolute URL. (Filters have access toIUrlHelper, just like controllers do!)

Map resources using AutoMapper

It's a good idea to keep the classes that represent database entities separate from the classes that model what is returned to the client. (In this project, for example, theCommentEntity class contains anId and other properties that aren't directly exposed to the client inCommentResource.)

A object mapping library likeAutoMapper saves you from manually mapping properties from entity classes to resource classes. AutoMapper integrates with ASP.NET Core easily with theAutoMapper.Extensions.Microsoft.DependencyInjection package and aservices.AddAutoMapper() line inConfigureServices. Most properties are mapped automatically, and you can define amapping profile for custom cases.

AutoMapper plays nice with Entity Framework Core and async LINQ, too:

varitems=awaitquery// of CommentEntity.Skip(pagingOptions.Offset.Value).Take(pagingOptions.Limit.Value).ProjectTo<CommentResource>()// lazy mapping!.ToArrayAsync(ct);// of CommentResource

Use strongly-typed route parameter classes

If you look at the AutoMappermapping profile from above, you'll notice that strongly-typed route values are saved in each Link:

Link.To(nameof(ConversationsController.GetConversationByIdAsync),newGetConversationByIdParameters{ConversationId=src.Id})

(You'll also notice a practical use of thenamed routes pattern!)

When the URL of the link is generated (later), the provided route values are matched up with the route method definition. TheRouteValues property is just anobject, so you could pass an anonymous object instead:

Link.To(nameof(ConversationsController.GetConversationByIdAsync),new{conversationId=src.Id})

However, defining asimple POCO makes this type-safe and foolproof:

publicclassGetConversationByIdParameters{[FromRoute]publicGuidConversationId{get;set;}}

Instead of defining the parameters directly in the method signature, like this:

[HttpGet("{conversationId}",Name=nameof(GetConversationByIdAsync))]publicasyncTask<IActionResult>GetConversationByIdAsync(GuidconversationId,CancellationTokenct)

The method signature contains the POCO itself:

[HttpGet("{conversationId}",Name=nameof(GetConversationByIdAsync))]publicasyncTask<IActionResult>GetConversationByIdAsync(GetConversationByIdParametersparameters,CancellationTokenct)

Consume application configuration in services

ASP.NET Core comes with a powerfulconfiguration system that you can use to provide dynamic configuration data to your API. You can define a group of properties inappsettings.json:

"DefaultPagingOptions": {"limit":25,"offset":0}

And bind that group to a POCO inConfigureServices:

services.Configure<Models.PagingOptions>(Configuration.GetSection("DefaultPagingOptions"));

This places the POCO in the services (DI) container as a singleton wrapped inIOptions<>, which can be injected in controllers and services:

privatereadonlyPagingOptions_defaultPagingOptions;publicCommentsController(ICommentService commentService,IOptions<PagingOptions> defaultPagingOptionsAccessor){// ..._defaultPagingOptions=defaultPagingOptionsAccessor.Value;}

Add paging to collections

Collections with more than a few dozen items start to become heavy to send over the wire. By enriching theCollection<T> class with paging metadata, the client can get a paged collection experience complete with HATEOAS navigation links:

{"href":"http://api.foo.bar/comments","rel": ["collection" ],"offset":0,"limit":25,"size":200,"first": {"href":"http://api.foo.bar/comments","rel": ["collection" ] },"next": {"href":"http://api.foo.bar/comments?limit=25&offset=25","rel": ["collection" ] },"last": {"href":"http://api.foo.bar/comments?limit=25&offset=175","rel": ["collection"] },"value": ["items..."    ]}

In this project, theCollectionWithPaging{T} class handles the logic and math behind the scenes. Controllers that return collections accept a[FromQuery] PagingOptions parameter that binds to thelimit andoffset parameters needed for paging.

Paging using a limit and offset is a stateless approach to paging. You could implement paging statefully (using a cursor) instead by saving a cursor position in the database.

More to come...

About

Beautiful REST API design with ASP.NET Core and Ion

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors2

  •  
  •  

Languages


[8]ページ先頭

©2009-2025 Movatter.jp