Technologies: ASP.NET Core 6, Azure API Management (APIM), Azure App Service, Azure CLI, PowerShell
Table of Contents
- Course Overview
- Module 2 — Azure API Management Overview
- Module 3 — Deploying APIs to Azure API Management
- 3.1 Pricing Tiers
- 3.2 Provisioning — Azure Portal
- 3.3 Provisioning — Azure CLI
- 3.4 Provisioning — Azure PowerShell
- 3.5 Key Concepts: Products, Subscriptions, Versions, Revisions
- 3.6 Importing the API into APIM
- 3.7 Preventing APIM Bypass
- 3.8 Products and Subscriptions (demo)
- 3.9 Developer Portal
- 3.10 API Versions
- 3.11 API Revisions
- 3.12 Saving Configuration to a Git Repository
- Module 4 — Enhancing APIs Using API Management Policies
- Architecture Diagrams
- Reference Tables
- Summary and Best Practices
1. Course Overview
This course teaches how to enhance HTTP API capabilities without modifying application code, using Azure API Management (APIM).
Main Objectives
- Understand Azure API Management use cases
- Master APIM concepts: products, subscriptions, versions, revisions, policies
- Provision an APIM instance via the Azure portal, Azure CLI, and PowerShell
- Import an ASP.NET Core 6 API into APIM
- Apply policies to secure, transform, and limit access to APIs
Prerequisites
- Familiarity with HTTP APIs (REST)
- Basic Azure knowledge
2. Module 2 — Azure API Management Overview
2.1 Introduction to Web APIs
A Web API is a programming interface exposed over HTTP/HTTPS.
| Characteristic | Detail |
|---|---|
| Protocol | HTTP (port 80) or HTTPS (port 443) |
| Typical response format | JSON or XML |
| Role | Expose business functionality to clients |
2.2 Developing the ASP.NET Core 6 API
The demo project is the IoT Device Catalogue API for the fictional company Globomantics, which sells industrial IoT devices.
Data Model — IoTDevice.cs
namespace IoTCatalogueAPI
{
public class IoTDevice
{
public int Id { get; set; }
public string? Model { get; set; }
public string? DeviceType { get; set; }
public double Price { get; set; }
public bool IsEdge { get; set; } // true = supports edge computing
}
}
JSON Persistence — JsonDBHelper.cs
using System.Text.Json;
namespace IoTCatalogueAPI
{
public class JsonDBHelper
{
private string _jsonFile = "catalogue.json";
public JsonDBHelper()
{
if (!File.Exists(_jsonFile))
File.WriteAllText(_jsonFile, JsonSerializer.Serialize(new List<IoTDevice>()));
}
public void SaveToDB(List<IoTDevice> catalogue)
{
var json = JsonSerializer.Serialize(catalogue);
File.WriteAllText(_jsonFile, json);
}
public List<IoTDevice> LoadFromDB()
{
if (!File.Exists(_jsonFile))
return new List<IoTDevice>();
return JsonSerializer.Deserialize<List<IoTDevice>>(File.ReadAllText(_jsonFile));
}
}
}
Initial Database — catalogue.json
[
{"Id": 1, "Model": "model-alpha", "DeviceType": "magnetic", "Price": 10, "IsEdge": false},
{"Id": 3, "Model": "model-beta", "DeviceType": "heat", "Price": 650, "IsEdge": true}
]
Main Controller — IoTDeviceCatalogueController.cs
using Microsoft.AspNetCore.Mvc;
namespace IoTCatalogueAPI.Controllers
{
[ApiController]
[Route("[controller]")]
public class IoTDeviceCatalogueController : ControllerBase
{
private List<IoTDevice> catalogue = new();
private JsonDBHelper jsonDBHelper = new();
private readonly ILogger<IoTDeviceCatalogueController> _logger;
public IoTDeviceCatalogueController(ILogger<IoTDeviceCatalogueController> logger)
{
_logger = logger;
catalogue = jsonDBHelper.LoadFromDB();
}
// GET /IoTDeviceCatalogue
[HttpGet(Name = "GetCatalogue")]
public IEnumerable<IoTDevice> GetCatalogue() => catalogue;
// GET /IoTDeviceCatalogue/{deviceId}
[HttpGet("{deviceId}", Name = "GetDevice")]
public IoTDevice GetDevice(int deviceId)
{
var device = catalogue.SingleOrDefault(c => c.Id == deviceId);
jsonDBHelper.SaveToDB(catalogue);
return device ?? new IoTDevice();
}
// POST /IoTDeviceCatalogue
[HttpPost(Name = "AddDevice")]
public IoTDevice AddDevice(IoTDevice device)
{
if (device == null) return new IoTDevice();
int newId = catalogue.Any() ? catalogue.Max(c => c.Id) + 1 : 1;
device.Id = newId;
catalogue.Add(device);
jsonDBHelper.SaveToDB(catalogue);
return device;
}
// PUT /IoTDeviceCatalogue/{deviceId}/{newPrice}
[HttpPut("{deviceId}/{newPrice}", Name = "UpdatePrice")]
public IoTDevice UpdatePrice(int deviceId, double newPrice)
{
var device = catalogue.SingleOrDefault(c => c.Id == deviceId);
if (device == null) return new IoTDevice();
device.Price = newPrice;
jsonDBHelper.SaveToDB(catalogue);
return device;
}
// DELETE /IoTDeviceCatalogue/{deviceId}
[HttpDelete("{deviceId}", Name = "DeleteDevice")]
public void DeleteDevice(int deviceId)
{
var device = catalogue.SingleOrDefault(c => c.Id == deviceId);
if (device != null) catalogue.Remove(device);
jsonDBHelper.SaveToDB(catalogue);
}
}
}
API Operations
| HTTP Method | Route | Operation Name | Description |
|---|---|---|---|
GET | /IoTDeviceCatalogue | GetCatalogue | Returns all devices |
GET | /IoTDeviceCatalogue/{deviceId} | GetDevice | Returns a device by ID |
POST | /IoTDeviceCatalogue | AddDevice | Adds a device |
PUT | /IoTDeviceCatalogue/{deviceId}/{newPrice} | UpdatePrice | Updates the price |
DELETE | /IoTDeviceCatalogue/{deviceId} | DeleteDevice | Deletes a device |
Entry Point — Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); // exposes /swagger/v1/swagger.json
var app = builder.Build();
// Swagger enabled outside of development (for demos)
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Note: The
if(true)condition replaces theIsDevelopment()check so that Swagger is accessible from Azure — useful when importing into APIM via the/swagger/v1/swagger.jsonURL.
2.3 Deploying to Azure App Service
Steps in the Azure portal:
- Go to App Services → Create
- Choose the existing Resource Group
- Name the service (e.g.
appservicedemo-iotcatalogue) — must be globally unique - Publish: Code
- Runtime stack: .NET 6
- OS: Linux (.NET Core runtime works on Linux and Windows)
- Region: Canada Central (or the closest region)
- App Service Plan: Select or create a plan
Deploying from Visual Studio:
- Right-click on the project → Publish → select the created App Service → Publish
Verification: Navigate to https://<app-service-url>/swagger to access the Swagger UI.
2.4 New Business Requirements
After the initial deployment, the team receives requirements unrelated to IoT domain logic:
| Requirement | Description | APIM Solution |
|---|---|---|
| Discovery for external developers | Expose the API to third-party developers | Developer Portal |
| Usage quotas | Max 1,000 calls/hour per client | rate-limit-by-key policy |
| Defense in depth (IP filtering) | Restrict by IP address | ip-filter policy |
| Format transformation | Return XML instead of JSON | json-to-xml policy |
| Azure AD authentication | JWT validation | validate-jwt policy |
| Header validation | Verify the presence of specific headers | check-header policy |
| Caching | Improve performance | cache-lookup / cache-store policy |
All these requirements share one thing in common: they have nothing to do with IoT business logic and should not pollute the API code.
2.5 Introduction to Azure API Management
Microsoft definition: Azure API Management is an API management platform that allows you to import and enhance existing APIs without modifying their code.
Key capabilities:
- API Discovery & Consumption: built-in developer portal
- Internal and external support: internal consumers (intranet) and external (internet)
- No-code / low-code enhancement: XML policies
- Native Azure integrations: App Services, Function Apps, Logic Apps, AKS, Service Fabric
- Advanced security: Application Gateway + Virtual Networks integration (Premium tier)
2.6 Azure APIM Global Architecture
The main components of an APIM instance:
| Component | Role |
|---|---|
| API Gateway | Single entry point; relays requests to backends |
| Management Plane (Azure Portal) | Configuration of APIs, policies, products, subscriptions |
| Developer Portal | Self-service portal for API consumers |
| Git Repository | Backup and restore of APIM configuration |
3. Module 3 — Deploying APIs to Azure API Management
3.1 Pricing Tiers
| Tier | Usage | Characteristics |
|---|---|---|
| Consumption | Serverless / Pay-per-use | Usage-based billing, no dedicated resources |
| Developer | Evaluation & Testing | Near-Premium features, not for production |
| Basic | Entry-level production | Basic capabilities |
| Standard | Mid-volume production | Intermediate capabilities |
| Premium | Enterprise / High availability | VNet integration, multi-region, scaling |
| Isolated (preview) | Isolated environments | Currently in preview |
Tip: Use the Developer tier to evaluate Premium features before making a financial commitment.
3.2 Provisioning — Azure Portal
Steps:
- API Management services → Create
- Resource Group: select the existing group
- Region: Canada Central
- Name: e.g.
apim-demo-01(globally unique name) - Organization: Globomantics
- Administrator email: your address
- Pricing tier: choose according to needs (e.g. Premium)
- Monitoring: optional — Azure Application Insights integration
- Scale: number of processing units (more = more expensive)
- Managed Identity: optional — for AAD authentication
- Virtual Network: optional —
None,External, orInternal - Review + Create
⚠️ Provisioning takes 30 to 45 minutes.
3.3 Provisioning — Azure CLI
# Log in to Azure
az login
# Create an APIM instance
az apim create \
--name myapim-demo-cli01 \
-g demo-rg \
-l canadacentral \
--publisher-email admin@globomantics.com \
--publisher-name Globomantics
# Display the name and SKU of an existing instance
az apim show \
--name myapim-demo-cli \
--resource-group demo-rg \
--query "{Name:name, Sku:sku.name}"
# Delete an instance
az apim delete -n apimdemo02 -g demo-rg
Required parameters for az apim create:
| Parameter | Description |
|---|---|
--name | APIM instance name (globally unique) |
-g / --resource-group | Parent Resource Group |
-l / --location | Azure region |
--publisher-email | Administrator email |
--publisher-name | Organization name |
3.4 Provisioning — Azure PowerShell
# Create an APIM instance via PowerShell
New-AzApiManagement `
-ResourceGroupName "demo-rg" `
-Name "apim-powershell-demo" `
-Location "Canada Central" `
-Organization "Globomantics" `
-AdminEmail "admin@globomantics.com" `
-Sku Developer
Can be run locally or via the Cloud Shell in the Azure portal.
3.5 Key Concepts: Products, Subscriptions, Versions, Revisions
Backend API vs Frontend API
Client
│
▼
┌─────────────────────────┐
│ Azure API Management │
│ ┌─────────────────┐ │
│ │ Frontend API │ │ ← what clients see
│ └────────┬────────┘ │
│ │ relay │
│ ┌────────▼────────┐ │
│ │ API Gateway │ │
└──┴────────┬────────┴────┘
│
▼
┌───────────────┐
│ Backend API │ ← Azure App Service / Function App / etc.
└───────────────┘
Products
A Product groups multiple Frontend APIs to apply shared permissions to them.
- Each Product can have different access levels
- By default, APIM creates two products: Starter and Unlimited
- APIs are associated with one or more Products
- Products can be published (visible in the Developer Portal) or unpublished
Subscriptions
A Subscription grants access to a Product via subscription keys:
- Primary Key and Secondary Key (allow key rotation without interruption)
- The key is sent in the
Ocp-Apim-Subscription-Keyheader - Subscriptions can be scoped to: Global, Product, or individual API
Versions
- Allow maintaining multiple versions in production simultaneously
- Three versioning methods:
- Path:
/v1/...,/v2/... - Header:
Api-Version: v2 - Query string:
?api-version=v2
- Path:
- Each version has its own independent policies
Revisions
- Allow testing changes before going to production
- Only one revision is Current (active) at a time
- Revisions are non-disruptive for consumers
- Key difference from Versions: Revisions are not meant to be in production simultaneously
3.6 Importing the API into APIM
Steps in the Azure portal:
- API Management → APIs → Add API
- Select OpenAPI (import via swagger.json)
- Enter the swagger URL:
https://<app-service-url>/swagger/v1/swagger.json - Configure the Display name and URL suffix
- Associate with a Product (optional)
- Create
Backend URL Configuration:
- After import: go to API Settings
- Update Web service URL with the App Service URL
- Save
3.7 Preventing APIM Bypass
Problem: Clients can bypass APIM by calling the App Service URL directly.
Solution: Security Header in API code
// In the controller — SecurityToken header check
[HttpGet(Name = "GetCatalogue")]
public IActionResult GetCatalogue()
{
// Validate the security header
if (!Request.Headers.TryGetValue("SecurityToken", out var token) || token != "gateway-secret-789")
return Unauthorized();
return Ok(catalogue);
}
⚠️ In production, never hardcode the token. Use Azure Key Vault or App Settings.
APIM Policy to automatically inject the header:
<!-- File: IoTCatalogueAPI__1[Current].xml -->
<policies>
<inbound>
<base />
<!-- APIM injects the SecurityToken before relaying to the backend -->
<set-header name="SecurityToken" exists-action="override">
<value>gateway-secret-789</value>
</set-header>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Secured flow:
Client ──────────────────────────────────────► App Service (Direct)
→ 401 Unauthorized ✗
Client ──► APIM (set-header SecurityToken) ──► App Service
→ 200 OK ✓
3.8 Products and Subscriptions (demo)
Create a custom Product:
- APIs → Products → Add
- Name:
Catalog-Product - Add the
IoTCatalogueAPIAPI to the Product - Configure:
subscriptionRequired: true,approvalRequired: true
Product JSON configuration (excerpt from Git repository):
{
"id": "/products/catalog-product",
"name": "Catalog-Product",
"subscriptionRequired": true,
"approvalRequired": true,
"state": "published",
"$refs-groups": [
"api-management/groups/Administrators/configuration.json",
"api-management/groups/Developers/configuration.json"
]
}
API call with Subscription key (Postman):
GET https://<apim-name>.azure-api.net/IoTDeviceCatalogue
Ocp-Apim-Subscription-Key: <primary-or-secondary-key>
3.9 Developer Portal
The Developer Portal is a self-service portal for API consumers.
Two versions:
- New Developer Portal: customizable (look & feel)
- URL:
https://<apim-name>.developer.azure-api.net
- URL:
- Deprecated Developer Portal (classic): functional but being phased out
- URL:
https://<apim-name>.portal.azure-api.net
- URL:
Developer registration process:
- Developer accesses the Developer Portal
- Signs up and creates an account
- Browses the available Products
- Subscribes to a Product (
Request subscription) - Administrator approves the request (if
approvalRequired: true) - Developer receives their subscription keys
- Can test APIs directly from the portal
Subscription management:
- Admins can grant keys directly from the Azure portal
- Developers can self-serve via the Developer Portal
3.10 API Versions
Create a new version:
- APIs → right-click on the API → Add version
- Choose the method: Path (
/v2), Header, or Query string - Full version name: e.g.
iotcatalogueapi-v2 - Associate with the same Product or a different one
Structure after creation:
IoTCatalogueAPI
├── Original (v1) ← URL: /IoTDeviceCatalogue
└── v2 ← URL: /IoTDeviceCatalogue/v2
Each version has its own independent policies.
3.11 API Revisions
Create a revision:
- APIs → right-click on a version → Add revision
- The new revision is created as a copy of the current version
- Make changes and test on the new revision
- When ready: Make current to push to production
Versions vs Revisions:
| Versions | Revisions | |
|---|---|---|
| Purpose | Multiple versions simultaneously in production | Test changes before going to production |
| Simultaneously in production | Yes | No (only one revision is Current) |
| Visible to consumers | Yes | No (except Current) |
3.12 Saving Configuration to a Git Repository
Each APIM instance has a built-in Git repository.
Save configuration:
- Deployment + Infrastructure → Repository
- Save to repository
- Choose the branch (e.g.
master) - Name the commit (e.g.
adding security policy) - Save (approximately 1 minute)
Restore from repository:
- Repository → Deploy to API Management
- Choose the branch
- Deploy (approximately 2 minutes)
Clone locally:
git clone https://<apim-name>.scm.azure-api.net/
APIM Git repository structure:
api-management/
├── apis/
│ └── IoTCatalogueAPI__1[Current]/
│ └── configuration.json
├── backends/
│ └── _WebApp_iotdevicecatalogue/
│ └── configuration.json
├── groups/
│ ├── Administrators/
│ ├── Developers/
│ └── Guests/
├── policies/
│ ├── global.xml ← Global scope
│ ├── apis/
│ │ └── IoTCatalogueAPI__1[Current].xml ← API scope
│ └── products/
│ └── Starter.xml ← Product scope
├── products/
│ ├── Catalog-Product/
│ │ └── configuration.json
│ ├── Starter/
│ └── Unlimited/
└── configuration.json
4. Module 4 — Enhancing APIs Using API Management Policies
4.1 Policy Concepts
Microsoft definition: Azure API Management policies allow API publishers to modify API behavior through XML configuration statements.
Key points:
- No backend code modification required
- Declarative XML configuration
- Applied in real time on each request/response
- Inheritable according to the scope hierarchy
4.2 XML Policy Structure
<policies>
<!-- Executed upon receiving the inbound request -->
<inbound>
<base /> <!-- inherits policies from the parent scope -->
<!-- your inbound policies here -->
</inbound>
<!-- Executed just before sending the request to the backend -->
<backend>
<base />
<!-- your backend policies here -->
</backend>
<!-- Executed on the backend response before sending it back to the client -->
<outbound>
<base />
<!-- your outbound policies here -->
</outbound>
<!-- Executed on error -->
<on-error>
<base />
<!-- your error policies here -->
</on-error>
</policies>
Mnemonic guide for policy placement:
| Section | When to use it |
|---|---|
<inbound> | Validate/transform the incoming request (auth, IP filter, headers) |
<backend> | Modify the request before sending to backend (set-backend-service) |
<outbound> | Transform the backend response (json-to-xml, caching) |
<on-error> | Handle errors (logging, custom messages) |
4.3 Policy Scopes
┌─────────────────────────────────────────────────────┐
│ GLOBAL SCOPE │
│ (applies to ALL APIs) │
│ ┌───────────────────────────────────────────────┐ │
│ │ PRODUCT SCOPE │ │
│ │ (applies to APIs in the Product) │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ API SCOPE │ │ │
│ │ │ (applies to all operations) │ │ │
│ │ │ ┌───────────────────────────────────┐ │ │ │
│ │ │ │ OPERATION SCOPE │ │ │ │
│ │ │ │ (individual operation) │ │ │ │
│ │ │ └───────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
| Scope | Where to configure | Coverage |
|---|---|---|
| Global | All APIs → Policy | All APIs in the instance |
| Product | Products → [Product] → Policy | All APIs in the Product |
| API | APIs → [API] → Policy | All operations of the API |
| Operation | APIs → [API] → [Operation] → Policy | A single operation |
<base />inherits policies from the parent scope. Removing it prevents inheritance.
4.4 Policy Categories
| Category | Example policies |
|---|---|
| Access restriction | rate-limit, rate-limit-by-key, quota, ip-filter, check-header |
| Authentication | authentication-basic, authentication-certificate, validate-jwt |
| Caching | cache-lookup, cache-store, cache-lookup-value, cache-store-value |
| Cross-domain | cors, jsonp |
| Transformation | json-to-xml, xml-to-json, set-header, set-body, rewrite-uri |
| Routing | set-backend-service, forward-request |
| Logging & Monitoring | log-to-eventhub, trace |
| Mock | mock-response |
4.5 Demo: Rate Limiting / Quotas
Objective: Limit API calls to 5 calls every 30 seconds per IP address.
XML Policy — Scope: Operation (GetCatalogue)
<policies>
<inbound>
<base />
<!-- Limit: 5 calls / 30 seconds, key = client IP address -->
<rate-limit-by-key calls="5"
renewal-period="30"
counter-key="@(context.Request.IpAddress)"
increment-condition="@(context.Response.StatusCode == 200)" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Starter Product Policy — Weekly Quota:
<policies>
<inbound>
<!-- Limit: 5 calls/min and 100 calls/week (604800 seconds) -->
<rate-limit calls="5" renewal-period="60" />
<quota calls="100" renewal-period="604800" />
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
</policies>
HTTP response when limit is exceeded:
- Status Code:
429 Too Many Requests - Header:
Retry-After: <seconds>
Benefit: Also protects against Denial of Service (DoS) attacks.
4.6 Demo: IP Filtering (ip-filter)
Objective: Only allow certain IP addresses (defense in depth).
XML Policy — Scope: API
<policies>
<inbound>
<base />
<!-- Allow only specific IPs -->
<ip-filter action="allow">
<address>203.0.113.42</address> <!-- Single IP -->
<address-range from="192.168.1.1"
to="192.168.1.254" /> <!-- IP range -->
</ip-filter>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Result if IP is not allowed:
{
"statusCode": 403,
"message": "Forbidden"
}
Variant: Use
action="forbid"to block specific IPs (blocklist).
4.7 Demo: JSON → XML Conversion
Objective: Return XML instead of JSON, without changing the backend API code.
XML Policy — Scope: Operation (GetCatalogue)
<policies>
<inbound>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- Converts the JSON response to XML, ignores the client's Accept header -->
<json-to-xml apply="always" consider-accept-header="false" />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Transformation example:
Backend response (JSON):
[
{"Id": 1, "Model": "model-alpha", "DeviceType": "magnetic", "Price": 10, "IsEdge": false}
]
APIM response after policy (XML):
<ArrayOfIoTDevice>
<IoTDevice>
<Id>1</Id>
<Model>model-alpha</Model>
<DeviceType>magnetic</DeviceType>
<Price>10</Price>
<IsEdge>false</IsEdge>
</IoTDevice>
</ArrayOfIoTDevice>
4.8 Demo: HTTP Header Validation
Objective: Require the presence of a specific header with an expected value — applied at the Global scope.
XML Policy — Scope: Global (All APIs)
<policies>
<inbound>
<!-- Default CORS policy -->
<cors allow-credentials="true">
<allowed-origins>
<origin>https://myapim-demo-cli.developer.azure-api.net</origin>
</allowed-origins>
<allowed-methods preflight-result-max-age="300">
<method>*</method>
</allowed-methods>
<allowed-headers>
<header>*</header>
</allowed-headers>
<expose-headers>
<header>*</header>
</expose-headers>
</cors>
<!-- Custom header validation -->
<check-header name="x-custom-api-key"
failed-check-httpcode="401"
failed-check-error-message="Not authorized"
ignore-case="false">
<value>api-key-5678</value>
</check-header>
</inbound>
<backend>
<forward-request />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Test with Postman:
GET https://<apim-name>.azure-api.net/IoTDeviceCatalogue
Ocp-Apim-Subscription-Key: <subscription-key>
x-custom-api-key: api-key-5678
4.9 Demo: Mock API Response
Objective: Return a fake response when the backend is not ready.
Context: The GetDevice operation on the backend returns a 401 (intentionally broken code). Frontend developers need a response to continue their work.
Steps in the portal:
- Select the GetDevice operation
- Inbound processing → Add policy → Mock responses
- Configure the mocked response:
200 OKwith a JSON body - Save
Generated XML policy:
<policies>
<inbound>
<base />
<!-- Returns a mocked response instead of calling the backend -->
<mock-response status-code="200" content-type="application/json" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Use cases: Parallel front/back development, integration testing, demos.
5. Architecture Diagrams
5.1 Azure APIM Global Architecture
graph TB
subgraph Internet
ExtDev[External Developer]
ExtClient[External Client]
end
subgraph AzureAPIM["Azure API Management"]
GW[API Gateway]
MP[Management Plane\nAzure Portal]
DP[Developer Portal]
GIT[Git Repository\nconfiguration]
end
subgraph AzureBackend["Azure Backend Services"]
AS[Azure App Service\nIoT Catalogue API]
FA[Function App]
LA[Logic App]
AKS[AKS / Service Fabric]
end
subgraph Security["Security"]
AAD[Azure Active Directory]
KV[Key Vault]
AG[Application Gateway]
VNet[Virtual Network\nPremium tier]
end
ExtDev -->|"Ocp-Apim-Subscription-Key"| GW
ExtClient -->|HTTPS| GW
GW -->|relay + SecurityToken| AS
GW -->|relay| FA
GW -->|relay| LA
GW -->|relay| AKS
MP -->|configure| GW
DP -->|self-service subscription| ExtDev
GW <-->|validate-jwt| AAD
GW <-->|secrets| KV
AG --> GW
VNet --> GW
MP -->|save/restore| GIT
5.2 Deployment Flow
flowchart LR
A[Visual Studio 2022\nASP.NET Core 6] -->|Publish - Zip Deploy| B[Azure App Service\nIoT Catalogue API]
B -->|swagger.json URL| C[Azure APIM\nImport OpenAPI]
C -->|Configure Backend URL| D[APIM Frontend API\nIoTCatalogueAPI]
D -->|Add to Product| E[Catalog-Product\nSubscription Required]
E -->|Publish| F[Developer Portal\nVisible to developers]
F -->|Subscribe| G[Developer\nSubscription Key]
G -->|Ocp-Apim-Subscription-Key| D
5.3 Conceptual CI/CD Pipeline
flowchart TD
A[Code Push\ngit push] --> B[Build\ndotnet build]
B --> C[Tests\ndotnet test]
C --> D{Tests OK?}
D -->|No| E[Notification\nfailure]
D -->|Yes| F[Publish\ndotnet publish]
F --> G[Deploy App Service\naz webapp deploy]
G --> H[Update APIM\naz apim api import]
H --> I[Apply Policies\naz apim policy set]
I --> J[Save Config\nAPIM Git Repository]
J --> K[Smoke Tests\nPostman / Newman]
K --> L{Smoke OK?}
L -->|No| M[Rollback\nDeploy previous revision]
L -->|Yes| N[Production Live ✓]
5.4 Policy Processing Flow
sequenceDiagram
participant C as Client
participant GW as APIM Gateway
participant BE as Backend API
C->>GW: HTTP Request + Subscription Key
rect rgb(230, 245, 255)
Note over GW: INBOUND PROCESSING
GW->>GW: Validate Subscription Key
GW->>GW: check-header (x-custom-api-key)
GW->>GW: ip-filter (IP check)
GW->>GW: rate-limit-by-key (quota)
GW->>GW: set-header SecurityToken
end
rect rgb(255, 245, 220)
Note over GW: BACKEND PROCESSING
GW->>BE: Forward request + SecurityToken
BE->>BE: Business logic processing
BE->>GW: JSON Response
end
rect rgb(220, 255, 230)
Note over GW: OUTBOUND PROCESSING
GW->>GW: json-to-xml (if configured)
GW->>GW: cache-store
GW->>C: Response (XML or JSON)
end
Note over C,GW: On error → on-error section
6. Reference Tables
Comparison of Provisioning Methods
| Method | Interface | Ideal for | Automatable |
|---|---|---|---|
| Azure Portal | GUI | Demo, learning | No |
Azure CLI (az apim) | Terminal | Scripts, CI/CD | Yes |
Azure PowerShell (New-AzApiManagement) | PowerShell | Windows environments | Yes |
| ARM Template | JSON | Infrastructure as Code | Yes |
| Bicep | DSL | Modern Infrastructure as Code | Yes |
| VS Code Extension | IDE | Local development | No |
Most Common Policies
| Policy | Section | Purpose |
|---|---|---|
set-header | inbound / outbound | Add/modify an HTTP header |
check-header | inbound | Validate the value of a header |
ip-filter | inbound | Filter by IP address |
rate-limit | inbound | Rate limiting (global quota) |
rate-limit-by-key | inbound | Limit by key (IP, subscription) |
quota | inbound | Quota over a long period |
validate-jwt | inbound | Validate a JWT (Azure AD) |
json-to-xml | outbound | Convert JSON → XML |
xml-to-json | outbound | Convert XML → JSON |
cache-lookup | inbound | Look up in cache |
cache-store | outbound | Store in cache |
mock-response | inbound | Return fake response |
cors | inbound | Configure CORS |
forward-request | backend | Relay the request |
APIM Endpoints
| Type | URL Pattern |
|---|---|
| API Gateway | https://<apim-name>.azure-api.net |
| Developer Portal (new) | https://<apim-name>.developer.azure-api.net |
| Developer Portal (classic) | https://<apim-name>.portal.azure-api.net |
| Management API | https://<apim-name>.management.azure-api.net |
| Git Repository (SCM) | https://<apim-name>.scm.azure-api.net |
HTTP Responses from Restriction Policies
| Situation | Status Code | Description |
|---|---|---|
| Missing/invalid subscription key | 401 Unauthorized | Subscription key required |
IP not allowed (ip-filter) | 403 Forbidden | IP blocked |
Missing header (check-header) | 401 Unauthorized | Required header absent |
Quota exceeded (rate-limit) | 429 Too Many Requests | Too many calls |
7. Summary and Best Practices
Security Best Practices
- Never hardcode tokens/passwords in code → use Azure Key Vault + Named Values in APIM
- Always force traffic through APIM via a security header injected by APIM and verified by the backend
- Combine
ip-filter+ subscription keys for defense in depth - Use
validate-jwtfor Azure Active Directory integration - Enable HTTPS only on the backend App Service
APIM Best Practices
- Regularly save configuration to the APIM Git repository
- Use Revisions for policy changes in production (zero-downtime)
- Apply policies at the right scope: global for cross-cutting rules, operation for specific rules
- Monitor with Azure Application Insights (native APIM integration)
- Delete unused APIM instances (high cost, especially Premium/Standard tiers)
- Test on Developer tier before upgrading to Premium
Key Takeaways
- Azure APIM is not an API development tool — it is a management and enhancement tool
- The backend API must be deployed and accessible before configuring it in APIM
- XML policies enable implementing: security, transformation, throttling, caching without touching the code
- The Developer Portal simplifies onboarding of API consumers (internal and external)
- Products and Subscriptions are the central access control mechanism in APIM
- The distinction between Frontend API / Backend API is fundamental: clients never see the backend directly
Training notes — Deploying ASP.NET Core 6 Web API to Azure API Management
Search Terms
asp.net · deploying · core · web · api · azure · management · apis · c# · .net · development · apim · policy · provisioning · architecture · policies · products · revisions · subscriptions · versions · concepts · flow · global · http