DEV Community

Cover image for Deploy multiple APIs in Azure API management, hosted in the same App service.
Sam Vanhoutte
Sam Vanhoutte

Posted on

Deploy multiple APIs in Azure API management, hosted in the same App service.

We are building an API that contains several controllers with many operations. This API has to be consumed by different client applications.

  • Our own management portal
  • External integration companies
  • A mobile application

While we prefer to keep the code in the same ASP.NET Core Web API project (this is easy for many reasons, of which simplicity may be the biggest one), we don't want to expose every part of functionality to every client.

And this post is describing how we declare our API in such a way that we have multiple logical API specs that we then deploy in Azure API Management as different API's.

The WebAPI project

Enabling nswag in the build action

After creating the new ASP.NET Web API project, we can add the following NSwag packages to our project.

<PackageReference Include="NSwag.AspNetCore" Version="14.0.3" />
<PackageReference Include="NSwag.MSBuild" Version="14.0.3">

Enter fullscreen mode Exit fullscreen mode

These packages will be used to decorate our operations and to generate the OpenAPI spec at build/deploy time.

We also add a build action to generate the open api spec in our deployment/api folder.

    <Target Name="NSwag" AfterTargets="PostBuildEvent" Condition=" '$(Docker)' != 'yes' ">
        <Exec WorkingDirectory="$(ProjectDir)" EnvironmentVariables="ASPNETCORE_ENVIRONMENT=Development;IS_NSWAG_BUILD=true;API_TITLE=Events_Backend_Api;INCLUDE_ALL_OPERATIONS=true" Command="$(NSwagExe_Net80) run nswag-client.json /variables:Configuration=$(Configuration),Output=backend-openapi.json" />
        <Exec WorkingDirectory="$(ProjectDir)" EnvironmentVariables="ASPNETCORE_ENVIRONMENT=Development;IS_NSWAG_BUILD=true;API_TITLE=Events_Public_Api;API_TYPE=Public" Command="$(NSwagExe_Net80) run nswag-spec.json /variables:Configuration=$(Configuration),Output=backend-public-openapi.json" />
    </Target>
Enter fullscreen mode Exit fullscreen mode

When we build our project, the PostBuildEvent actions will be executed and the nswag will be used to use the nswag-spec.json configuration file to output the open-api as json to the configured output file. (/deployment/api in our case).

In the above configuration, we are generating two different api specs:

  • backend-openapi.json: we are passing INCLUDE_ALL_OPERATIONS=true as the argument to the nswag command.
  • backend-public-openapi.json: we are passing API_TYPE=Public as the argument to the nswag command.

In the next steps, I will show how these arguments are being used to filter out the correct operations in the open API spec generation.

Decorating the operations

In order to filter out the correct operations, I have created a custom Attribute that I can use on controllers and operations to indicate in which API output spec they should be included or not.

The custom attribute looks like this:

[AttributeUsage(AttributeTargets.All)]
public class ApiTypeAttribute : Attribute
{
    private readonly string? apiType;
    private readonly bool alwaysInclude;

    public ApiTypeAttribute(string? apiType = null, bool alwaysInclude = false)
    {
        this.apiType = apiType;
        this.alwaysInclude = alwaysInclude;
    }

    public string? ApiType => apiType;
    public bool AlwaysInclude => alwaysInclude;
}
Enter fullscreen mode Exit fullscreen mode

This attribute can then be used and applied to operations and controllers, like in the following example:

using Events.WebAPI.Models;
using Events.WebAPI.Responses;
using Events.WebAPI.Runtime;
using Events.WebAPI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using NSwag.Annotations;

namespace Events.WebAPI.Controllers;

/// <summary>
/// API endpoint that exposes Events functionality
/// </summary>
[ApiController]
[Route("events")]
[ApiType(apiType: ApiTypes.Admin)]
public class EventController(EventsService eventsService): ControllerBase
{
    /// <summary>
    /// Get all events
    /// </summary>
    [HttpGet()]
    [ApiType(apiType: ApiTypes.Public)]
    [SwaggerResponse(StatusCodes.Status200OK, typeof(EventsResponse), Description = "The available events.")]
    public async Task<IActionResult> ListEvents()
    {
        var events = await eventsService.GetEventsAsync();
        return Ok(new EventsResponse(events.ToArray()));
    }

    /// <summary>
    /// Get all events
    /// </summary>
    [HttpPost()]
    [SwaggerResponse(StatusCodes.Status200OK, typeof(void), Description = "The event was created.")]
    public async Task<IActionResult> CreateEvent(Event @event)
    {
        await eventsService.CreateEventAsync(@event);
        return Ok();
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above controller, you can see that by default, the controller and its operations are considered to be part of the "Admin" API. But the ListEvents operation is decorated with the [ApiType(apiType: ApiTypes.Public)] attribute, indicating it's part of the public API as well.

In order to set this up, the Program.cs file contains the following logic, that is being called at the time of nswag-build.

builder.Services.AddOpenApiDocument(document => { GenerateOpenApiSpec(document, "v1"); });

void GenerateOpenApiSpec(AspNetCoreOpenApiDocumentGeneratorSettings nswagSettings, string documentName)
{
    string apiTitle = "Savanh Events API";

    bool includeAllOperations = false;
    string? apiType = null;

    if (!string.IsNullOrEmpty(builder.Configuration["API_TYPE"]))
    {
        apiType = builder.Configuration["API_TYPE"];
    }

    if (!string.IsNullOrEmpty(builder.Configuration["INCLUDE_ALL_OPERATIONS"]))
    {
        _ = bool.TryParse(builder.Configuration["INCLUDE_ALL_OPERATIONS"], out includeAllOperations);
    }

    if (!string.IsNullOrEmpty(builder.Configuration["API_TITLE"]))
    {
        apiTitle = builder.Configuration["API_TITLE"];
        apiTitle = apiTitle.Replace("_", " ");
    }

    Console.WriteLine($"Generating api doc : AllOperations: {includeAllOperations} // ApiType: {apiType}");
    nswagSettings.OperationProcessors.Add(new OpenApiSpecOperationProcessor(includeAllOperations, apiType));
    nswagSettings.DocumentName = documentName;
    nswagSettings.Title = apiTitle;
    nswagSettings.Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
}
Enter fullscreen mode Exit fullscreen mode

In the above code snippet, you can see that the input variables (API_TYPE, INCLUDE_ALL_OPERATIONS, API_TITLE) that are passed in the PostBuild action are being used to apply the behavior for the open API spec generation.

And there is an important line (nswagSettings.OperationProcessors.Add(new OpenApiSpecOperationProcessor(includeAllOperations, apiType));) that passed those configuration values to an OperationProcessor that will then filter out the operation or not. And that Processor is seen in the following snippet.

public class OpenApiSpecOperationProcessor : IOperationProcessor
{
    private readonly bool includeAllOperations;
    private readonly string? apiType;

    public OpenApiSpecOperationProcessor(bool includeAllOperations = false, string? apiType = null)
    {
        this.includeAllOperations = includeAllOperations;
        this.apiType = apiType;
    }

    // This method return a boolean, indicating to include the operation or not in the output
    public bool Process(OperationProcessorContext context)
    {
        if (includeAllOperations)
        {
            return true;
        }

        // Looking for the method or controller to have the ApiType attribute defined

        ApiTypeAttribute? attribute = null;
        if (context.MethodInfo.IsDefined(typeof(ApiTypeAttribute), true))
        {
            // First we check the method (deepest level), as that can override the controller
            attribute = (ApiTypeAttribute?)context.MethodInfo.GetCustomAttribute(typeof(ApiTypeAttribute));
        }
        else
        {
            // If no attribute on the method, we check the controller
            if (context.ControllerType.IsDefined(typeof(ApiTypeAttribute), true))
            {
                attribute = (ApiTypeAttribute?)context.ControllerType.GetCustomAttribute(typeof(ApiTypeAttribute));
            }
        }

        // Neither the method, nor the controller have the attribute
        // So we return false as it should not be included
        if (attribute == null)
        {
            return false;
        }

        // Since we found an attribute, we are now applying the logic where the method
        // will be included in the open api spec, when AlwaysInclude is on,
        // or when the ApiType matches the requested api type
        if (!string.IsNullOrEmpty(apiType))
        {
            var include = attribute.AlwaysInclude || apiType.Equals(attribute.ApiType, StringComparison.CurrentCultureIgnoreCase); 
            return include;
        }

        return attribute.AlwaysInclude;
    }
}
Enter fullscreen mode Exit fullscreen mode

The result is now that, on every build of the WebAPI project, we will have the generated OpenAPI specs (swagger) stored in the corresponding output folder. (which is configured in nswag-spec.json)

Deploying the WebAPI to Azure API Management

This article leaves out the deployment logic to deploy the actual API as a Docker container and host it in an App Service. That would lead us too far and can be found in the github repo of this article.

We now just focus on importing the OpenAPI specs in Azure API Management as different API's.

This happens by leveraging the following bicep code.

Bicep modules

The following bicep modules can just be reused and then called from within your own project specific bicep file.

api-management-api.bicep

//Parameters
param environmentAcronym string
param locationAcronym string
param apiManagementName string
param serviceUrl string
param apiSpec string
param format string
param displayName string
param description string
param apiRevision string = '1'
param subscriptionRequired bool = true
param path string
param name string
param policy string
param policyFormat string = 'rawxml'
param namedValues array

// Apply naming conventions
var apiName = '${locationAcronym}-${environmentAcronym}-api-${name}'

// Get the existing APIM instance
resource apiManagement 'Microsoft.ApiManagement/service@2021-08-01' existing = {
  name: apiManagementName
}

// Define API resource
resource apiManagementApi 'Microsoft.ApiManagement/service/apis@2021-12-01-preview' = {
  parent: apiManagement
  name: apiName
  properties: {
    displayName: displayName
    description: description
    apiRevision: apiRevision
    subscriptionRequired: subscriptionRequired
    serviceUrl: serviceUrl
    path: path
    format: format
    value: apiSpec
    protocols: [
      'https'
    ]
    // Changing the subscription header name
    subscriptionKeyParameterNames: {
      header: 'x-subscription-key'
      query: 'x-subscription-key'
    }
    isCurrent: true
  }
  dependsOn: apiManagementNamedValues
}

// Specifying the policy on the API level
resource apiManagementapiPolicy 'Microsoft.ApiManagement/service/apis/policies@2021-12-01-preview' = {
  parent: apiManagementApi
  name: 'policy'
  properties: {
    value: policy
    format: policyFormat
  }
}

// Add named values
resource apiManagementNamedValues 'Microsoft.ApiManagement/service/namedValues@2021-08-01' = [for namedValue in namedValues: {
  parent: apiManagement
  name: '${namedValue.name}'
  properties: {
    displayName: namedValue.displayName
    value: namedValue.value
    secret: namedValue.isSecret
  }
}]

output apiName string = apiManagementApi.name
output basePath string = path
Enter fullscreen mode Exit fullscreen mode

We can also deploy an API product for every seperate API. That module can be found below.

api-management-product.bicep

param name string
param apimName string
param displayName string
param description string
param subscriptionRequired bool = true
param apiNames array


resource product 'Microsoft.ApiManagement/service/products@2021-01-01-preview' = {
  name: '${apimName}/${name}'
  properties: {
    displayName: displayName
    description: description
    subscriptionRequired: subscriptionRequired
    state: 'published'
  }
  resource api 'apis@2021-01-01-preview' = [for apiName in apiNames: {
    name: apiName
  }]
}
Enter fullscreen mode Exit fullscreen mode

And then we can call these modules from our main .bicep file, as can be seen in the following snippet:

// Deploy actual API
var apiManagementName = '${locationAcronym}-${environmentAcronym}-${companyName}-apim'

// deploy APIs to the API management instance
var backend_api_policy = '''
<policies>
    <inbound>
        <authentication-managed-identity resource="{0}" />
        <base />
    </inbound>
</policies>
'''

module backendApi 'modules/api-management-api.bicep' = {
  name: 'backendApi'
  scope: backendResourceGroup
  params: {
    locationAcronym: locationAcronym
    environmentAcronym: environmentAcronym
    apiManagementName: apiManagementName
    apiSpec: string(loadJsonContent('../api/backend-openapi.json'))
    format: 'openapi+json'
    path: 'backend/api/v1'
    // The following should typically be taken from output of the actual bicep module for App Service
    // But this sample is focusing on the API Management side of things
    serviceUrl: 'https://weu-dev-samvhintx-app-backend-api.azurewebsites.net/'
    displayName: 'SVH IntX API (${environmentAcronym})'
    description: 'The API that provides all required functionality on the ${environmentAcronym} environment.'
    name: '${environmentAcronym}-api-backend'
    policy: format(backend_api_policy, appServiceClientId)
    namedValues: [ ]
  }
}

module publicApi 'modules/api-management-api.bicep' = {
  name: 'publicApi'
  scope: backendResourceGroup
  params: {
    locationAcronym: locationAcronym
    environmentAcronym: environmentAcronym
    apiManagementName: apiManagementName
    apiSpec: string(loadJsonContent('../api/backend-public-openapi.json'))
    format: 'openapi+json'
    path: 'public/api/v1'
    // The following should typically be taken from output of the actual bicep module for App Service
    // But this sample is focusing on the API Management side of things
    serviceUrl: 'https://weu-dev-samvhintx-app-backend-api.azurewebsites.net/'
    displayName: 'SVH IntX Public API (${environmentAcronym})'
    description: 'The API that provides all required functionality on the ${environmentAcronym} environment.'
    name: '${environmentAcronym}-api-public'
    policy: format(backend_api_policy, appServiceClientId)
    namedValues: [ ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The result

After our successful deployment, this is the result in the API Management instance, where we have two different API products, each showing a different set of Operations.

This can be seen in the following screenshots.

*The "backend" API exposing two operations. *

Backend API with all operation

The "public" API exposing just one operation.

Public API with only one operation

As always, all code can be found online. Here.

Top comments (0)