
Fusion Backend Dev
- 650 installs
- 1 repo stars
- Updated August 4, 2026
- equinor/fusion-skills
fusion-backend-dev is a Claude Code skill that helps developers understand and consume Equinor Fusion C# backend APIs, authorization, validation, and async patterns using verified reference code.
About
fusion-backend-dev is an Equinor Fusion platform skill (version 0.1.2) for developers who integrate with Fusion backend services without modifying them. It walks frontend developers, integrators, and architects through mcp_fusion_search_backend_code to locate real C# reference implementations, cite repository paths, and explain contracts for People, Org, and Context APIs. Six bundled reference guides cover API contracts, authorization, validation, async messaging, integration, and CQRS handlers. The workflow clarifies integration context, searches with top 3–5 results, explains patterns with evidence, and verifies completeness before handoff. Developers reach for fusion-backend-dev when calling Fusion APIs, understanding validation errors, or learning event and cross-service patterns. Backend service changes are explicitly out of scope and routed to fusion-services-develop.
- Fusion platform APIs
- Enterprise service layout
- Auth integration patterns
- Backend conventions
- SaaS service scaffold
Fusion Backend Dev by the numbers
- 650 all-time installs (skills.sh)
- Ranked #560 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/equinor/fusion-skills --skill fusion-backend-devAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 650 |
|---|---|
| repo stars | ★ 1 |
| Last updated | August 4, 2026 |
| Repository | equinor/fusion-skills ↗ |
How do Fusion frontend developers consume backend API patterns?
Build backend services aligned with Equinor Fusion platform conventions—APIs, service layout, auth integration, and data access patterns expected in Fusion-backed enterprise applications.
Who is it for?
Frontend or integration developers wiring clients against Equinor Fusion C# backend services who need verified contracts and reference code.
Skip if: Teams creating new Fusion backend endpoints, database migrations, or authorization requirement definitions should use backend service repos instead.
When should I use this skill?
A developer asks how to call a Fusion API, understand CQRS handlers, or find authorization and async messaging reference implementations.
What you get
Integration guidance, cited C# reference snippets, repository file paths, and pattern tradeoff notes.
- API contract explanations
- Cited reference code snippets
- Integration pattern recommendations
By the numbers
- Skill version 0.1.2 in equinor/fusion-skills
- Bundles 6 reference guides under references/ for Fusion backend patterns
Files
Fusion Backend Consumption
When to use
Use when needing to understand Fusion backend services, available APIs, integration patterns, or architectural decisions.
Typical triggers:
- "How do I call the People API?"
- "Show an example of how the authorization pattern works"
- "What's the contract for the Org service?"
- "How do services handle validation errors?"
- "What async/messaging patterns does Fusion use?"
- "Can I see a reference implementation of a CQRS handler?"
- "How do services integrate with external APIs?"
- "What authentication/authorization requirements do I need?"
- "Show how events flow through the system"
- "What's the pattern for cross-service calls?"
- "How should I structure my API client?"
- "Where should I call the Context API?"
- "What's the difference between a command and a query in Fusion?"
Implicit triggers:
- Building frontend/client app needing to understand backend contracts
- Integrating with Fusion APIs and need patterns
- Designing architecture needing backend best practices
- Learning from existing Fusion service implementations
When not to use
- Creating or modifying backend services — use service-specific repo skill
- Adding new endpoints or API operations — backend development
- Database schema changes or migrations — backend development
- Authorization requirement definitions — backend development (this skill shows what exists, not new requirements)
- Pure architecture discussions without code references — use
fusion-researchor ADR-focused skills - Selecting between Fusion Framework alternatives — use
fusion-researchorfusion-app-react-dev
Required inputs
Mandatory
- What you're trying to do: clear description of integration point, use case, or pattern
- Your role/context: building a frontend app? Integrating externally? Designing architecture?
Conditional
- When comparing patterns: which two options you're deciding between
- When consuming an API: what operation/scenario (CRUD, async, real-time, etc.)
- When integrating: external system name and direction of flow (calling out vs being called)
Instructions
Step 1 — Clarify consumption context
Before searching for code, understand what you need:
1. Integration point: Calling a backend API? Reading event messages? Implementing a webhook? Integrating with external system? 2. Your boundaries: Frontend developer? Backend developer in another service? External integrator? Architect? 3. Scope: Single API contract? Full pattern? Reference implementation? Architectural tradeoffs?
Use assets/follow-up-questions.md if user intent is unclear.
Step 2 — Search for reference implementation
Use mcp_fusion_search_backend_code to locate existing patterns:
1. Call with high-level intent: "How People service exposes authorization" or "Cross-service API integration patterns" 2. Start with top: 3-5 results 3. Capture metadata.repository, metadata.service, metadata.filePath 4. Extract minimal code snippets showing the pattern (method signature, type contract, authorization check) 5. If results are unclear, refine once:
- Add specific service name or interface
- Narrow to specific layer (controller, handler, client interface)
- Try a different phrase focusing on outcome rather than implementation details
Step 3 — Explain the pattern
Use evidence from Step 2:
1. State the pattern clearly: What does the service do? What contract does it expose? 2. Show the reference code: Quote relevant snippet with file path and line range 3. Explain the constraints: Preconditions? Authorization? Error handling? Async behavior? 4. Relate to your use case: How to apply this pattern? 5. Surface tradeoffs or alternatives if they exist
Step 4 — Verify completeness
Before ending, check:
- [ ] User understands the contract (inputs, outputs, errors)
- [ ] User sees a real code reference (not invented)
- [ ] User knows where the code lives (repository, service, file path)
- [ ] User knows prerequisites (authentication, configuration, dependencies)
- [ ] User has enough context to implement or integrate
If uncertainty remains, flag it explicitly.
Reference guides
See references/ for deeper pattern documentation:
api-contracts.md— Fusion service API contracts and versioningauthorization-patterns.md— Authentication, authorization requirements, role-based accessvalidation-patterns.md— Input validation, error responses, business rulesasync-patterns.md— Events, service bus, domain notifications, eventual consistencyintegration-patterns.md— Cross-service calls, external APIs, webhook handlingcqrs-reference.md— CQRS handlers, commands, queries, notifications structure
Assets
assets/follow-up-questions.md— Clarifying questions for ambiguous requestsreferences/integration-patterns.md— Common integration scenarios and which patterns apply
Safety & constraints
Never:
- Describe real backend API behavior as fact unless verifiable in retrieved source code or cited repo docs
- Claim a pattern exists when search returns no evidence
- Present illustrative pseudo-code as retrieved source code
- Suggest modifying a backend service — that's out of scope
Always:
- Label illustrative examples as examples/pseudo-code when explanatory rather than retrieved
- Capture and cite repository, file path, and line references for real code
- State which repository the pattern comes from
- Note when a pattern exists in one service but not others
- Offer to escalate to
fusion-services-developskill if user wants to implement changes
Follow-Up Questions
Use these when the user's request is ambiguous or missing key context.
Integration Scenario
When: User asks about using a backend service but doesn't specify the context
"I need to call the People API"
→ Are you building a:
- Frontend app that displays person data?
- Backend service that needs person information?
- External integration consuming Fusion APIs?
- Batch/scheduled job syncing data?When: User asks for patterns but doesn't specify scope
"Show me error handling patterns"
→ Are you:
- Building a new backend service and want to see how Fusion does it?
- Calling another service and want to handle failures?
- Building an external integration?
- Something else?Scope Clarification
When: User mentions an operation without detail
"How do I create a context?"
→ Are you:
- A backend developer adding a new endpoint?
- A frontend developer calling a create API?
- An architect designing the workflow?When: User asks about data without context
"How is Person data structured?"
→ Do you need:
- The API contract (what fields the API returns)?
- The database schema (internal storage)?
- How data flows between systems?
- Authorization/access rules?Technical Constraints
When: User asks for implementation without constraints
"How should I cache this data?"
→ Consider:
- How often does data change? (Invalidation frequency)
- Is consistency critical? (Real-time vs eventual)
- What's your storage? (In-memory, Redis, database)
- Scale? (Few calls/day vs thousands/minute)When: User asks about integration without specifying direction
"Integrate with external system"
→ Direction matters:
- Does Fusion call the external system?
- Does external system call Fusion?
- Both (bidirectional)?Example Scenarios
Pick the scenario that matches:
- Frontend app consuming Fusion APIs: Use
api-contracts.md,authorization-patterns.md,validation-patterns.md - Backend service calling another Fusion service: Use
integration-patterns.md,async-patterns.md - External system integration: Use
integration-patterns.md,async-patterns.md,api-contracts.md - Learning handler patterns: Use
cqrs-reference.md - Understanding real-time updates: Use
async-patterns.md - Debugging authorization: Use
authorization-patterns.md - Error recovery: Use
validation-patterns.md
Changelog
0.1.2 - 2026-05-29
patch
0.1.1 - 2026-05-07
patch
- Drop articles, filler, hedging from SKILL.md activation body
- Compress authorization-patterns, validation-patterns, integration-patterns, async-patterns, cqrs-reference, api-contracts references
0.1.0 - 2026-04-22
minor
- #143 `3c02e1d` Thanks @alftore! - Add new shared skill for consuming and understanding Fusion backend services
- Comprehensive references on API contracts, versioning, and error handling
- Authorization patterns covering Azure AD, RBAC, and error scenarios
- Validation layer patterns, error codes, and retry strategies
- Async patterns for events, webhooks, polling, and idempotent processing
- Integration patterns for cross-service calls, resilience, and caching
- CQRS reference guide for command/query patterns and handlers
- Follow-up questions asset for clarifying ambiguous requests
- Integrates with fusion-research skill for code discovery via MCP
- Published as active skill; suitable for shared consumption across teams
Used by backend API consumption, service integration, and backend contract understanding workflows.
API Contracts & Versioning
Fusion Service API Structure
All Fusion backend services follow a consistent REST API pattern:
Base Pattern
Routes use absolute paths without a version prefix. API version is supplied via query string or header — not as a URL segment.
GET /resources # List resources
GET /resources/{id} # Get single resource
POST /resources # Create resource
PATCH /resources/{id} # Partial update
DELETE /resources/{id} # Delete resourceVersion negotiation: ?api-version=3.0 (query string) or api-version: 3.0 (request header).
Versioning Strategy
Asp.Versioning with HeaderOrQueryVersionReader. Version via:
- Query string:
?api-version=3.0(preferred) - Request header:
api-version: 3.0
AssumeDefaultVersionWhenUnspecified is enabled, so unversioned requests receive the default version.
Version levels:
- Major version: Incompatible changes (breaking changes to response shape, required fields, semantics)
- Minor version: Non-breaking enhancements (new optional fields, new endpoints)
Error Response Format
Fusion services return errors as RFC 7807 ProblemDetails with Fusion-specific extensions:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"error": {
"code": "ModelValidationError",
"message": "Model contained 1 error"
},
"errors": {
"email": ["Email format is invalid"]
},
"traceId": "...",
"timestamp": "2026-04-17T10:30:00Z"
}See validation-patterns.md for the full error format reference, including domain errors, authorization errors, and retry guidance.
Authorization Header
All requests (except health checks) require:
Authorization: Bearer {access_token}The access token is a JWT obtained from Azure AD. Scope required depends on the endpoint.
---
Common API Contracts
People Service
Endpoint: GET /persons/{personId}?api-version=3.0
Route attribute: [HttpGet("/persons/{personId}")] with [MapToApiVersion("3.0")]. Also supports GET /persons/me.
Response (illustrative — verify exact shape in the service's Swagger/OpenAPI spec):
{
"azureUniqueId": "bbe7b3e5-b1da-4a3f-a0b8-7f7f8f8f8f8f",
"name": "John Doe",
"mail": "john.doe@equinor.com",
"accountType": "Employee",
"department": "Engineering",
"fullDepartment": "TDI PRD ENG TSE"
}Supports OData expansion: ?$expand=roles,positions,contracts,manager (v3) and additionally companies (v4).
Required scope:
- Delegated (user token):
api://{resource-app-id}/user_impersonation - App-only (client credentials):
api://{resource-app-id}/.default
Verify the actual resource App ID URI for the People service in your environment.
Authentication: Azure AD access token
---
LineOrg Service
Endpoint: GET /org-units/{orgUnitId}
The Org service handles projects, positions, and contracts (/projects/{id}/positions/...). The org-unit hierarchy (departments, management chain) is served by the LineOrg service.
Response (illustrative — verify exact shape in the service's Swagger/OpenAPI spec):
{
"sapId": "51234",
"name": "Engineering",
"fullDepartment": "TDI PRD ENG TSE",
"shortName": "TSE",
"level": 5,
"children": []
}Supports OData expansion: ?$expand=children,management.
Required scope:
- Delegated:
api://{resource-app-id}/user_impersonation - App-only:
api://{resource-app-id}/.default
Verify the actual resource App ID URI for the LineOrg service in your environment.
---
Context Service
Endpoint: GET /contexts/{id}?api-version=1.0
Route attribute: [HttpGet("/contexts/{id}")] with [ApiVersion("1.0")]. Also supports GET /contexts with OData filter/search and GET /contexts/{id}/relations.
Response (illustrative — verify exact shape in the service's Swagger/OpenAPI spec):
{
"id": "ctx-uuid",
"title": "Project Alpha",
"type": { "id": "ProjectMaster", "isCustom": false },
"externalId": "external-ref",
"isActive": true
}Required scope:
- Delegated:
api://{resource-app-id}/user_impersonation - App-only:
api://{resource-app-id}/.default
Verify the actual resource App ID URI for the Context service in your environment.
---
Integration Points
REST API Consumption
Most common for:
- Frontend applications reading data
- External integrations (webhooks, periodic sync)
- Service-to-service calls via typed HTTP clients
Event Subscription
Available for:
- Real-time updates (Person promoted, Context created)
- Async integration with other systems
- Workflow triggers
See async-patterns.md for event details.
---
Rate Limiting
Fusion services may apply rate limiting; quotas and retry behavior vary by service and environment.
Common patterns:
- Exceeded:
429 Too Many Requests, sometimes with aRetry-Afterheader - Headers: Some services or gateways may expose headers such as
X-RateLimit-Remaining - Quotas: Request limits are service-specific; verify exact limits in the target API/OpenAPI documentation
---
Pagination
Large result sets use OData offset pagination ($top, $skip). Standard paged response envelope:
{
"totalCount": 245,
"count": 50,
"@nextPage": "/resources?$top=50&$skip=50&api-version=1.0",
"@prevPage": null,
"value": [ /* items */ ]
}| Field | Description |
|---|---|
totalCount | Total matching items (ignoring paging) |
count | Items in this page |
@nextPage | Relative URL for next page (null when on last page) |
@prevPage | Relative URL for previous page (omitted on first page) |
value | Array of result items |
Common query parameters:
$top=50— page size (max varies by endpoint, e.g. 100 for notifications)$skip=0— offset into result set$filter=field eq 'value'— OData filter (allowed fields declared per endpoint)$search=term— full-text search (where supported)$orderby=field asc— sort order (where supported)
Note: Not all endpoints support pagination. Some return the full collection directly as an array. Check the target endpoint's OpenAPI/Swagger spec for the actual response shape and supported query parameters.
---
When Contracts Change
Breaking Changes
- New required field
- Removed field
- Changed field type or semantics
- New mandatory query parameter
Action: New major version (e.g., api-version=4.0); keep the old version available according to the target service's documented compatibility or deprecation policy
Non-Breaking Changes
- New optional field
- New optional query parameter
- New operation/endpoint
Action: Increment minor version in header; no URL change required
Async & Event Patterns
Event Publishing
Fusion services publish events to Azure Service Bus using IEventNotificationClient, following CloudEvents v1.0.
Event Type Categories
Types use dotted naming: com.equinor.fusion.{domain}.{type}.
| Category | Type name | Service | Sub-types (in payload) |
|---|---|---|---|
| People | com.equinor.fusion.people.profile | People | ProfileUpdated, UserRemoved |
| People | com.equinor.fusion.people.security | People | RolesUpdated |
| Context | com.equinor.fusion.context.context | Context | Created, Modified, Deleted |
| Context | com.equinor.fusion.context.relation | Context | Relation changes |
| Org | com.equinor.fusion.org.position | Org | PersonAssigned, PersonUnassigned, PositionCreated, PositionUpdated, PositionRemoved |
| Org | com.equinor.fusion.org.project | Org | ProjectCreated, ProjectUpdated, ProjectRemoved |
| Org | com.equinor.fusion.org.contract | Org | ContractCreated, ContractUpdated, ContractRemoved |
Event Format (CloudEvents v1.0)
{
"specversion": "1.0",
"id": "{event-id}",
"source": "fusion-context-service",
"type": "com.equinor.fusion.context.context",
"time": "2026-04-17T10:30:00Z",
"datacontenttype": "application/json",
"fusionCategory": "optional-category",
"data": "{ /* JSON-serialized payload string */ }"
}Note: data is a JSON-serialized string, not a nested object. Deserialize separately.---
Event Subscription
Subscription API Pattern
Fusion services expose subscription endpoints that create a Service Bus subscription and return connection details with a SAS token. Only service principals can subscribe.
Known subscription endpoints:
| Service | Endpoint | Topic |
|---|---|---|
| Context | PUT /subscriptions/contexts | context-sub |
| ContractPersonnel | PUT /subscriptions/contracts | contractpersonnel-sub |
| FusionTasks | PUT /subscriptions/fusiontasks | fusiontask-sub |
| Roles V2 | PUT /subscriptions/roles-v2 | role-v2-sub |
Request:
{
"id": "optional-guid",
"identifier": "my-app-name",
"type": "Persistent",
"typeFilter": ["com.equinor.fusion.org.position"]
}type:"Persistent"(auto-deletes after 14 days idle) or"Transient"(auto-deletes after 5 minutes idle)typeFilter: Optional array of event type names for SQL filter on thetypemessage propertyidentifier: Your application name; used to generate a recognizable subscription name
Response:
{
"id": "subscription-guid",
"handlerName": "context-sub",
"connection": {
"endpoint": "sb://namespace.servicebus.windows.net",
"path": "topic/subscriptions/subscription-name",
"token": {
"audience": "https://namespace.servicebus.windows.net/path",
"tokenValue": "SharedAccessSignature sr=...&sig=...&se=...&skn=SubscribersKey",
"tokenType": "servicebus.windows.net:sastoken",
"expiresAtUtc": "2026-04-17T12:30:00Z"
}
}
}Connecting with the SAS Token
Use the returned connection details to create a Service Bus client. The endpoint is an sb:// URI — extract the host for ServiceBusClient:
// Parse the sb:// endpoint to get the host name
var endpointUri = new Uri(connection.Endpoint);
var client = new ServiceBusClient(
endpointUri.Host,
new AzureSasCredential(connection.Token.TokenValue));
// connection.Path is the full subscription path: "{topic}/subscriptions/{name}"
// Use the topic + subscription overload for ServiceBusProcessor
const string subscriptionSegment = "/subscriptions/";
var subscriptionIndex = connection.Path.IndexOf(subscriptionSegment, StringComparison.OrdinalIgnoreCase);
if (subscriptionIndex < 0)
{
throw new InvalidOperationException(
$"Unexpected Service Bus subscription path format: '{connection.Path}'.");
}
var topicName = connection.Path[..subscriptionIndex];
var subscriptionName = connection.Path[(subscriptionIndex + subscriptionSegment.Length)..];
if (string.IsNullOrWhiteSpace(topicName) || string.IsNullOrWhiteSpace(subscriptionName))
{
throw new InvalidOperationException(
$"Unexpected Service Bus subscription path format: '{connection.Path}'.");
}
var processor = client.CreateProcessor(
topicName,
subscriptionName,
new ServiceBusProcessorOptions { MaxConcurrentCalls = 1 });
processor.ProcessMessageAsync += async (args) =>
{
string body = args.Message.Body.ToString();
// Body is a CloudEvent v1.0 JSON; deserialize accordingly
var cloudEvent = JsonConvert.DeserializeObject<CloudEventV1>(body);
// Process the event...
};
processor.ProcessErrorAsync += async (args) =>
{
if (args.Exception is UnauthorizedAccessException)
{
// SAS token expired — renew by calling the subscription endpoint again
}
};
await processor.StartProcessingAsync();Token renewal: SAS tokens expire. On UnauthorizedAccessException, stop processor, re-call subscription endpoint for fresh token, reconnect.Filtering Events
Subscriptions support SQL filters on message properties:
type = 'com.equinor.fusion.org.position'
app = 'my-app-id'Set via typeFilter in subscription request or managed by service.
Service Bus Message Properties
Message ApplicationProperties (available for filtering):
| Property | Description |
|---|---|
type | Event type name (e.g. com.equinor.fusion.org.position) |
app | App context identifier |
origin | Event origin |
category | Event category |
batch-id | Batch identifier (when events are sent as a batch) |
---
Common Event Patterns
Pattern: Real-Time Sync
When another system needs to stay in sync:
1. User does action in Fusion
2. Fusion publishes event
3. External system receives event
4. External system updates its local copyUse case: Dashboard showing current org chart, active positions
Challenge: Initial state. Solution: Fetch full snapshot on startup, then subscribe to incremental updates.
Pattern: Eventual Consistency
When operations span multiple services:
1. User creates context → Context service publishes ContextCreated
2. Approvals service receives → Creates default approvals
3. Reporting service receives → Adds to reporting index
4. Notifications service receives → Sends "New project" messageKey: All listeners are independent; failure in one doesn't block the others.
Pattern: Webhook Delivery
When an external system needs event notifications via HTTP callbacks:
1. External system registers webhook URL (via the provider's webhook API)
2. Event occurs in the source system
3. Source system makes HTTP POST to the registered callback URL
4. Receiver validates signature and processes the eventNote: Webhook header names, signature formats, and registration APIs vary by provider. Check provider docs for exact contract. See integration-patterns.md for signature validation example.
---
Handling Events
Idempotent Processing
Events may be delivered multiple times. Handlers must be idempotent:
// ❌ NOT idempotent:
public void Handle(PositionAssigned @event)
{
DbPosition position = new DbPosition
{
PositionId = @event.Data.PositionId,
PersonId = @event.Data.PersonId,
Title = @event.Data.Title
};
_db.Positions.Add(position);
_db.SaveChanges();
}
// ✅ Idempotent:
public void Handle(PositionAssigned @event)
{
DbPosition? existing = _db.Positions.Find(@event.Data.PositionId);
if (existing == null)
{
DbPosition position = new DbPosition
{
PositionId = @event.Data.PositionId,
PersonId = @event.Data.PersonId,
Title = @event.Data.Title
};
_db.Positions.Add(position);
_db.SaveChanges();
}
// If already processed, no-op
}Error Handling
try
{
ProcessEvent(@event);
AcknowledgeMessage(message); // Tell bus we succeeded
}
catch (RecoverableException ex)
{
// Retry later (service bus will redeliver)
// Don't acknowledge; message stays on queue
}
catch (PoisonMessage ex)
{
// This event is bad; move to dead-letter queue
MoveToDlq(message);
}Ordering Guarantees
Don't assume messages arrive in order.
- Events for the same context may still be delivered out of order
- Events for different contexts may be interleaved or out of order
- Don't assume global ordering
If you need strict ordering: Use an explicit ordering strategy such as Service Bus Sessions (for example, sessionId = contextId) so related events are processed FIFO within that session.
Tradeoffs: Sessions require session-aware consumers and can reduce parallelism for events that share the same session. ---
Async APIs (Polling)
Some long-running operations use polling:
1. Start operation → Returns operation ID
2. Poll status endpoint with operation ID
3. Operation completes → Returns resultExample:
POST /api/contexts?api-version=1.0 → 202 Accepted
{ "operationId": "op-uuid" }
GET /api/contexts/op-uuid?api-version=1.0 →
{ "status": "InProgress", "progress": 45 }
// Later...
GET /api/contexts/op-uuid?api-version=1.0 →
{ "status": "Completed", "contextId": "ctx-uuid" }
// Alternatively, send the version in a header:
// api-version: 1.0---
When to Use Each Pattern
| Pattern | Use Case | Pro | Con |
|---|---|---|---|
| Event Subscription | Real-time sync, multi-system orchestration | Decoupled, fire-and-forget, scales | Complex setup, eventual consistency |
| Webhook | External system notification | Simple for external partners, HTTP standard | Requires public endpoint, delivery challenges |
| Polling | Frontend UX (show progress), long operations | Simple client code, user control | Chatty, can be slow |
| Direct API Call | Immediate result needed, tight coupling acceptable | Simple, immediate feedback | Tightly coupled, not resilient to changes |
Authorization Patterns
Request Authentication
Most Fusion services require a Bearer token. See API Key Authentication for exceptions:
Authorization: Bearer {jwt_token}Token from Azure AD using the Fusion app registration:
- Client ID:
{app-client-id}(from your Fusion app registration) - Authority:
https://login.microsoftonline.com/{tenant-id}/ - Delegated scope format:
api://{resource-app-id}/{scope-name} - Client credentials scope format:
api://{resource-app-id}/.default
Example Scopes
Delegated scopes (user/app acting on behalf of a signed-in user):
| Service | Scope |
|---|---|
| People | api://{resource-app-id}/user_impersonation |
| Org | api://{resource-app-id}/user_impersonation |
| Context | api://{resource-app-id}/user_impersonation |
Delegated scope names defined per service's Azure AD app registration. user_impersonation is common — verify actual scope in target service.Application scope (client credentials / app-only access):
| Flow | Scope |
|---|---|
| Client credentials | api://{resource-app-id}/.default |
---
Authorization Requirements
Fusion services use ASP.NET Core authorization requirements. Common patterns:
Requirement-Based Authorization
// Backend service defines a requirement
public class MustBeContextManagerRequirement : IAuthorizationRequirement
{
public string ContextId { get; set; }
}
// Frontend passes the requirement check implicitly via:
// 1. User identity (roles, claims)
// 2. Resource ownership (context manager, project lead, etc.)Consumers:
- Azure AD identity must include the required role/claim
- Or must be explicitly listed as responsible (context manager, position holder, etc.)
- If denied:
403 Forbidden
Common Requirements
| Scenario | Requirement | How to satisfy |
|---|---|---|
| Read Context | CanReadContext | Any authenticated user with delegated api://{context-resource-app-id}/user_impersonation scope |
| Modify Context | IsContextManager | Must have "ContextManager" role in that context OR hold a position in it |
| Delete Position | CanDeletePosition | Must be HR admin OR context manager where position exists |
| View Person | CanViewPerson | Any authenticated user with delegated api://{people-resource-app-id}/user_impersonation scope |
Note: Use.defaultscopes only for client-credentials (app-only) flows. For delegated (on-behalf-of-user) flows, use service-specific scopes likeuser_impersonation.
---
Role-Based Access Control (RBAC)
Fusion services assign roles per user per resource. Example: roles in a context:
{
"contextId": "abc-def",
"userId": "person-123",
"roles": ["ContextManager", "Approver", "Contributor"]
}How roles affect API behavior:
- ContextManager: Can create positions, assign responsibilities, modify context properties
- Approver: Can approve requests within the context
- Contributor: Can edit owned resources; read access to context
- Viewer: Read-only access to context and positions
Check token scp claim or app roles for coarse permissions. Per-context roles (ContextManager, Approver, Contributor): use Fusion endpoints — not Azure AD token claims.
---
Common Authorization Errors
401 Unauthorized
Cause: Missing or invalid token
Fix:
- Confirm token is in
Authorization: Bearer {token}header - Verify token is not expired
- Verify token was requested with correct scope
403 Forbidden
Cause: Token is valid but user doesn't have required role/permission
Fix:
- Confirm user has required role in this context/service
- Ask context manager to assign role if needed
- Check if user is responsible (position holder, manager, etc.)
403 Forbidden with authorization detail
Cause: Caller doesn't satisfy authorization requirement
Response example (ProblemDetails with Fusion error extension):
{
"type": "https://docs.fusion-dev.net/development/api/errors/#403",
"title": "User is not authorized to access data",
"status": 403,
"detail": "User must be context manager",
"error": {
"code": "Forbidden",
"message": "User must be context manager",
"accessRequirements": [
{ "code": "IsContextManager", "description": "Must be context manager", "outcome": "Failed", "wasEvaluated": true }
]
},
"traceId": "...",
"timestamp": "..."
}Fix: Escalate to someone with required role
---
Service-to-Service Authentication
Internal service-to-service:
1. Service acquires token for its service principal (not user) 2. Token requested with service's own scope 3. Called service validates caller's service identity
As client, don't implement directly. Know:
- Fusion services communicate securely
- Cross-service calls are authenticated and authorized
- If a service can't reach another, it's usually a service principal permission issue
---
API Key Authentication (Special Cases)
Non-user integrations may use API keys instead of Azure AD tokens:
Authorization: ApiKey {api-key}When: Limited external integrations, batch operations, webhook receivers
How to get: Request from service owner; usually provided in project setup
---
Checking Your Permissions
Before calling an endpoint:
1. Get token: Azure AD with appropriate scope:
- Delegated (user flow):
api://{resource-app-id}/user_impersonation - App-only (client credentials):
api://{resource-app-id}/.default
2. Decode the token: Use jwt.ms to see your roles/claims 3. Check the endpoint docs: Look for "Required role" or "Requirement" 4. Verify you have that role: If not, ask manager or context manager to assign it
Example Token Claims
Delegated token (user acting via an app):
{
"oid": "person-object-id",
"name": "John Doe",
"email": "john.doe@equinor.com",
"scp": "user_impersonation",
"aud": "{resource-app-id}"
}App-only token (client credentials, no user context):
{
"oid": "service-principal-object-id",
"appid": "{client-app-id}",
"roles": ["Application.ReadWrite"],
"aud": "{resource-app-id}"
}Key difference: Delegated tokens carryscp(delegated permissions); app-only tokens carryroles(application permissions). Never both in one token.
Per-context roles (e.g. ContextManager, Approver) are Fusion-level concepts returned in API responses, not Azure AD token claims.
CQRS: Commands, Queries, and Handlers
Command/Query Pattern Basics
Fusion services use CQRS (Command Query Responsibility Segregation) via MediatR:
Commands (Write Operations)
Commands express intent to change state:
// Example command
public class CreatePositionCommand : IRequest<PositionDto>
{
public string ContextId { get; set; }
public string Title { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public string? ExternalId { get; set; } // Optional idempotency key
}Characteristics:
- Imperative name (Create, Update, Delete, Assign, Publish)
- Returns a result (
IRequest<TResponse>) - Goes through validation pipeline
- May trigger events
- Executed once (idempotent when possible)
Queries (Read Operations)
Queries request data:
// Example query
public class GetPositionsQuery : IRequest<List<PositionDto>>
{
public string ContextId { get; set; }
public bool IncludeArchived { get; set; }
}Characteristics:
- Indicative name (Get, List, Find, Search)
- Returns data (
IRequest<TResponse>) - Should be idempotent (no side effects)
- Can be cached
- Multiple executions are safe
Handlers
Each command/query has a handler that processes it:
// Command handler
public class CreatePositionHandler : IRequestHandler<CreatePositionCommand, PositionDto>
{
public async Task<PositionDto> Handle(CreatePositionCommand request, ...)
{
// Validate input
// Check authorization
// Apply business logic
// Save to database
// Publish events
// Return result
}
}
// Query handler
public class GetPositionsHandler : IRequestHandler<GetPositionsQuery, List<PositionDto>>
{
public async Task<List<PositionDto>> Handle(GetPositionsQuery request, ...)
{
// Query database
// Apply filters
// Return data
}
}---
Handler Lifecycle
When you call a command/query, this happens:
1. Handler created (dependency injection)
2. Validation behaviors run
├─ Validate input schema
└─ Validate business rules
3. Authorization behavior runs
└─ Check if user has permission
4. Logging/telemetry begins
5. Handle() method executes
6. Response captured
7. Events published (if any)
8. Logging/telemetry ends
9. Response returned---
Command Patterns
Create Pattern
public class CreateContextCommand : IRequest<ContextDto>
{
public string Title { get; set; }
public string Type { get; set; } // ProjectContext, ProgramContext, etc.
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
// Handler responsibilities:
// 1. Validate: Title not empty, dates valid, type recognized
// 2. Check auth: User is admin or has create permission
// 3. Create: Context entity with initial state
// 4. Persist: Save to database
// 5. Publish: ContextCreated event
// 6. Return: ContextDto with new IDUpdate Pattern
public class UpdateContextCommand : IRequest<ContextDto>
{
public string ContextId { get; set; }
public string? Title { get; set; } // Optional — null means unchanged
public DateTime? EndDate { get; set; } // Optional — null means unchanged
}
// Handler responsibilities:
// 1. Load: Fetch current context
// 2. Validate: New values are valid
// 3. Check auth: User is context manager
// 4. Update: Modify only specified fields
// 5. Persist: Save changes
// 6. Publish: ContextModified event
// 7. Return: Updated ContextDtoDelete Pattern
public class DeleteContextCommand : IRequest<bool>
{
public string ContextId { get; set; }
}
// Handler responsibilities:
// 1. Load: Fetch context
// 2. Check auth: Only admin or context owner
// 3. Validate: Can delete (no active positions, etc.)
// 4. Delete: Mark as archived (soft delete) or remove
// 5. Persist: Save deletion state
// 6. Publish: ContextDeleted event
// 7. Return: Success boolean---
Query Patterns
Single Item
public class GetContextQuery : IRequest<ContextDto>
{
public string ContextId { get; set; }
}
// Handler: Fetch by ID, apply read authorization, returnList with Filtering
public class ListContextsQuery : IRequest<List<ContextDto>>
{
public string? Type { get; set; } // Optional filter
public bool IncludeArchived { get; set; }
public int? Take { get; set; } // Pagination
public int? Skip { get; set; }
}
// Handler: Query with optional filters, paginate, return listFluent Builder Pattern
Illustrative fluent builder (actual query classes vary by service):
// Fluent builder query example:
GetContextQuery query = new GetContextQuery { ContextId = contextId }
.WithPositions()
.WithManager()
.WithResponsibilities();
ContextDto context = await mediator.Send(query);What this enables: Load only needed data
---
Validation & Authorization
Validation Flow
1. Input validation: Are required fields present? Correct types?
2. Business rule validation: Does this make sense?
- StartDate before EndDate?
- Context not archived?
- Quota not exceeded?
3. Authorization check: Does user have permission?Authorization Requirements
public class CreatePositionCommand : IRequest<PositionDto>, ITrackableRequest
{
public string ContextId { get; set; }
public string Title { get; set; }
}
// Implicit requirement:
// Must satisfy "IsContextManager" for ContextId
// This is checked before Handle() runsIf authorization fails: 403 Forbidden returned; Handle() never runs
---
Error Handling
Validation Errors
Command fails in validation phase (ProblemDetails envelope):
Response: 400 Bad Request
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"error": { "code": "ModelValidationError", "message": "Model contained 1 error" },
"errors": { "startDate": ["Must be before end date"] },
"traceId": "..."
}Authorization Errors
Command fails in authorization phase:
Response: 403 Forbidden
{
"type": "https://docs.fusion-dev.net/development/api/errors/#403",
"title": "User is not authorized to access data",
"status": 403,
"error": { "code": "Forbidden", "message": "Only context managers can create positions" },
"traceId": "..."
}Business Rule Violations
Handler executes but business rule check fails:
if (context.IsArchived)
{
throw new InvalidOperationException("Cannot modify archived context");
}Response: 400 Bad Request
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Invalid operation",
"status": 400,
"detail": "Cannot modify archived context",
"error": { "code": "InvalidOperation", "message": "Cannot modify archived context" },
"traceId": "..."
}Unexpected Errors
Handler crashes or unhandled exception:
Response: 500 Internal Server Error
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title": "Unexpected error",
"status": 500,
"error": { "code": "UnexpectedError", "message": "An unexpected error occurred" },
"traceId": "00-abc123..." // Share with support
}---
Choosing Command vs Query
| Aspect | Command | Query |
|---|---|---|
| Intent | Change state | Read state |
| Side effects | Expected (save to DB, publish events) | None (read-only) |
| Idempotent | Usually not (or explicitly handled) | Always |
| Can cache | No | Yes |
| Execution | Once, carefully | Multiple times safe |
| Example | CreatePosition, UpdateContext, AssignRole | GetContext, ListPositions, SearchPeople |
---
Common Handler Patterns
Idempotent Creation
public async Task<PositionDto> Handle(
CreatePositionCommand request,
CancellationToken cancellationToken)
{
// Check if already exists only when an idempotent key was provided
if (!string.IsNullOrWhiteSpace(request.ExternalId))
{
DbPosition? existing = await _db.Positions
.FirstOrDefaultAsync(p => p.ExternalId == request.ExternalId, cancellationToken);
if (existing != null)
return _mapper.Map<PositionDto>(existing); // Return existing
}
// Create new
DbPosition position = new DbPosition { ... };
await _db.SaveChangesAsync(cancellationToken);
await _mediator.Publish(new PositionCreated(position), cancellationToken);
return _mapper.Map<PositionDto>(position);
}Soft Delete
public async Task<bool> Handle(
DeleteContextCommand request,
CancellationToken cancellationToken)
{
DbFusionContext context = await _db.Contexts.FindAsync(request.ContextId)
?? throw new NotFoundException($"Context {request.ContextId} not found");
context.IsArchived = true;
context.ArchivedAt = DateTime.UtcNow;
context.ArchivedBy = _currentUser.Id;
await _db.SaveChangesAsync(cancellationToken);
await _mediator.Publish(new ContextArchived(context), cancellationToken);
return true;
}Transactional Consistency
public async Task<ContextDto> Handle(
CreateContextCommand request,
CancellationToken cancellationToken)
{
using IDbContextTransaction transaction = await _db.Database.BeginTransactionAsync(cancellationToken);
try
{
DbFusionContext context = new DbFusionContext { ... };
await _db.Contexts.AddAsync(context, cancellationToken);
// Create default positions, approvals, etc. in same transaction
DbPosition defaultPosition = new DbPosition { ContextId = context.Id, ... };
await _db.Positions.AddAsync(defaultPosition, cancellationToken);
await _db.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
// Publish events AFTER transaction succeeds
await _mediator.Publish(new ContextCreated(context), cancellationToken);
return _mapper.Map<ContextDto>(context);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}Cross-Service & External API Integration
Calling Other Fusion Services
Typed HTTP Client Pattern
Fusion services use typed HTTP clients for service-to-service calls:
// Backend service example structure
public interface IPeopleApiClient
{
Task<PersonDto> GetPerson(string personId);
Task<List<PersonDto>> SearchPeople(string query);
Task<bool> UserHasRole(string personId, string role);
}
// Implementation handles:
// - Base URL configuration
// - Authorization token acquisition
// - Error handling and retries
// - Timeout managementWell-defined contracts. Typed interfaces (type-safe, testable).
How Services Find Each Other
Services resolve peers via config + Fusion service discovery client:
1. Service startup → Load base URLs from config / Key Vault
2. Need to call People API → Use IFusionServiceDiscovery to get the endpoint
3. Discovery returns → Current URL for People API
4. Service calls → People API via typed HTTP clientBase URLs managed via Azure App Configuration and Key Vault.
Why: Stable, centrally managed endpoints. Typed HTTP client handles auth, retries, and timeouts.
Authentication Between Services
When Service A calls Service B:
1. Service A needs token for Service B
2. Service A requests token (using its own credentials)
3. Token returned with Service B's scope
4. Service A includes token in call to Service B
5. Service B validates token → Check caller service identityService-to-service: service principal, not user identity.
---
External API Integration
When Fusion services call external systems (outside Equinor):
Pattern: HTTP Client with Retry
{
"ExternalApis": {
"SAPSystem": {
"BaseUrl": "https://sap.example.com/api",
"RetryPolicy": "ExponentialBackoff",
"RetryCount": 3,
"TimeoutSeconds": 30
}
}
}Pattern: API Keys & Secrets
Credentials stored in:
- Key Vault (Azure Key Vault) — for production and development
- User secrets (
dotnet user-secrets) — for local development (never committed to source control) - Environment variables — for containerized deployment
Warning: Never store secrets inappsettings.jsonor files tracked by source control, even for development. Usedotnet user-secretsor environment variables instead.
As consumer: keys are managed by the service team.
Pattern: Request Mapping
// Fusion model
public class PersonDto
{
public string Name { get; set; }
public string Email { get; set; }
}
// External system model
public class SAPPerson
{
public string FULLNAME { get; set; } // Different casing
public string EMAIL_ADDRESS { get; set; }
public string STATUS { get; set; } // Extra field
}
// Mapper converts between formats
PersonDto fusion = mapper.Map<PersonDto>(sapPerson);---
Webhook Handling
Note: Webhook patterns observed in Fusion ecosystem. Header names, signature formats, registration endpoints vary by service and external system. Verify in target service docs.
Receiving Webhooks from External Systems
Some Fusion services receive webhooks from external systems (e.g. CommonLib) via registered callback URL:
POST https://{fusion-host}/api/webhook/inbound
Content-Type: application/json
x-commonlib-sig: {signature} // Signature header name varies by provider
{ /* event body */ }Validate signature against shared secret. Header name and algorithm depend on provider's contract.
Registering Webhooks
Managed by external API, not Fusion. Example: CommonLib subscribes via external endpoint:
{
"callbackUrl": "https://{fusion-host}/api/webhook/inbound",
"secret": "{shared-secret}",
"library": "ProjectMaster",
"enabled": true
}Signature Validation (Illustrative)
General HMAC pattern. Algorithm (SHA-1, SHA-256), header, format depend on provider:
// Illustrative pattern — verify actual header name, algorithm, and format
// with the specific webhook provider's documentation
byte[] keyBytes = System.Text.Encoding.UTF8.GetBytes(secret);
request.EnableBuffering();
byte[] bodyBytes;
using (var bodyStream = new System.IO.MemoryStream())
{
await request.Body.CopyToAsync(bodyStream);
bodyBytes = bodyStream.ToArray();
request.Body.Position = 0;
}
// Algorithm depends on provider (HMACSHA1, HMACSHA256, etc.)
byte[] computedSignature;
using (var hmac = new System.Security.Cryptography.HMACSHA256(keyBytes))
{
computedSignature = hmac.ComputeHash(bodyBytes);
}
string headerValue = request.Headers["x-provider-signature"].ToString();
if (string.IsNullOrEmpty(headerValue))
{
return Results.Unauthorized(); // Missing signature
}
// Compare using constant-time comparison to prevent timing attacks
byte[] providedSignature;
try
{
providedSignature = Convert.FromHexString(headerValue);
}
catch (FormatException)
{
return Results.Unauthorized(); // Malformed signature
}
if (providedSignature.Length != computedSignature.Length)
{
return Results.Unauthorized(); // Signature length mismatch
}
if (!System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
computedSignature, providedSignature))
{
return Results.Unauthorized(); // Signature mismatch
}---
Resilience Patterns
Services handle external API calls carefully:
Timeout Management
// All external calls have timeouts
TimeSpan timeout = TimeSpan.FromSeconds(30);
using CancellationTokenSource cts = new CancellationTokenSource(timeout);
HttpResponseMessage response = await _httpClient.GetAsync(url, cts.Token);Why: Prevents a slow external API from blocking the service
Circuit Breaker
When external API is failing:
1. Initial: Requests go through
2. Errors exceed threshold → Circuit opens
3. Circuit open: Requests immediately fail (fast-fail)
4. After delay: Circuit half-open → Try one request
5. If succeeds: Circuit closes → Back to normalResult: Prevents hammering a broken external API.
Fallback & Degraded Mode
If external integration fails:
try
{
SAPPersonDto sap = await _sapClient.GetPerson(personId);
return enriched(sap);
}
catch (SAPUnavailableException)
{
// SAP is down; use cached data or return minimal response
PersonDto? cached = _cache.Get(personId);
return cached ?? new MinimalPersonDto();
}---
When to Cache External Data
Cache Pattern
public async Task<PersonDto> GetPerson(string id)
{
PersonDto? cached = _cache.Get<PersonDto>($"person:{id}");
if (cached != null)
return cached;
PersonDto person = await _sapClient.GetPerson(id);
_cache.Set($"person:{id}", person, expiration: TimeSpan.FromHours(1));
return person;
}When to cache:
- Data doesn't change frequently (person profile)
- Read volume is high
- External API has rate limits or latency
When NOT to cache:
- Data must be current (real-time inventory)
- Small read volume (caching overhead > benefit)
- Consistency is critical
---
Testing External Integration
Mock/Stub Pattern
// Interfaces allow swapping real vs mock
public interface IExternalApiClient
{
Task<PersonDto> GetPerson(string id);
}
// Production: Real HTTP client
public class HttpExternalApiClient : IExternalApiClient { }
// Testing: Mock
public class TestExternalApiClient : IExternalApiClient
{
public Task<PersonDto> GetPerson(string id)
{
return Task.FromResult(new PersonDto { Name = "Test User" });
}
}Contract Testing
1. External API publishes its OpenAPI/Swagger spec
2. Your service generates tests from spec
3. Tests verify: "If I call this endpoint with this request, I get this response"
4. Detect breaking changes early---
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| No timeout | Slow API blocks service | Always set request timeout |
| No retry | Transient failure crashes feature | Retry transient errors (500, 503, 429) |
| No circuit breaker | Cascade failures | Use resilience library (Polly, etc.) |
| Exposing secrets | Security breach | Use Key Vault, not config files |
| No caching | Rate limit hit | Cache appropriately |
| Tight coupling | Hard to test/change | Use interfaces and dependency injection |
Input Validation & Error Handling
Validation Pattern
All input validated before processing. Layers:
1. Request-Level Validation (HTTP)
✓ Content-Type: application/json
✓ Authorization header present
✓ API version supportedErrors: 400 Bad Request, 401 Unauthorized, 415 Unsupported Media Type
2. Model Validation (FluentValidation)
Before business logic runs, all input is validated:
// Backend service validation rules:
Email must be a valid format
Name must be 1-200 characters
StartDate must be before EndDateError response (ProblemDetails with validation extensions):
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"error": {
"code": "ModelValidationError",
"message": "Model contained 2 errors",
"errors": [
{ "property": "email", "message": "Email must be a valid email address" },
{ "property": "startDate", "message": "Start date must be before end date" }
]
},
"errors": {
"email": ["Email must be a valid email address"],
"startDate": ["Start date must be before end date"]
},
"traceId": "...",
"timestamp": "..."
}3. Business Logic Validation
After model validation, business rules are checked:
// Example business rules:
Cannot assign position holder outside hiring period
Cannot delete context with active positions
Cannot modify archived contextError response (via FusionApiError.InvalidOperation or thrown as domain exception):
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Invalid operation",
"status": 400,
"detail": "Cannot delete context with active positions",
"error": {
"code": "InvalidOperation",
"message": "Cannot delete context with active positions"
},
"traceId": "...",
"timestamp": "..."
}---
Error Response Format
Services return RFC 7807 ProblemDetails with Fusion extensions. Envelope consistent across services using this pattern; error content varies. Pre-convention/custom-middleware services may differ.
Validation errors (400)
FluentValidation failures produce a ProblemDetails with both a legacy error object and a standard errors dictionary:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"error": {
"code": "ModelValidationError",
"message": "Model contained 2 errors",
"errors": [
{ "property": "email", "message": "Email must be a valid email address", "attemptedValue": "bad" },
{ "property": "startDate", "message": "Must be before end date" }
]
},
"errors": {
"email": ["Email must be a valid email address"],
"startDate": ["Must be before end date"]
},
"traceId": "00-abc123...",
"timestamp": "2026-04-17T10:30:00Z"
}Top-levelerrors(field → message[]) matches ASP.NET Core format.error.errorsis legacy with extra context (attemptedValue). Prefer top-levelerrors.
Domain and operational errors (404, 409, 424, etc.)
Domain errors use same envelope with error extension:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Resource not found",
"status": 404,
"instance": "abc-123",
"error": {
"code": "ResourceNotFound",
"message": "Context abc-123 was not found",
"resourceIdentifier": "abc-123"
},
"traceId": "00-abc123...",
"timestamp": "2026-04-17T10:30:00Z"
}Controllers: FusionApiError factory methods (common patterns from Fusion.AspNetCore):
FusionApiError.NotFound(resource, message)→ 404FusionApiError.InvalidOperation(code, message)→ 400FusionApiError.ResourceExists(resource, message, exception)→ 409FusionApiError.Forbidden(message)→ 403FusionApiError.FailedDependency(code, message)→ 424FusionApiError.IncorrectETag(message)→ 409
Signatures vary by service version. Verify via source or mcp_fusion_search_backend_code.Unhandled exceptions (500, middleware-caught)
Common exception middleware (e.g. ApiExceptionMiddleware) maps exception types to HTTP status. Typical:
NotFoundError→ 404NotAuthorizedError→ 403 (may includeaccessRequirementsin the error)ResourceExistsError→ 409ReadOnlyModeError→ 500 with read-only context
Illustrative of fusion-libraries pattern. Not all services implement every mapping — check target middleware config.
Common Error Codes
| Code | HTTP Status | Meaning |
|---|---|---|
ModelValidationError | 400 | Input doesn't match schema or FluentValidation rules |
ResourceNotFound | 404 | Resource doesn't exist |
InvalidOperation | 400 | Business logic constraint violated |
ResourceExists / exception type | 409 | Resource already exists |
NotAuthorized | 403 | Not authorized for action |
FailedDependency | 424 | Downstream service error |
Gone | 410 | Resource has been removed |
NotImplemented | 501 | Endpoint not yet implemented |
---
How to Handle Error Responses
For Validation Errors (400)
1. Check the errors dictionary for field-level messages 2. Show the user the specific error message 3. Let them correct the input 4. Retry the request
Frontend example:
if (response.status === 400) {
const body = await response.json();
// body.errors is a Record<string, string[]>
const fieldErrors: Record<string, string> = {};
for (const [field, messages] of Object.entries(body.errors)) {
fieldErrors[field] = (messages as string[])[0];
}
// Show errors in form next to fields
form.setErrors(fieldErrors);
}For Business Rule Violations (400)
Business rules require additional steps:
1. Parse error.code and error.message from the ProblemDetails response 2. Either:
- Fix the prerequisite (delete active positions first, etc.)
- Take an alternate action (update instead of delete)
- Contact someone with higher permissions
Example (returned via FusionApiError.InvalidOperation):
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Invalid operation",
"status": 400,
"detail": "Cannot delete context with active positions",
"error": {
"code": "InvalidOperation",
"message": "Cannot delete context with active positions"
},
"traceId": "...",
"timestamp": "..."
}For Conflict (409)
Resource already exists or state changed:
1. Fetch current state 2. Decide: overwrite (send version), merge, or error to user
Optimistic lock pattern:
Request: PUT /contexts/{id}
Headers: { "If-Match": "\"etag-value\"" }
Body: { "title": "New Title" }{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.8",
"title": "Resource version does not match",
"status": 409,
"error": {
"code": "IncorrectETag",
"message": "ETag did not match, the resource might have been updated. Refresh and try again."
},
"traceId": "...",
"timestamp": "..."
}---
Retry Strategy
Retry only for operations safe to repeat:
- Inherently idempotent methods (
GET,HEAD) - Write operations protected by idempotency key,
ETag/If-Match, or explicit deduplication
Avoid auto-retries for non-idempotent writes (e.g. POST/command operations) unless API contract explicitly guarantees safe retry.
| Status | Retryable? | Strategy |
|---|---|---|
| 400, 401, 403, 404, 422 | ❌ No | Fix the request; retrying won't help |
| 409 | ⚠️ Depends | Do not blindly retry version/state conflicts; fetch current state, resolve the conflict, then retry only with an updated request. Use backoff only for explicitly transient conflicts |
| 429, 503, 504 | ✅ Conditional | Retry with exponential backoff only for idempotent or deduplication-protected requests |
| 500 | ✅ Conditional | Retry once only if the operation is idempotent or protected; if it still fails, investigate |
Recommended backoff:
Attempt 1: immediate
Attempt 2: wait 100ms + random jitter
Attempt 3: wait 200ms + random jitter
Attempt 4: wait 400ms + random jitter
...stop after 3-4 attempts---
Testing Validation
Before sending requests to production:
1. Happy path: Valid data with all required fields 2. Missing required fields: Each required field removed one at a time 3. Invalid formats:
- Wrong type (string instead of number)
- Invalid email format
- Date outside allowed range
4. Business rules:
- Conflicting values (StartDate > EndDate)
- Violating constraints (duplicate, out of bounds)
- State violations (can't transition from this state)
Most Fusion services include example payloads in their Swagger/OpenAPI documentation.
Related skills
How it compares
Pick fusion-backend-dev for consuming existing Fusion APIs; use fusion-research for pure architecture discussions without code references.
FAQ
Does fusion-backend-dev modify Fusion backend services?
fusion-backend-dev does not modify Fusion backend services. The skill explains existing C# APIs and patterns from retrieved reference code and routes backend changes to fusion-services-develop or service-specific repos.
What MCP tool does fusion-backend-dev use?
fusion-backend-dev uses mcp_fusion_search_backend_code to locate Fusion backend reference implementations. Searches typically start with top 3–5 results and return repository, service, and file path metadata.