OpenAPI (OrchardCore.OpenApi)¶
The OpenAPI module exposes an OpenAPI (Swagger) specification for your Orchard Core site and provides auto-generated TypeScript and C# clients via NSwag.
It also ships UI explorers — Swagger UI, ReDoc, and Scalar — so developers can browse and test API endpoints directly from the browser.
Features¶
| Feature | Description |
|---|---|
| Swagger UI | Interactive API explorer at /swagger |
| ReDoc | Alternative read-only API documentation at /redoc |
| Scalar | Modern API reference at /scalar/v1 |
Getting Started¶
- Enable the OrchardCore.OpenApi feature from the admin dashboard (Configuration → Features).
- Log in with an account that has the ViewOpenApiContent permission (granted to Administrators by default).
- Navigate to Configuration → Settings → OpenApi to enable the desired UI(s) and configure authentication.
- Navigate to one of the explorer URLs listed above.
Note: All OpenAPI documentation endpoints (
/swagger,/redoc,/scalar,/openapi) require authentication and theViewOpenApiContentpermission. Unauthenticated users are redirected to the admin login page. Authenticated users without the permission receive a403 Forbiddenresponse.
Configuration¶
The OpenAPI settings page (Configuration → Settings → OpenApi) allows you to:
- Enable/disable each UI independently (Swagger UI, ReDoc, Scalar). Disabled UIs return
404 Not Found. - Choose the authentication method used by the "Try it out" / "Send" buttons in the documentation UIs.
Authentication Types¶
| Type | Description |
|---|---|
| Cookie (default) | No additional configuration needed. If you are logged in, the UIs automatically use your session cookie. |
| OAuth2 Authorization Code + PKCE | Interactive login. The "Authorize" button redirects to the authorization server. Suitable for browser-based API access. This is the only OAuth2 flow supported here — it requires no client secret embedded in the browser, unlike Client Credentials or the deprecated Password grant. |
Cookie Authentication¶
This is the simplest option. The API documentation UIs include the session cookie with every request, so if you are logged into the admin panel, API calls work automatically.
OAuth2 Setup¶
For OAuth2 Authorization Code + PKCE authentication, you need to:
- Enable the OpenID Server feature (Configuration → Features → OpenID Authorization Server).
- Enable the OpenID Token Validation feature — this is required for the API to validate Bearer tokens. Without it, API requests will return
401 Unauthorizedeven with a valid token. - Create an OpenID application (Security → OpenID Connect → Applications): enable Allow Authorization Code Flow and configure a redirect URI for the Swagger UI callback.
- Configure the OpenAPI settings (Configuration → Settings → OpenApi):
- Select the authentication type.
- Enter the Token URL (e.g.,
/connect/token). - Enter the Authorization URL (e.g.,
/connect/authorize). - Enter the Client ID from the OpenID application.
- Enter the Scopes (e.g.,
api).
Note: Client Credentials and Password grant are intentionally not supported by these JavaScript-based documentation UIs — both require a client secret to be embedded in browser-delivered code, which cannot be kept confidential. PKCE is the recommended flow for public/browser-based clients per the OAuth 2.0 Security Best Current Practice.
OpenAPI Client Generation¶
The module uses an NSwag configuration (OrchardCore.OpenApi.nswag) to generate typed clients from the live OpenAPI specification.
Prerequisites¶
-
NSwag CLI — install the global tool:
dotnet tool install -g NSwag.ConsoleCore -
Running application — the site must be running so NSwag can fetch the OpenAPI JSON.
Regeneration Steps¶
1. Start the Application¶
cd src/OrchardCore.Cms.Web
dotnet run
Wait for the message "Application started." in the console.
2. Verify the OpenAPI Endpoint¶
Open the Swagger UI to confirm your endpoints are listed:
- Swagger UI:
https://localhost:5001/swagger - OpenAPI JSON:
https://localhost:5001/swagger/v1/swagger.json
3. Run NSwag¶
From the module directory:
cd src/OrchardCore.Modules/OrchardCore.OpenApi
nswag run OrchardCore.OpenApi.nswag
This generates two clients:
| Client | Output path |
|---|---|
| TypeScript (Axios) | .scripts/bloom/services/OpenApiClient.ts |
| C# (HttpClient) | Services/OpenApiClient.cs |
4. Build Frontend Assets¶
After regenerating the TypeScript client, rebuild the frontend:
# From the repository root
yarn build
NSwag Configuration¶
The generation is configured via OrchardCore.OpenApi.nswag:
- Source URL:
https://localhost:5001/swagger/v1/swagger.json - JSON library (C#):
System.Text.Json— Newtonsoft.Json is not used. - TypeScript template: Axios
- C# HTTP layer:
System.Net.Http.HttpClient
ApiService (api-service.ts)¶
The ApiService class (.scripts/bloom/services/api-service.ts) is a reusable HTTP service that wraps Axios with authentication handling. It supports both cookie and Bearer token authentication and provides the configured Axios instance to NSwag-generated clients.
Authentication Types¶
| Type | Behavior |
|---|---|
"cookie" (default) |
Sets withCredentials: true and attaches the anti-forgery token from the page. |
"bearer" |
Sets withCredentials: false and attaches an Authorization: Bearer <token> header. |
Basic Usage¶
import { ApiService, createApiService } from "@bloom/services/api-service";
// Cookie auth (default) — for admin pages where the user is logged in.
const api = new ApiService();
const response = await api.get("/api/content/my-item-id");
// Bearer auth — for machine-to-machine or external consumers.
const api = new ApiService({ authType: "bearer", token: "eyJ..." });
await api.post("/api/content", { contentType: "Article" });
// Update the token later (e.g., after refresh).
api.setToken("newToken...");
Using with the NSwag-Generated Client¶
The ApiService exposes its underlying Axios instance via getAxiosInstance(), which can be passed directly to the NSwag-generated Client constructor:
import { ApiService } from "@bloom/services/api-service";
import { Client } from "@bloom/services/OpenApiClient";
// Cookie auth — admin pages.
const apiService = new ApiService();
const client = new Client("", apiService.getAxiosInstance());
await client.contentGET("my-content-item-id");
// Bearer auth — external consumers.
const apiService = new ApiService({ authType: "bearer", token: accessToken });
const client = new Client("", apiService.getAxiosInstance());
await client.contentGET("my-content-item-id");
This gives the NSwag-generated client all the authentication handling (cookies + anti-forgery token, or Bearer token) without any additional configuration.
Manual Token Management¶
If you already have a token (e.g., from a different auth flow), pass it directly:
const apiService = new ApiService({ authType: "bearer", token: "eyJ..." });
const client = new Client("", apiService.getAxiosInstance());
// Update the token later (e.g., after refresh).
apiService.setToken("newToken...");
Available Methods¶
| Method | Description |
|---|---|
get<T>(url, config?) |
Perform a GET request. |
post<T>(url, data?, config?) |
Perform a POST request. |
put<T>(url, data?, config?) |
Perform a PUT request. |
patch<T>(url, data?, config?) |
Perform a PATCH request. |
delete<T>(url, config?) |
Perform a DELETE request. |
setToken(token) |
Update the Bearer token for subsequent requests. |
getAxiosInstance() |
Returns the underlying Axios instance for use with generated clients. |
Adding New API Endpoints¶
To add a new API endpoint that is auto-discovered by the OpenAPI specification:
- Create a controller inheriting from
ControllerBase. - Add the
[ApiController]attribute. - Add a
[Route("api/...")]attribute. - Add
[Authorize(AuthenticationSchemes = "Api")]for secured endpoints. - Use XML documentation comments (
///) for richer Swagger descriptions. - Decorate actions with
[ProducesResponseType]to document all response types. - Follow the regeneration steps above to update the clients.
Example Controller¶
[ApiController]
[Route("api/dashboard/myfeature")]
[Authorize(AuthenticationSchemes = "Api")]
public sealed class MyFeatureController : ControllerBase
{
/// <summary>
/// Gets data from my feature.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(MyDataDto), StatusCodes.Status200OK)]
public async Task<IActionResult> GetDataAsync()
{
// Implementation
}
}
ProblemDetails Error Handling¶
API controllers should return standardized RFC 9457 Problem Details responses for error conditions. The ProblemDetailsApiControllerExtensions class in OrchardCore.Abstractions provides convenient extension methods for this.
Available Extension Methods¶
| Method | Status Code | Purpose |
|---|---|---|
ApiChallengeOrForbidForCookieAuth() |
401 / 403 | Returns Unauthorized or Forbidden depending on the authentication state. |
ApiBadRequestProblem() |
400 | Generic bad-request error. |
ApiNotFoundProblem() |
404 | Resource not found. |
ApiValidationProblem() |
400 | Validation error with a ModelStateDictionary containing field-level errors. |
All methods support localized title and detail parameters via LocalizedString.
Usage in Controllers¶
[ApiController]
[Route("api/media")]
public class MediaApiController : ControllerBase
{
[HttpGet("{path}")]
[ProducesResponseType(typeof(FileStoreEntryDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetMediaItem(string path)
{
if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageMedia))
{
return this.ApiChallengeOrForbidForCookieAuth();
}
var item = await _mediaFileStore.GetFileInfoAsync(path);
if (item == null)
{
return this.ApiNotFoundProblem(S["Media not found: {0}", path]);
}
return Ok(ToDto(item));
}
}
JSON Response Shape¶
A ProblemDetails response looks like this:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
"title": "Not Found",
"detail": "Media not found: images/photo.jpg",
"status": 404
}
A ValidationProblemDetails response includes an errors dictionary:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "A validation error occurred.",
"status": 400,
"errors": {
"Name": ["The Name field is required."],
"Path": ["The path contains invalid characters."]
}
}
Frontend Notification System¶
The Bloom frontend framework includes a notification service (@bloom/services/notifications/notifier) that understands ProblemDetails responses out of the box. This creates a seamless error-handling pipeline from the API to the user interface.
How It Works¶
- An API controller returns a
ProblemDetailsresponse (e.g.ApiNotFoundProblem()). - The NSwag-generated client throws the response as an error.
- The calling code passes the error to
notify(). - The notifier detects the
ProblemDetailsshape and converts it to a UI notification.
Architecture¶
┌─────────────────────┐ ProblemDetails JSON ┌─────────────────────────┐
│ API Controller │ ──────────────────────────► │ NSwag OpenApiClient.ts │
│ (C# / Server) │ │ (throws on non-2xx) │
└─────────────────────┘ └────────────┬────────────┘
│ error
┌────────────▼────────────┐
│ Service Layer │
│ (e.g. FileDataService) │
└────────────┬────────────┘
│ catch
┌────────────▼────────────┐
│ notify(error) │
│ (notifier.ts) │
└────────────┬────────────┘
│ emit("notify")
┌────────────▼────────────┐
│ NotificationToast.vue │
│ (UI Toast Component) │
└─────────────────────────┘
Using the Notifier¶
Setup¶
Register the notification bus once at application startup:
import { registerNotificationBus } from "@bloom/services/notifications/notifier";
registerNotificationBus();
Sending Notifications¶
import { notify, NotificationMessage } from "@bloom/services/notifications/notifier";
import { SeverityLevel } from "@bloom/services/notifications/interfaces";
// Success notification
notify(new NotificationMessage({
summary: "Success",
detail: "File uploaded successfully.",
severity: SeverityLevel.Success,
}));
// Catch API errors — ProblemDetails are handled automatically
try {
await fileDataService.deleteMedia(path);
} catch (error) {
notify(error); // title → summary, detail → detail, severity → Error
}
Supported Message Types¶
The notify() function accepts several shapes:
| Input type | Handling |
|---|---|
NotificationMessage |
Passed through directly. |
ValidationProblemDetails (has errors) |
title → summary, field errors are joined and shown as detail. |
ProblemDetails (has title / detail) |
title → summary, detail → detail. |
Error |
"Server Error" summary, message as detail. |
| Falsy / unknown | Falls back to a generic error message. |
Listening for Notifications (Vue 3)¶
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { registerNotificationBus } from "@bloom/services/notifications/notifier";
import type { NotificationMessage } from "@bloom/services/notifications/notifier";
const bus = registerNotificationBus();
const messages = ref<NotificationMessage[]>([]);
function onNotify(msg: NotificationMessage) {
messages.value.push(msg);
}
onMounted(() => bus.on("notify", onNotify));
onUnmounted(() => bus.off("notify", onNotify));
</script>
Severity Levels¶
The SeverityLevel enum (@bloom/services/notifications/interfaces) defines four levels:
| Level | Typical use |
|---|---|
Success |
Confirming a completed action (file moved, item saved). |
Info |
Informational messages. |
Warn |
Non-blocking warnings. |
Error |
API errors, validation failures, unexpected exceptions. |
Troubleshooting¶
OAuth2 "Failed to authorize" or 401 Unauthorized on API Requests¶
- Enable OpenID Token Validation: The most common cause. Go to Configuration → Features and enable the OpenID Token Validation feature. Without it, the API cannot validate Bearer tokens.
- Restart after settings changes: OAuth2 settings are applied at startup. After changing authentication settings, the tenant must be reloaded.
Endpoints Not Appearing in Swagger¶
- Ensure the controller has the
[ApiController]attribute. - Verify the module containing the controller is enabled.
- Check the route does not conflict with MVC routes.
- Restart the application after adding new controllers.
NSwag Generation Fails¶
- Verify the application is running and accessible.
- Check the OpenAPI JSON URL is correct.
- Ensure NSwag CLI is installed correctly (
dotnet tool list -g). - Check for syntax errors in the
.nswagconfiguration.
TypeScript Compilation Errors¶
- Run
yarn buildto rebuild all assets. - Check for type mismatches in your components.
- Verify imports use the correct path to
OpenApiClient.ts.