Skip to content

Commit 3f07ea1

Browse files
Docs improvements (#1024)
* Use the WebApplicationBuilder style * Update snippets * Update snippets * typos * Explain the difference between the filter types * Update snippets * typos, leftobvers, and update snippets * Fix link, remove reference to JSON.NET, update snippets * update snippets * Update snippets * typo, update snippets * typos * Update snippets * typos * Use snippets
1 parent ebb69d9 commit 3f07ea1

37 files changed

Lines changed: 935 additions & 549 deletions

README.md

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,15 @@ To start using ServiceComposer, follow the outlined steps:
3131
<!-- snippet: sample-startup -->
3232
<a id='snippet-sample-startup'></a>
3333
```cs
34-
public class Startup
35-
{
36-
public void ConfigureServices(IServiceCollection services)
37-
{
38-
services.AddRouting();
39-
services.AddViewModelComposition();
40-
}
34+
var builder = WebApplication.CreateBuilder();
35+
builder.Services.AddRouting();
36+
builder.Services.AddViewModelComposition();
4137

42-
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
43-
{
44-
app.UseRouting();
45-
app.UseEndpoints(builder => builder.MapCompositionHandlers());
46-
}
47-
}
38+
var app = builder.Build();
39+
app.MapCompositionHandlers();
40+
app.Run();
4841
```
49-
<sup><a href='/src/Snippets/BasicUsage/Startup.cs#L8-L23' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample-startup' title='Start of snippet'>anchor</a></sup>
42+
<sup><a href='/src/Snippets/BasicUsage/Startup.cs#L11-L19' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample-startup' title='Start of snippet'>anchor</a></sup>
5043
<!-- endSnippet -->
5144

5245
- Add a new .NET 10 class library project named `Sales.ViewModelComposition`.

docs/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ MVC Action results support allows composition handlers to set custom response re
4343

4444
### Serialization
4545

46-
By default, ServiceComposer serializes responses using the Newtonsoft JSON serializer. The built-in serialization support can be configured to serialize responses using a camel case or pascal case approach on a per-request basis by adding to the request an `Accept-Casing` custom HTTP header. For more information, refer to the [response serialization casing](response-serialization-casing.md) section. Or it's possible to take full control over the [response serialization settings on a case-by-case](custom-json-response-serialization-settings.md) by supplying at configuration time a customization function.
46+
By default, ServiceComposer serializes responses using `System.Text.Json`. The built-in serialization support can be configured to serialize responses using a camel case or pascal case approach on a per-request basis by adding to the request an `Accept-Casing` custom HTTP header. For more information, refer to the [response serialization casing](response-serialization-casing.md) section. Or it's possible to take full control over the [response serialization settings on a case-by-case](custom-json-response-serialization-settings.md) by supplying at configuration time a customization function.
4747

4848
Starting with version 1.9.0, regular MVC Output Formatters can be used to serialize the response model and honor the `Accept` HTTP header set by clients. When using output formatters, the serialization casing is controlled by the formatter configuration and not by ServiceComposer. For more information on using output formatters refers to the [output formatters serialization section](output-formatters-serialization.md).
4949

5050
### Authentication and Authorization
5151

52-
By leveraging ASP.NET Core 3.x Endpoints, ServiceComposer automatically supports authentication and authorization metadata attributes to express authentication and authorization requirements for routes. For more information, refer to the [Authentication and Authorization](authentication-authorization.md) section.
52+
By leveraging ASP.NET Core Endpoints, ServiceComposer automatically supports authentication and authorization metadata attributes to express authentication and authorization requirements for routes. For more information, refer to the [Authentication and Authorization](authentication-authorization.md) section.
5353

5454
### Endpoint filters
5555

docs/action-results.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ Using MVC action results require enabling output formatters support:
3434
<!-- snippet: action-results-required-config -->
3535
<a id='snippet-action-results-required-config'></a>
3636
```cs
37-
services.AddViewModelComposition(options =>
37+
builder.Services.AddViewModelComposition(options =>
3838
{
3939
options.ResponseSerialization.UseOutputFormatters = true;
4040
});
4141
```
42-
<sup><a href='/src/Snippets/ActionResult/UseSetActionResultHandler.cs#L37-L42' title='Snippet source file'>snippet source</a> | <a href='#snippet-action-results-required-config' title='Start of snippet'>anchor</a></sup>
42+
<sup><a href='/src/Snippets/ActionResult/UseSetActionResultHandler.cs#L39-L44' title='Snippet source file'>snippet source</a> | <a href='#snippet-action-results-required-config' title='Start of snippet'>anchor</a></sup>
4343
<!-- endSnippet -->
4444

4545
> [!NOTE]

docs/authentication-authorization.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Authentication and Authorization
22

3-
By virtue of leveraging ASP.NET Core 3.x Endpoints ServiceComposer automatically supports authentication and authorization metadata attributes to express authentication and authorization requirements on routes. For example, it's possible to use the `Authorize` attribute to specify that a handler requires authorization. The authorization process is the regular ASP.NET Core 3.x process and no special configuration is needed to plugin ServiceComposer:
3+
By virtue of leveraging ASP.NET Core Endpoints, ServiceComposer automatically supports authentication and authorization metadata attributes to express authentication and authorization requirements on routes. For example, it's possible to use the `Authorize` attribute to specify that a handler requires authorization. The authorization process is the regular ASP.NET Core process and no special configuration is needed to plug in ServiceComposer:
44

55
<!-- snippet: sample-handler-with-authorization -->
66
<a id='snippet-sample-handler-with-authorization'></a>
@@ -17,3 +17,58 @@ public class SampleHandlerWithAuthorization : ICompositionRequestsHandler
1717
```
1818
<sup><a href='/src/Snippets/SampleHandler/SampleHandler.cs#L10-L20' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample-handler-with-authorization' title='Start of snippet'>anchor</a></sup>
1919
<!-- endSnippet -->
20+
21+
## How it works
22+
23+
ServiceComposer registers composition endpoints using ASP.NET Core's endpoint routing system. Any endpoint metadata attributes — `[Authorize]`, `[AllowAnonymous]`, `[RequireAuthorization]`, custom policy attributes — are collected from all handlers registered for a route and merged onto the composed endpoint.
24+
25+
This means authorization is evaluated **before** composition handlers execute, as part of the standard ASP.NET Core middleware pipeline. If the request is not authorized, it is rejected and no composition handlers run.
26+
27+
## Multiple handlers with different authorization requirements
28+
29+
When multiple handlers are registered for the same route, their authorization metadata is merged. If any handler declares `[Authorize]`, the route requires authorization. The most restrictive combination applies.
30+
31+
For example, if one handler requires an authenticated user and another requires a specific policy, both requirements are enforced:
32+
33+
<!-- snippet: multiple-handlers-different-auth-requirements -->
34+
<a id='snippet-multiple-handlers-different-auth-requirements'></a>
35+
```cs
36+
public class SalesHandler : ICompositionRequestsHandler
37+
{
38+
[Authorize]
39+
[HttpGet("/product/{id}")]
40+
public Task Handle(HttpRequest request) { /* ... */ return Task.CompletedTask; }
41+
}
42+
43+
public class InventoryHandler : ICompositionRequestsHandler
44+
{
45+
[Authorize(Policy = "WarehouseStaff")]
46+
[HttpGet("/product/{id}")]
47+
public Task Handle(HttpRequest request) { /* ... */ return Task.CompletedTask; }
48+
}
49+
```
50+
<sup><a href='/src/Snippets/Authentication/AuthenticationSnippets.cs#L11-L25' title='Snippet source file'>snippet source</a> | <a href='#snippet-multiple-handlers-different-auth-requirements' title='Start of snippet'>anchor</a></sup>
51+
<!-- endSnippet -->
52+
53+
Both `[Authorize]` and `[Authorize(Policy = "WarehouseStaff")]` are collected and applied to the route, so callers must satisfy both requirements.
54+
55+
## Setup
56+
57+
No special configuration is required beyond the standard ASP.NET Core authentication/authorization setup. Add authentication and authorization middleware before `app.MapCompositionHandlers()`:
58+
59+
<!-- snippet: auth-middleware-setup -->
60+
<a id='snippet-auth-middleware-setup'></a>
61+
```cs
62+
var builder = WebApplication.CreateBuilder();
63+
builder.Services.AddViewModelComposition();
64+
builder.Services.AddAuthentication(); // configure your scheme here
65+
builder.Services.AddAuthorization();
66+
67+
var app = builder.Build();
68+
app.UseAuthentication();
69+
app.UseAuthorization();
70+
app.MapCompositionHandlers();
71+
app.Run();
72+
```
73+
<sup><a href='/src/Snippets/Authentication/AuthenticationSnippets.cs#L31-L42' title='Snippet source file'>snippet source</a> | <a href='#snippet-auth-middleware-setup' title='Start of snippet'>anchor</a></sup>
74+
<!-- endSnippet -->

docs/composition-filters.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Composition requests filter can be defined as attributes or as classes.
1010

1111
### Defining composition requests filters as attributes
1212

13-
Create an attribute that inherites from `CompositionRequestFilterAttribute` like in the following snippet:
13+
Create an attribute that inherits from `CompositionRequestFilterAttribute` like in the following snippet:
1414

1515
<!-- snippet: composition-filter-attribute -->
1616
<a id='snippet-composition-filter-attribute'></a>
@@ -49,7 +49,7 @@ public class SampleHandler : ICompositionRequestsHandler
4949

5050
### Defining composition requests filters as classes
5151

52-
Create a class te implements the `ICompositionRequestFilter<T>` interface, where the generic `T` parameter is the composition handler type to intercept:
52+
Create a class that implements the `ICompositionRequestFilter<T>` interface, where the generic `T` parameter is the composition handler type to intercept:
5353

5454
<!-- snippet: composition-filter-class -->
5555
<a id='snippet-composition-filter-class'></a>
@@ -68,4 +68,22 @@ public class SampleCompositionFilter : ICompositionRequestFilter<SampleHandler>
6868
The above snippet defines a filter intercepting requests to the `SampleHandler` composition handler.
6969

7070
> [!NOTE]
71-
> Filters defined as classes implementing the `ICompositionRequestFilter<T>` interface will be automatically registered in DI as transiten, and can use DI to resolve dependencies.
71+
> Filters defined as classes implementing the `ICompositionRequestFilter<T>` interface will be automatically registered in DI as transient, and can use DI to resolve dependencies.
72+
73+
## When to use composition filters vs endpoint filters
74+
75+
ServiceComposer provides two filter extension points. Choosing the right one depends on the scope of interception needed.
76+
77+
**Use [endpoint filters](endpoint-filters.md) when:**
78+
79+
- The logic applies to the entire composed request regardless of which handlers are involved (e.g. request logging, timing, global validation).
80+
- You want to short-circuit the whole composition before any handler runs.
81+
- The logic is independent of specific handler types.
82+
83+
**Use composition filters when:**
84+
85+
- The logic is specific to one particular composition handler type.
86+
- Different handlers on the same route need different pre/post processing (e.g. handler-specific authorization checks, handler-specific input validation).
87+
- You want to use the attribute form (`[SampleCompositionFilter]`) to keep the filter declaration co-located with the handler method.
88+
89+
In short: endpoint filters are coarse-grained (the whole endpoint), composition filters are fine-grained (a specific handler).

docs/composition-over-controllers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ ServiceComposer can be used to enhance a MVC web application by adding compositi
55
<!-- snippet: enable-composition-over-controllers -->
66
<a id='snippet-enable-composition-over-controllers'></a>
77
```cs
8-
services.AddViewModelComposition(options =>
8+
builder.Services.AddViewModelComposition(options =>
99
{
1010
options.EnableCompositionOverControllers();
1111
});
1212
```
13-
<sup><a href='/src/Snippets/CompositionOverController.cs#L10-L15' title='Snippet source file'>snippet source</a> | <a href='#snippet-enable-composition-over-controllers' title='Start of snippet'>anchor</a></sup>
13+
<sup><a href='/src/Snippets/CompositionOverController.cs#L12-L17' title='Snippet source file'>snippet source</a> | <a href='#snippet-enable-composition-over-controllers' title='Start of snippet'>anchor</a></sup>
1414
<!-- endSnippet -->
1515

1616
Once composition over controllers is enabled, ServiceComposer will inject a MVC filter to intercept all controllers invocations. If a route matches a regular controller and a set of composition handlers ServiceComposer will invoke the matching handlers after the controller and before the view is rendered.

docs/contract-less-composition-requests-handlers.md

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
_Available starting version 4.2.0-beta.1_
44

5-
Contract-less composition handlers allow to write composition handlers using a syntax like the following:
5+
Contract-less composition handlers allow writing composition handlers using a syntax like the following:
66

77
<!-- snippet: contract-less-handler-sample -->
88
<a id='snippet-contract-less-handler-sample'></a>
@@ -81,9 +81,11 @@ Both classes will be registered in DI, allowing the injection of dependencies in
8181

8282
Parameters decorated with `[FromServices]` are resolved from the DI container at request time. For example:
8383

84-
```csharp
84+
<!-- snippet: contract-less-handler-from-services -->
85+
<a id='snippet-contract-less-handler-from-services'></a>
86+
```cs
8587
[CompositionHandler]
86-
class SampleCompositionHandler
88+
class SampleCompositionHandlerWithServices
8789
{
8890
[HttpGet("/sample/{id}")]
8991
public Task SampleMethod(int id, [FromServices] IMyService myService)
@@ -92,20 +94,34 @@ class SampleCompositionHandler
9294
}
9395
}
9496
```
97+
<sup><a href='/src/Snippets/Contractless.CompositionHandlers/SampleCompositionHandlerWithServices.cs#L13-L23' title='Snippet source file'>snippet source</a> | <a href='#snippet-contract-less-handler-from-services' title='Start of snippet'>anchor</a></sup>
98+
<!-- endSnippet -->
9599

96100
The source generator will emit a `[BindFromServices<T>]` attribute for the parameter and resolve it via the DI container when the request is handled:
97101

98-
```csharp
99-
[HttpGetAttribute("/sample/{id}")]
100-
[BindFromRoute<Int32>("id")]
101-
[BindFromServices<IMyService>("myService")]
102-
public Task Handle(HttpRequest request)
102+
<!-- snippet: contract-less-handler-from-services-generated -->
103+
<a id='snippet-contract-less-handler-from-services-generated'></a>
104+
```cs
105+
// <auto-generated/>
106+
#pragma warning disable SC0001
107+
[EditorBrowsable(EditorBrowsableState.Never)]
108+
class SampleCompositionHandlerWithServices_SampleMethod_int_id_IMyService_myService(
109+
SampleCompositionHandlerWithServices userHandler) : ICompositionRequestsHandler
103110
{
104-
var ctx = HttpRequestExtensions.GetCompositionContext(request);
105-
var arguments = ctx.GetArguments(this);
106-
var p0_id = ModelBindingArgumentExtensions.Argument<Int32>(arguments, "id", BindingSource.Path);
107-
var p1_myService = ModelBindingArgumentExtensions.Argument<IMyService>(arguments, "myService", BindingSource.Services);
111+
[HttpGetAttribute("/sample/{id}")]
112+
[BindFromRoute<int>("id")]
113+
[BindFromServices<IMyService>("myService")]
114+
public Task Handle(HttpRequest request)
115+
{
116+
var ctx = HttpRequestExtensions.GetCompositionContext(request);
117+
var arguments = ctx.GetArguments(this);
118+
var p0_id = ModelBindingArgumentExtensions.Argument<int>(arguments, "id", BindingSource.Path);
119+
var p1_myService = ModelBindingArgumentExtensions.Argument<IMyService>(arguments, "myService", BindingSource.Services);
108120

109-
return userHandler.SampleMethod(p0_id, p1_myService);
121+
return userHandler.SampleMethod(p0_id, p1_myService);
122+
}
110123
}
124+
#pragma warning restore SC0001
111125
```
126+
<sup><a href='/src/Snippets/Contractless.CompositionHandlers/SampleCompositionHandlerWithServices.cs#L25-L46' title='Snippet source file'>snippet source</a> | <a href='#snippet-contract-less-handler-from-services-generated' title='Start of snippet'>anchor</a></sup>
127+
<!-- endSnippet -->

docs/custom-http-status-codes.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Custom HTTP status codes in ASP.NET Core 3.x
1+
# Custom HTTP status codes
22

3-
The response status code can be set in requests handlers and it'll be honored by the composition pipeline. To set a custom response status code the following snippet can be used:
3+
The response status code can be set in composition handlers and it will be honored by the composition pipeline. To set a custom response status code the following snippet can be used:
44

55
<!-- snippet: sample-handler-with-custom-status-code -->
66
<a id='snippet-sample-handler-with-custom-status-code'></a>
@@ -20,5 +20,36 @@ public class SampleHandlerWithCustomStatusCode : ICompositionRequestsHandler
2020
<sup><a href='/src/Snippets/SampleHandler/SampleHandler.cs#L22-L34' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample-handler-with-custom-status-code' title='Start of snippet'>anchor</a></sup>
2121
<!-- endSnippet -->
2222

23-
> [!NOTE]
24-
> Requests handlers are executed in parallel in a non-deterministic way, setting the response code in more than one handler can have unpredictable effects.
23+
> [!WARNING]
24+
> Composition handlers execute in parallel in a non-deterministic order. If more than one handler sets the response status code, the final code written to the response is unpredictable. Only one handler in a composition group should set the status code.
25+
26+
## Recommended approach: use action results instead
27+
28+
For error scenarios (validation failures, not found, forbidden), prefer using [MVC Action results](action-results.md) via `request.SetActionResult(result)`. Action results are designed for exactly this case — ServiceComposer guarantees only the first handler to call `SetActionResult` takes effect, making the behavior deterministic:
29+
30+
<!-- snippet: set-action-result-preferred -->
31+
<a id='snippet-set-action-result-preferred'></a>
32+
```cs
33+
public class SampleHandler : ICompositionRequestsHandler
34+
{
35+
[HttpGet("/sample/{id}")]
36+
public Task Handle(HttpRequest request)
37+
{
38+
if (!IsValid(request))
39+
{
40+
request.SetActionResult(new BadRequestResult());
41+
return Task.CompletedTask;
42+
}
43+
44+
var vm = request.GetComposedResponseModel();
45+
vm.Data = "...";
46+
return Task.CompletedTask;
47+
}
48+
49+
bool IsValid(HttpRequest request) => request.RouteValues["id"] != null;
50+
}
51+
```
52+
<sup><a href='/src/Snippets/SampleHandler/SetActionResultHandler.cs#L8-L27' title='Snippet source file'>snippet source</a> | <a href='#snippet-set-action-result-preferred' title='Start of snippet'>anchor</a></sup>
53+
<!-- endSnippet -->
54+
55+
Setting a status code directly on `HttpContext.Response` is appropriate only when a handler is the sole authority on the response code for its route, or when using a non-MVC endpoint where action results are not available.

0 commit comments

Comments
 (0)