Skip to content

Controller Conventions

Chris Martinez edited this page Dec 29, 2022 · 3 revisions

There are a few implicit conventions to be aware of.

Every Controller Has an API Version

Once you opt into API versioning, every API controller has an API version. This is true even if the controller does not have an explicit attribute or configured convention. When otherwise unspecified, the version applied to a controller derives from ApiVersioningOptions.DefaultApiVersion.

Naming

ASP.NET provides a built-in convention for controller names that use the form [name]Controller where Controller will be trimmed off when exactly that text. API Versioning slightly expands this convention. It will honor the convention of [name][#]Controller. This allows you to have two controller types in the same namespace for different API versions, but for the same resource; for example, ValuesController and Values2Controller will both have the name Values. Naming is important for grouping controllers together.

Unfortunately, this can cause an issue for service API versioning if you want to split the implementation across different types. If the defining type is in a different .NET namespace, then there is no issue; however, if they are in the same namespace there would be a name collision. For example:

namespace My.Services.V1
{
    [ApiVersion( 1.0 )]
    [Route( "[controller]" )]
    public class HelloWorldController : ControllerBase
    {
        [HttpGet]
        public string Get() => "Hello world v1.0!";
    }
}

namespace My.Services.V2
{
    [ApiVersion( 2.0 )]
    [Route( "[controller]" )]
    public class HelloWorldController : ControllerBase
    {
        [HttpGet]
        public string Get() => "Hello world v2.0!";
    }
}

Controllers separated by .NET namespace

namespace My.Services.Controllers
{
    [ApiVersion( 1.0 )]
    [Route( "[controller]" )]
    public class HelloWorldController : ControllerBase
    {
        [HttpGet]
        public string Get() => "Hello world v1.0!";
    }

    [ApiVersion( 2.0 )]
    [Route( "helloworld" )]
    public class HelloWorld2Controller : ControllerBase
    {
        [HttpGet]
        public string Get() => "Hello world v2.0!";
    }
}

Controllers with different names in the same .NET namespace

To address name collisions and provide control over how collation happens, API Versioning provides the following service:

public interface IControllerNameConvention
{
    string NormalizeName( string controllerName );
    string GroupName( string controllerName );
}

NormalizeName controls how or whether a controller name is normalized. GroupName provides the name used to group and collate on, which may not necessarily be the same as the normalized name. ControllerNameConvention provides three implementations out-of-the-box.

Default

ControllerNameConvention.Default provides the default configuration which extends the original convention to have the form: [Name][#]Controller. This means that if you already have a HelloWorldController, you can now have a HelloWorld2Controller and HelloWorld3Controller. Each type name removes the Controller suffix as well as any trailing numbers. All of these controllers would end up named and grouped HelloWorld.

Original

ControllerNameConvention.Original provides an alternate configuration that retains the original naming convention. Consider that you have a type named S3Controller. In this scenario, you do not want the 3 to be stripped away. If you have multiple versions of a such a controller, you would need your own implementation that understands this behavior or separate the types into different .NET namespaces.

Grouped

ControllerNameConvention.Grouped is a hybrid configuration the combines the Default and Original conventions. For the purposes of the name, the original convention is used. For the purposes of grouping, the default convention is used. A controller type of S3Controller would have the name S3, but the group name S. The group name is only used for collation and is never displayed anywhere, so this behavior is acceptable.

Attribute

If you do not want to rely on a convention, you can explicitly provide a name using the ControllerNameAttribute. The name provided will be used verbatim for the [controller] token, the controller name, and for grouping. This attribute is particularly useful with OData because the name of the controller must also exactly match the name of the associated entity set.

[ApiVersion( 2.0 )]
[ControllerName( "HelloWorld" )]
[Route( "[controller]" )]
public class HelloWorld2Controller : ControllerBase
{
    [HttpGet]
    public string Get() => "Hello world v2.0!";
}

API Controllers

Applies to ASP.NET Core only

A controller is just a controller in ASP.NET Core; there is no distinction between a UI Controller and an API Controller. Some applications mix UI controllers and API controllers together. This will result in all controllers requiring an API version, which is undesirable for UI controllers. The advent of the ApiControllerAttribute made it possible to disambiguate the two types of controllers.

API Versioning 3.0 introduced two new interfaces:

interface IApiControllerFilter
{
    IList<ControllerModel> Apply( IList<ControllerModel> controllers );
}

interface IApiControllerSpecification
{
    bool IsSatisifedBy( ControllerModel controller );
}

The IApiControllerFilter filters which controllers should be considered API controllers. The default implementation typically does not need to be replaced.

The IApiControllerSpecification defines a specification as to whether a particular controller is an API controller.

There are two built-in specifications:

  • ApiBehaviorSpecification - matches controllers decorated by [ApiController]
  • ODataControllerSpecification - matches controllers decorated by [ODataRouting]

An API controller will be considered any controller that matches at least one specification. If a built-in specification does not meet your specific needs, you can create your own:

// considers controllers inheriting from Controller to be a UI controller
public class NonUIControllerSpecification : IApiControllerSpecification
{
    private readonly Type UIControllerType = typeof( Controller ).GetTypeInfo();

    public bool IsSatisfiedBy( ControllerModel controller ) =>
        !UIControllerType.IsAssignableFrom( controller.ControllerType )
}

Register your specification in the services configuration:

services.TryAddEnumerable(
    ServiceDescriptor.Transient<IApiControllerSpecification, NonUIControllerSpecification>() );
Clone this wiki locally