Skip to content

Commit

Permalink
Resolved #650: Document how to create API controllers from applicatio…
Browse files Browse the repository at this point in the history
…n services conventionally.
  • Loading branch information
hikalkan committed Dec 21, 2018
1 parent 6521f9b commit a8e38ff
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 12 deletions.
138 changes: 138 additions & 0 deletions docs/en/AspNetCore/Auto-API-Controllers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Auto API Controllers

Once you create an [application service](Application-Services.md), you generally want to create an API controller to expose this service as an HTTP (REST) API endpoint. A typical API controller does nothing but redirects method calls to the application service and configures the REST API using attributes like [HttpGet], [HttpPost], [Route]... etc.

ABP can **automagically** configures your application services as MVC API Controllers by convention. Most of time you don't care about its detailed configuration, but it's possible fully customize it.

## Configuration

Basic configuration is simple. Just configure `AbpAspNetCoreMvcOptions` and use `ConventionalControllers.Create` method as shown below:

````csharp
[DependsOn(BookStoreApplicationModule)]
public class BookStoreWebModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers.Create(typeof(BookStoreApplicationModule).Assembly);
});
}
}
````

This example code configures all the application services in the assembly containing the class `BookStoreApplicationModule`. The figure below shows the resulting API on the [Swagger UI](https://swagger.io/tools/swagger-ui/).

![bookstore-apis](../images/bookstore-apis.png)

### Examples

Some example method names and the corresponding routes calculated by convention:

| Service Method Name | HTTP Method | Route |
| ----------------------------------------------------- | ----------- | -------------------------- |
| GetAsync(Guid id) | GET | /api/app/book/{id} |
| GetListAsync() | GET | /api/app/book |
| CreateAsync(CreateBookDto input) | POST | /api/app/book |
| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} |
| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} |
| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors |
| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor |

### HTTP Method

ABP uses a naming convention while determining the HTTP method for a service method (action):

- **Get**: Used if the method name starts with 'GetList', 'GetAll' or 'Get'.
- **Put**: Used if the method name starts with 'Put' or 'Update'.
- **Delete**: Used if the method name starts with 'Delete' or 'Remove'.
- **Post**: Used if the method name starts with 'Create', 'Add', 'Insert' or 'Post'.
- **Patch**: Used if the method name starts with 'Patch'.
- Otherwise, **Post** is used **by default**.

If you need to customize HTTP method for a particular method, then you can use one of the standard ASP.NET Core attributes ([HttpPost], [HttpGet], [HttpPut]... etc.). This requires to add [Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core) nuget package to your project that contains the service.

### Route

Route is calculated based on some conventions:

* It always starts with '**/api**'.
* Continues with a **route path**. Default value is '**/app**' and can be configured as like below:

````csharp
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers
.Create(typeof(BookStoreApplicationModule).Assembly, opts =>
{
opts.RootPath = "volosoft/book-store";
});
});
````

Then the route for getting a book will be '**/api/volosoft/book-store/book/{id}**'. This sample uses two-level root path, but you generally use a single level depth.

* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **camelCase**. If your application service class name is 'BookAppService' then it becomes only '/book'.
* If you want to customize naming, then set the `UrlControllerNameNormalizer` option. It's a func delegate which allows you to determine the name per controller/service.
* If the method has an '**id**' parameter then it adds '**/{id}**' ro the route.
* Then it adds the action name if necessary. Action name is obtained from the method name on the service and normalized by;
* Removing '**Async**' postfix. If the method name is 'GetPhonesAsync' then it becomes 'GetPhones'.
* Removing **HTTP method prefix**. 'GetList', 'GetAll', 'Get', 'Put', 'Update', 'Delete', 'Remove', 'Create', 'Add', 'Insert', 'Post' and 'Patch' prefixes are removed based on the selected HTTP method. So, 'GetPhones' becomes 'Phones' since 'Get' prefix is a duplicate for a GET request.
* Converting the result to **camelCase**.
* If the resulting action name is **empty** then it's not added to the route. If it's not empty, it's added to the route (like '/phones'). For 'GetAllAsync' method name it will be empty, for 'GetPhonesAsync' method name is will be 'phones'.
* Normalization can be customized by setting the `UrlActionNameNormalizer` option. It's an action delegate that is called for every method.
* If there is another parameter with 'Id' postfix, then it's also added to the route as the final route segment (like '/phoneId').

## Service Selection

Creating conventional HTTP API controllers are not unique to application services actually.

### IRemoteService Interface

If a class implements the `IRemoteService` interface then it's automatically selected to be a conventional API controller. Since application services inherently implement it, they are considered as natural API controllers.

### RemoteService Attribute

`RemoteService` attribute can be used to mark a class as a remote service or disable for a particular class that inherently implements the `IRemoteService` interface. Example:

````csharp
[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)]
public class PersonAppService : ApplicationService
{

}
````

### TypePredicate Option

You can further filter classes to become an API controller by providing the `TypePedicate` option:

````csharp
services.Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers
.Create(typeof(BookStoreApplicationModule).Assembly, opts =>
{
opts.TypePredicate = type => { return true; };
});
});
````

Instead of returning `true` for every type, you can check it and return `false` if you don't want to expose this type as an API controller.

## API Explorer

API Exploring a service that makes possible to investigate API structure by the clients. Swagger uses it to create a documentation and test UI for an endpoint.

API Explorer is automatically enabled for conventional HTTP API controllers by default. Use `RemoteService` attribute to control it per class or method level. Example:

````csharp
[RemoteService(IsMetadataEnabled = false)]
public class PersonAppService : ApplicationService
{

}
````

Disabled `IsMetadataEnabled` which hides this service from API explorer and it will not be discoverable. However, it still can be usable for the clients know the exact API path/route.
2 changes: 1 addition & 1 deletion docs/en/Index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Easiest way to start a new project with ABP is to use the Startup templates:

* [ASP.NET Core MVC Template](Getting-Started-AspNetCore-MVC-Template.md)

If you want to start from scratch (with an empty project) then manually install the ABP framework, use following tutorials:
If you want to start from scratch (with an empty project) then manually install the ABP framework, use the following tutorials:

* [Console Application](Getting-Started-Console-Application.md)
* [ASP.NET Core Web Application](Getting-Started-AspNetCore-Application.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/en/Tutorials/AspNetCore-Mvc/Part-I.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ namespace Acme.BookStore

You normally create **Controllers** to expose application services as **HTTP API** endpoints. Thus allowing browser or 3rd-party clients to call them via AJAX.

ABP can **automagically** configures your application services as MVC API Controllers by convention.
ABP can [**automagically**](../../AspNetCore/Auto-API-Controllers.md) configures your application services as MVC API Controllers by convention.

#### Swagger UI

Expand Down
8 changes: 7 additions & 1 deletion docs/en/docs-nav.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,13 @@
"text": "ASP.NET Core MVC",
"items": [
{
"text": "API Versioning"
"text": "API",
"items": [
{
"text": "Auto API Controllers",
"path": "AspNetCore/Auto-API-Controllers.md"
}
]
},
{
"text": "User Interface",
Expand Down
Binary file added docs/en/images/bookstore-apis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,11 @@ protected virtual string GetRootPathOrDefault(Type controllerType)
var controllerSetting = GetControllerSettingOrNull(controllerType);
if (controllerSetting?.RootPath != null)
{
return GetControllerSettingOrNull(controllerType)?.RootPath;
return controllerSetting.RootPath;
}

var areaAttribute = controllerType.GetCustomAttributes().OfType<AreaAttribute>().FirstOrDefault();
if (areaAttribute.RouteValue != null)
if (areaAttribute?.RouteValue != null)
{
return areaAttribute.RouteValue;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Versioning;
using Volo.Abp.Application.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Volo.Abp.Reflection;

namespace Volo.Abp.AspNetCore.Mvc.Conventions
Expand All @@ -17,7 +16,7 @@ public class ConventionalControllerSetting
public Assembly Assembly { get; }

[NotNull]
public HashSet<Type> ControllerTypes { get; }
public HashSet<Type> ControllerTypes { get; } //TODO: Internal?

[NotNull]
public string RootPath
Expand Down

0 comments on commit a8e38ff

Please sign in to comment.