Skip to content

Commit

Permalink
Merge pull request #168 from ServiceComposer/custom-status-code
Browse files Browse the repository at this point in the history
Support custom HTTP status code
  • Loading branch information
mauroservienti authored Aug 10, 2020
2 parents 74027f3 + 21c4983 commit 2bbdcd1
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 24 deletions.
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public class SampleHandler : ICompositionRequestsHandler
}
}
```
<sup><a href='/src/Snippets.NetCore3x/SampleHandler/SampleHandler.cs#L9-L18' title='File snippet `net-core-3x-sample-handler` was extracted from'>snippet source</a> | <a href='#snippet-net-core-3x-sample-handler' title='Navigate to start of snippet `net-core-3x-sample-handler`'>anchor</a></sup>
<sup><a href='/src/Snippets.NetCore3x/SampleHandler/SampleHandler.cs#L10-L19' title='File snippet `net-core-3x-sample-handler` was extracted from'>snippet source</a> | <a href='#snippet-net-core-3x-sample-handler' title='Navigate to start of snippet `net-core-3x-sample-handler`'>anchor</a></sup>
<!-- endsnippet -->

#### Authentication and Authorization
Expand All @@ -77,9 +77,33 @@ public class SampleHandlerWithAuthorization : ICompositionRequestsHandler
}
}
```
<sup><a href='/src/Snippets.NetCore3x/SampleHandler/SampleHandler.cs#L20-L30' title='File snippet `net-core-3x-sample-handler-with-authorization` was extracted from'>snippet source</a> | <a href='#snippet-net-core-3x-sample-handler-with-authorization' title='Navigate to start of snippet `net-core-3x-sample-handler-with-authorization`'>anchor</a></sup>
<sup><a href='/src/Snippets.NetCore3x/SampleHandler/SampleHandler.cs#L21-L31' title='File snippet `net-core-3x-sample-handler-with-authorization` was extracted from'>snippet source</a> | <a href='#snippet-net-core-3x-sample-handler-with-authorization' title='Navigate to start of snippet `net-core-3x-sample-handler-with-authorization`'>anchor</a></sup>
<!-- endsnippet -->

#### Custom HTTP status codes in ASP.NET Core 3.x

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:

<!-- snippet: net-core-3x-sample-handler-with-custom-status-code -->
<a id='snippet-net-core-3x-sample-handler-with-custom-status-code'></a>
```cs
public class SampleHandlerWithCustomStatusCode : ICompositionRequestsHandler
{
[HttpGet("/sample/{id}")]
public Task Handle(HttpRequest request)
{
var response = request.HttpContext.Response;
response.StatusCode = (int)HttpStatusCode.Forbidden;

return Task.CompletedTask;
}
}
```
<sup><a href='/src/Snippets.NetCore3x/SampleHandler/SampleHandler.cs#L33-L45' title='File snippet `net-core-3x-sample-handler-with-custom-status-code` was extracted from'>snippet source</a> | <a href='#snippet-net-core-3x-sample-handler-with-custom-status-code' title='Navigate to start of snippet `net-core-3x-sample-handler-with-custom-status-code`'>anchor</a></sup>
<!-- endsnippet -->

NOTE: Requests handlers are executed in parallel in a non-deterministic way, setting the response code in more than one handler can have unpredictable effects.

### ASP.NET Core 2.x

Create a new .NET Core console project and add a reference to the following Nuget packages:
Expand Down Expand Up @@ -124,6 +148,34 @@ More details on how to implement `IHandleRequests` and `ISubscribeToCompositionE
* [ViewModel Composition: show me the code!](https://milestone.topics.it/view-model-composition/2019/03/06/viewmodel-composition-show-me-the-code.html)
* [The ViewModels Lists Composition Dance](https://milestone.topics.it/view-model-composition/2019/03/21/the-viewmodels-lists-composition-dance.html)

#### Custom HTTP status codes in ASP.NET Core 2.x

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:

<!-- snippet: net-core-2x-sample-handler-with-custom-status-code -->
<a id='snippet-net-core-2x-sample-handler-with-custom-status-code'></a>
```cs
public class SampleHandlerWithCustomStatusCode : IHandleRequests
{
public bool Matches(RouteData routeData, string httpVerb, HttpRequest request)
{
return true;
}

public Task Handle(string requestId, dynamic vm, RouteData routeData, HttpRequest request)
{
var response = request.HttpContext.Response;
response.StatusCode = (int)HttpStatusCode.Forbidden;

return Task.CompletedTask;
}
}
```
<sup><a href='/src/Snippets.NetCore2x/SampleHandler/SampleHandler.cs#L9-L25' title='File snippet `net-core-2x-sample-handler-with-custom-status-code` was extracted from'>snippet source</a> | <a href='#snippet-net-core-2x-sample-handler-with-custom-status-code' title='Navigate to start of snippet `net-core-2x-sample-handler-with-custom-status-code`'>anchor</a></sup>
<!-- endsnippet -->

NOTE: Requests handlers are executed in parallel in a non-deterministic way, setting the response code in more than one handler can have unpredictable effects.

### MVC and Web API support

For information on how to host the Composition Gateway in a ASP.Net COre MVC application, please, refer to the [`ServiceComposer.AspNetCore.Mvc` package](https://github.com/ServiceComposer/ServiceComposer.AspNetCore.Mvc).
Expand Down
16 changes: 16 additions & 0 deletions README.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ By virtue of leveraging ASP.NET Core 3.x Endpoints ServiceComposer automatically

snippet: net-core-3x-sample-handler-with-authorization

#### Custom HTTP status codes in ASP.NET Core 3.x

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:

snippet: net-core-3x-sample-handler-with-custom-status-code

NOTE: Requests handlers are executed in parallel in a non-deterministic way, setting the response code in more than one handler can have unpredictable effects.

### ASP.NET Core 2.x

Create a new .NET Core console project and add a reference to the following Nuget packages:
Expand All @@ -50,6 +58,14 @@ More details on how to implement `IHandleRequests` and `ISubscribeToCompositionE
* [ViewModel Composition: show me the code!](https://milestone.topics.it/view-model-composition/2019/03/06/viewmodel-composition-show-me-the-code.html)
* [The ViewModels Lists Composition Dance](https://milestone.topics.it/view-model-composition/2019/03/21/the-viewmodels-lists-composition-dance.html)

#### Custom HTTP status codes in ASP.NET Core 2.x

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:

snippet: net-core-2x-sample-handler-with-custom-status-code

NOTE: Requests handlers are executed in parallel in a non-deterministic way, setting the response code in more than one handler can have unpredictable effects.

### MVC and Web API support

For information on how to host the Composition Gateway in a ASP.Net COre MVC application, please, refer to the [`ServiceComposer.AspNetCore.Mvc` package](https://github.com/ServiceComposer/ServiceComposer.AspNetCore.Mvc).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using ServiceComposer.AspNetCore.Testing;
using Xunit;

namespace ServiceComposer.AspNetCore.Endpoints.Tests
{
public class When_setting_custom_http_status_code
{
public class CustomStatusCodeHandler : ICompositionRequestsHandler
{
[HttpGet("/sample/{id}")]
public Task Handle(HttpRequest request)
{
var response = request.HttpContext.Response;
response.StatusCode = (int)HttpStatusCode.Forbidden;

return Task.CompletedTask;
}
}

[Fact]
public async Task Default_status_code_should_be_overwritten()
{
// Arrange
var expectedStatusCode = HttpStatusCode.Forbidden;
var client = new SelfContainedWebApplicationFactoryWithWebHost<When_setting_custom_http_status_code>
(
configureServices: services =>
{
services.AddViewModelComposition(options =>
{
options.AssemblyScanner.Disable();
options.RegisterCompositionHandler<CustomStatusCodeHandler>();
});
services.AddControllers();
services.AddRouting();
},
configure: app =>
{
app.UseRouting();
app.UseEndpoints(builder =>
{
builder.MapCompositionHandlers();
builder.MapControllers();
});
}
).CreateClient();

// Act
var composedResponse = await client.GetAsync("/sample/1");

// Assert
Assert.Equal(expectedStatusCode, composedResponse.StatusCode);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using ServiceComposer.AspNetCore.Gateway;
using ServiceComposer.AspNetCore.Testing;
using Xunit;

namespace ServiceComposer.AspNetCore.Tests
{
public class When_setting_custom_http_status_code
{
class CustomStatusCodeHandler : IHandleRequests
{
public Task Handle(string requestId, dynamic vm, RouteData routeData, HttpRequest request)
{
var response = request.HttpContext.Response;
response.StatusCode = (int)HttpStatusCode.Forbidden;

return Task.CompletedTask;
}

public bool Matches(RouteData routeData, string httpVerb, HttpRequest request)
{
var controller = routeData.Values["controller"]?.ToString();
return controller?.ToLowerInvariant() == "custom-status-code";
}
}

[Fact]
public async Task Default_status_code_should_be_overwritten()
{
// Arrange
var expectedStatusCode = HttpStatusCode.Forbidden;
var client = new SelfContainedWebApplicationFactoryWithWebHost<When_setting_custom_http_status_code>
(
configureServices: services =>
{
services.AddViewModelComposition(options =>
{
options.AssemblyScanner.Disable();
options.RegisterRequestsHandler<CustomStatusCodeHandler>();
});
services.AddRouting();
},
configure: app =>
{
app.RunCompositionGatewayWithDefaultRoutes();
}
).CreateClient();

// Act
var response = await client.GetAsync("/custom-status-code/1");

// Assert
Assert.Equal(expectedStatusCode, response.StatusCode);
}
}
}
19 changes: 12 additions & 7 deletions src/ServiceComposer.AspNetCore/CompositionEndpointBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,20 @@ public CompositionEndpointBuilder(RoutePattern routePattern, IEnumerable<Type> c
Order = order;
RequestDelegate = async context =>
{
var (viewModel, statusCode) = await CompositionHandler.HandleComposableRequest(context, _compositionHandlers);

context.Response.StatusCode = statusCode;
var json = (string)JsonConvert.SerializeObject(viewModel, GetSettings(context));
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(json);
var viewModel = await CompositionHandler.HandleComposableRequest(context, _compositionHandlers);
if (viewModel != null)
{
var json = (string) JsonConvert.SerializeObject(viewModel, GetSettings(context));
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(json);
}
else
{
await context.Response.WriteAsync(string.Empty);
}
};
}

JsonSerializerSettings GetSettings(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("Accept-Casing", out StringValues casing))
Expand Down
10 changes: 7 additions & 3 deletions src/ServiceComposer.AspNetCore/CompositionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
Expand Down Expand Up @@ -43,6 +44,8 @@ public class CompositionHandler

if (pending.Count == 0)
{
//we set this here to keep the implementation aligned with the .NET Core 3.x version
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
return (null, StatusCodes.Status404NotFound);
}
else
Expand Down Expand Up @@ -75,7 +78,7 @@ public class CompositionHandler
}

#if NETCOREAPP3_1
internal static async Task<(dynamic ViewModel, int StatusCode)> HandleComposableRequest(HttpContext context, Type[] handlerTypes)
internal static async Task<dynamic> HandleComposableRequest(HttpContext context, Type[] handlerTypes)
{
context.Request.EnableBuffering();

Expand Down Expand Up @@ -108,7 +111,8 @@ await Task.WhenAll(context.RequestServices.GetServices<IViewModelPreviewHandler>

if (pending.Count == 0)
{
return (null, StatusCodes.Status404NotFound);
context.Response.StatusCode = (int) StatusCodes.Status404NotFound;
return null;
}
else
{
Expand All @@ -128,7 +132,7 @@ await Task.WhenAll(context.RequestServices.GetServices<IViewModelPreviewHandler>
}
}

return (viewModel, StatusCodes.Status200OK);
return viewModel;
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,20 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE

if (handlerTypes.Any())
{
var (viewModel, statusCode) = await CompositionHandler.HandleComposableRequest(context.HttpContext, handlerTypes);
var viewModel = await CompositionHandler.HandleComposableRequest(context.HttpContext, handlerTypes);
switch (context.Result)
{
case ViewResult viewResult when viewResult.ViewData.Model == null:
{
//MVC
if (statusCode == StatusCodes.Status200OK)
{
viewResult.ViewData.Model = viewModel;
}
viewResult.ViewData.Model = viewModel;

break;
}
case ObjectResult objectResult when objectResult.Value == null:
{
//WebAPI
if (statusCode == StatusCodes.Status200OK)
{
objectResult.Value = viewModel;
}
objectResult.Value = viewModel;

break;
}
Expand Down
8 changes: 6 additions & 2 deletions src/ServiceComposer.AspNetCore/Gateway/Composition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Net;
using System.Threading.Tasks;

namespace ServiceComposer.AspNetCore.Gateway
Expand All @@ -15,15 +16,18 @@ public static async Task HandleRequest(HttpContext context)
var (viewModel, statusCode) = await CompositionHandler.HandleRequest(requestId, context);
context.Response.Headers.AddComposedRequestIdHeader(requestId);

if (statusCode == StatusCodes.Status200OK)
//to avoid a breaking change we cannot change the tuple returned by CompositionHandler.HandleRequest
//so the only option here is to check if the viewModel is null. View model is null only when there are
//no handlers registered for the route, so it's for sure an HTTP404
if (viewModel != null)
{
string json = JsonConvert.SerializeObject(viewModel, GetSettings(context));
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(json);
}
else
{
context.Response.StatusCode = statusCode;
await context.Response.WriteAsync(string.Empty);
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/Snippets.NetCore2x/SampleHandler/SampleHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using ServiceComposer.AspNetCore;

namespace Snippets.NetCore2x.SampleHandler
{
// begin-snippet: net-core-2x-sample-handler-with-custom-status-code
public class SampleHandlerWithCustomStatusCode : IHandleRequests
{
public bool Matches(RouteData routeData, string httpVerb, HttpRequest request)
{
return true;
}

public Task Handle(string requestId, dynamic vm, RouteData routeData, HttpRequest request)
{
var response = request.HttpContext.Response;
response.StatusCode = (int)HttpStatusCode.Forbidden;

return Task.CompletedTask;
}
}
// end-snippet
}
Loading

0 comments on commit 2bbdcd1

Please sign in to comment.