Skip to content

Commit 3fe17e5

Browse files
committed
feat: add Reporting README, update docs with recent features, add Clarity tracking
- Add README.md for OpenSleigh.Reporting NuGet package - Update .csproj with PackageReadmeFile for NuGet display - Add docs for Reporting & Monitoring (REST endpoints, setup, security) - Add docs for Multiple Saga Starters feature - Update homepage and installation with Reporting package - Add MS Clarity tracking via head_custom.html - Update copilot-instructions.md
1 parent 8d9aa99 commit 3fe17e5

File tree

12 files changed

+598
-211
lines changed

12 files changed

+598
-211
lines changed

.github/copilot-instructions.md

Lines changed: 253 additions & 208 deletions
Large diffs are not rendered by default.

docs/_includes/head_custom.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script type="text/javascript">
2+
(function(c,l,a,r,i,t,y){
3+
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
4+
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
5+
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
6+
})(window, document, "clarity", "script", "a7evv7zote");
7+
</script>

docs/how-to/idempotency.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
layout: default
33
title: Idempotency
44
parent: How-To
5-
nav_order: 5
5+
nav_order: 7
66
---
77

88
# Idempotency

docs/how-to/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ This section contains step-by-step guides to help you get started with OpenSleig
1212
- [Installation]({% link how-to/installation.md %}) — Install the NuGet packages
1313
- [First Steps]({% link how-to/first-steps.md %}) — Configure transport, persistence, and your first Saga
1414
- [Handling Messages]({% link how-to/handling-messages.md %}) — Start, handle, and stop Sagas
15+
- [Multiple Starters]({% link how-to/multiple-starters.md %}) — Start a saga from different message types
1516
- [Publishing Messages]({% link how-to/publishing-messages.md %}) — Publish messages and use publish-only mode
17+
- [Reporting & Monitoring]({% link how-to/reporting.md %}) — Query saga state via REST endpoints
1618
- [Idempotency]({% link how-to/idempotency.md %}) — Ensure at-most-once message processing
1719
- [Logging Levels]({% link how-to/logging-levels.md %}) — Configure logging for OpenSleigh

docs/how-to/installation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ OpenSleigh Core, Persistence, and Transport libraries are all available as NuGet
2424

2525
- [RabbitMQ](https://www.nuget.org/packages/OpenSleigh.Transport.RabbitMQ/)
2626
- [Kafka](https://www.nuget.org/packages/OpenSleigh.Transport.Kafka/)
27+
28+
## Reporting
29+
30+
- [Reporting](https://www.nuget.org/packages/OpenSleigh.Reporting/) — ASP.NET Core Minimal API endpoints for saga state querying and monitoring

docs/how-to/logging-levels.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
layout: default
33
title: Logging Levels
44
parent: How-To
5-
nav_order: 6
5+
nav_order: 8
66
---
77

88
# Logging Levels

docs/how-to/multiple-starters.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
layout: default
3+
title: Multiple Starters
4+
parent: How-To
5+
nav_order: 4
6+
---
7+
8+
# Multiple Saga Starters
9+
10+
By default, a saga is initiated by a single message type via the `IStartedBy<TMessage>` interface. Starting with v3.1.0, a saga can implement **multiple** `IStartedBy<>` interfaces, allowing it to be started by different message types.
11+
12+
## Use Cases
13+
14+
This is useful when a workflow can be triggered from different entry points. For example, an order processing saga might start when either an order is placed by a customer or when a payment is received from a payment gateway:
15+
16+
```csharp
17+
public record OrderPlaced(string OrderId) : IMessage;
18+
public record PaymentReceived(string OrderId, decimal Amount) : IMessage;
19+
public record ShipOrder : IMessage;
20+
21+
public class OrderSaga :
22+
Saga<OrderState>,
23+
IStartedBy<OrderPlaced>,
24+
IStartedBy<PaymentReceived>,
25+
IHandleMessage<ShipOrder>
26+
{
27+
public OrderSaga(ISagaInstance<OrderState> context) : base(context) { }
28+
29+
public ValueTask HandleAsync(
30+
IMessageContext<OrderPlaced> context,
31+
CancellationToken cancellationToken = default)
32+
{
33+
this.Context.State.OrderId = context.Message.OrderId;
34+
this.Context.State.IsOrderReceived = true;
35+
36+
if (this.Context.State.IsPaymentReceived)
37+
this.Publish(new ShipOrder());
38+
39+
return ValueTask.CompletedTask;
40+
}
41+
42+
public ValueTask HandleAsync(
43+
IMessageContext<PaymentReceived> context,
44+
CancellationToken cancellationToken = default)
45+
{
46+
this.Context.State.OrderId = context.Message.OrderId;
47+
this.Context.State.IsPaymentReceived = true;
48+
49+
if (this.Context.State.IsOrderReceived)
50+
this.Publish(new ShipOrder());
51+
52+
return ValueTask.CompletedTask;
53+
}
54+
55+
public ValueTask HandleAsync(
56+
IMessageContext<ShipOrder> context,
57+
CancellationToken cancellationToken = default)
58+
{
59+
this.Context.MarkAsCompleted();
60+
return ValueTask.CompletedTask;
61+
}
62+
}
63+
```
64+
65+
## How It Works
66+
67+
When a starter message arrives, OpenSleigh looks up the saga instance by its correlation ID:
68+
69+
- If **no instance exists**, a new one is created.
70+
- If an instance **already exists** (created by a different starter message), the message is handled as a regular message on the existing instance.
71+
72+
## Concurrency and Optimistic Locking
73+
74+
When multiple starter messages for the same correlation ID arrive at the same time, OpenSleigh uses optimistic concurrency control to ensure only **one** saga instance is created. If a concurrent creation attempt is detected, an `OptimisticLockException` is thrown internally and the operation is retried — the second message is then handled on the existing instance rather than creating a duplicate.
75+
76+
This is handled automatically by the framework; no additional code is needed in your saga implementation.

docs/how-to/publishing-messages.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
layout: default
33
title: Publishing Messages
44
parent: How-To
5-
nav_order: 4
5+
nav_order: 5
66
---
77

88
# Publishing Messages

docs/how-to/reporting.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
---
2+
layout: default
3+
title: Reporting & Monitoring
4+
parent: How-To
5+
nav_order: 6
6+
---
7+
8+
# Reporting & Monitoring
9+
10+
The `OpenSleigh.Reporting` package provides ready-to-use ASP.NET Core Minimal API endpoints for querying saga state at runtime. This is useful for building dashboards, debugging distributed workflows, and monitoring saga progress.
11+
12+
## Installation
13+
14+
Install the NuGet package:
15+
16+
```
17+
dotnet add package OpenSleigh.Reporting
18+
```
19+
20+
## Setup
21+
22+
Register the reporting services and map the endpoints in your ASP.NET Core application:
23+
24+
```csharp
25+
var builder = WebApplication.CreateBuilder(args);
26+
27+
builder.Services.AddOpenSleigh(cfg =>
28+
{
29+
// your transport and persistence configuration
30+
});
31+
32+
builder.Services.AddOpenSleighReporting();
33+
34+
var app = builder.Build();
35+
36+
app.MapOpenSleighReporting(); // default prefix: /opensleigh
37+
38+
app.Run();
39+
```
40+
41+
You can customize the endpoint prefix:
42+
43+
```csharp
44+
app.MapOpenSleighReporting("/api/sagas");
45+
```
46+
47+
## REST Endpoints
48+
49+
All endpoints are grouped under the configured prefix (default `/opensleigh`):
50+
51+
| Method | Path | Description |
52+
|--------|------|-------------|
53+
| `GET` | `/sagas` | List saga instances (paginated, with optional filters) |
54+
| `GET` | `/sagas/{instanceId}` | Get a saga instance by its unique ID |
55+
| `GET` | `/sagas/correlation/{correlationId}?sagaType=...` | Get a saga instance by correlation ID and saga type |
56+
| `GET` | `/sagas/types` | List all registered message types with saga handlers |
57+
58+
### Query Parameters
59+
60+
The **list sagas** endpoint (`GET /sagas`) supports the following query parameters:
61+
62+
| Parameter | Type | Default | Description |
63+
|-----------|------|---------|-------------|
64+
| `sagaType` | `string?` | `null` | Filter by saga type name |
65+
| `isCompleted` | `bool?` | `null` | Filter by completion status |
66+
| `page` | `int` | `1` | Page number (must be ≥ 1) |
67+
| `pageSize` | `int` | `20` | Results per page (1–100) |
68+
69+
The **get by correlation ID** endpoint (`GET /sagas/correlation/{correlationId}`) requires a `sagaType` query parameter.
70+
71+
### Response Format
72+
73+
The list endpoint returns a `PagedResult<SagaInstanceInfo>`:
74+
75+
```json
76+
{
77+
"items": [
78+
{
79+
"instanceId": "0193a7e2-...",
80+
"correlationId": "0193a7e2-...",
81+
"triggerMessageId": "0193a7e2-...",
82+
"sagaType": "OrderSaga",
83+
"sagaStateType": "OrderState",
84+
"isCompleted": false,
85+
"isLocked": false,
86+
"processedMessages": [
87+
{ "messageId": "0193a7e2-...", "when": "2025-01-10T12:00:00Z" }
88+
],
89+
"stateData": { ... }
90+
}
91+
],
92+
"totalCount": 42,
93+
"page": 1,
94+
"pageSize": 20
95+
}
96+
```
97+
98+
## OpenAPI Support
99+
100+
On .NET 9+, `AddOpenSleighReporting()` automatically registers OpenAPI services, and `MapOpenSleighReporting()` maps the OpenAPI document endpoint. The generated document is available at `/openapi/v1.json`.
101+
102+
## Security
103+
104+
{: .warning }
105+
> The reporting endpoints expose saga state data, which may contain sensitive information. Always configure appropriate authentication and authorization middleware **before** calling `MapOpenSleighReporting()` in production environments.
106+
107+
For example:
108+
109+
```csharp
110+
app.UseAuthentication();
111+
app.UseAuthorization();
112+
113+
app.MapOpenSleighReporting()
114+
.RequireAuthorization();
115+
```
116+
117+
## Example: PizzaTracker Sample
118+
119+
The [PizzaTracker sample](https://github.com/mizrael/OpenSleigh/tree/develop/samples/OpenSleigh.Samples.PizzaTracker) demonstrates a complete setup with reporting endpoints. It uses in-memory transport and persistence with a pizza order saga that progresses through multiple stages (Received → Preparing → Baking → Quality Check → Out for Delivery → Delivered).
120+
121+
```csharp
122+
var builder = WebApplication.CreateBuilder(args);
123+
124+
builder.Services.AddOpenSleigh(cfg =>
125+
{
126+
cfg.UseInMemoryTransport()
127+
.UseInMemoryPersistence()
128+
.AddSaga<OrderSaga, OrderState>();
129+
});
130+
131+
builder.Services.AddOpenSleighReporting();
132+
133+
var app = builder.Build();
134+
135+
app.MapOpenSleighReporting();
136+
137+
app.MapPost("/orders", async (PlaceOrderRequest request, IMessageBus bus) =>
138+
{
139+
var message = new PlaceOrder(request.CustomerName, request.PizzaType);
140+
await bus.PublishAsync(message);
141+
return Results.Accepted();
142+
});
143+
144+
app.Run();
145+
```
146+
147+
After placing an order, use `GET /opensleigh/sagas` to track its progress through the workflow.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ These are the packages available at the moment:
3636
| [PostgreSQL](https://www.nuget.org/packages/OpenSleigh.Persistence.PostgreSQL/) | ![Nuget](https://img.shields.io/nuget/v/OpenSleigh.Persistence.PostgreSQL?style=flat-square) |
3737
| [RabbitMQ](https://www.nuget.org/packages/OpenSleigh.Transport.RabbitMQ/) | ![Nuget](https://img.shields.io/nuget/v/OpenSleigh.Transport.RabbitMQ?style=flat-square) |
3838
| [Kafka](https://www.nuget.org/packages/OpenSleigh.Transport.Kafka/) | ![Nuget](https://img.shields.io/nuget/v/OpenSleigh.Transport.Kafka?style=flat-square) |
39+
| [Reporting](https://www.nuget.org/packages/OpenSleigh.Reporting/) | ![Nuget](https://img.shields.io/nuget/v/OpenSleigh.Reporting?style=flat-square) |
3940

4041
In-depth instructions can be found in the [How-To]({% link how-to/installation.md %}) section.
4142

0 commit comments

Comments
 (0)