Skip to content

Commit

Permalink
Intial refactoring to intercept regular MVC route requests
Browse files Browse the repository at this point in the history
  • Loading branch information
mauroservienti committed Jun 24, 2020
1 parent d14911e commit 3858ecf
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 97 deletions.
75 changes: 1 addition & 74 deletions src/ServiceComposer.AspNetCore/CompositionEndpointBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,80 +27,7 @@ public CompositionEndpointBuilder(RoutePattern routePattern, IEnumerable<Type> c
_compositionHandlers = compositionHandlers.ToArray();
RoutePattern = routePattern;
Order = order;
RequestDelegate = async context =>
{
context.Request.EnableBuffering();
var request = context.Request;
var routeData = context.GetRouteData();
var requestId = request.Headers.GetComposedRequestIdHeaderOr(() => Guid.NewGuid().ToString());
context.Response.Headers.AddComposedRequestIdHeader(requestId);
var viewModel = new DynamicViewModel(requestId, routeData, request);
request.SetModel(viewModel);
var handlers = _compositionHandlers.Select(type => context.RequestServices.GetRequiredService(type)).ToArray();
foreach (var subscriber in handlers.OfType<ICompositionEventsSubscriber>())
{
subscriber.Subscribe(viewModel);
}
var pending = new List<Task>();
foreach (var handler in handlers.OfType<ICompositionRequestsHandler>())
{
pending.Add(handler.Handle(request));
}
if (pending.Count == 0)
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
}
else
{
try
{
await Task.WhenAll(pending);
}
catch (Exception ex)
{
var errorHandlers = handlers.OfType<ICompositionErrorsHandler>();
foreach (var handler in errorHandlers)
{
await handler.OnRequestError(request, ex);
}
throw;
}
}
context.Response.StatusCode = StatusCodes.Status200OK;
var json = JsonConvert.SerializeObject(viewModel, GetSettings(context));
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(json);
};
}

JsonSerializerSettings GetSettings(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("Accept-Casing", out StringValues casing))
{
casing = "casing/camel";
}

switch (casing)
{
case "casing/pascal":
return new JsonSerializerSettings();

default: // "casing/camel":
return new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
}
RequestDelegate = context => CompositionHandler.ComposeResponse(context, _compositionHandlers);
}

public override Endpoint Build()
Expand Down
102 changes: 85 additions & 17 deletions src/ServiceComposer.AspNetCore/CompositionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace ServiceComposer.AspNetCore
{
public class CompositionHandler
{
//static Dictionary<string, IInterceptRoutes[]> cache = new Dictionary<string, IInterceptRoutes[]>();

public static async Task<(dynamic ViewModel, int StatusCode)> HandleRequest(string requestId, HttpContext context)
public static async Task<(dynamic ViewModel, int StatusCode)> HandleRequest(string requestId,
HttpContext context)
{
var routeData = context.GetRouteData();
var request = context.Request;
Expand All @@ -21,19 +23,8 @@ public static async Task<(dynamic ViewModel, int StatusCode)> HandleRequest(stri
try
{
var interceptors = context.RequestServices.GetServices<IInterceptRoutes>()
.Where(a => a.Matches(routeData, request.Method, request))
.ToArray();

/*
if (!cache.TryGetValue(request.Path, out var interceptors))
{
interceptors = context.RequestServices.GetServices<IInterceptRoutes>()
.Where(a => a.Matches(routeData, request.Method, request))
.ToArray();
cache.Add(request.Path, interceptors);
}
*/
.Where(a => a.Matches(routeData, request.Method, request))
.ToArray();

foreach (var subscriber in interceptors.OfType<ISubscribeToCompositionEvents>())
{
Expand Down Expand Up @@ -82,5 +73,82 @@ public static async Task<(dynamic ViewModel, int StatusCode)> HandleRequest(stri
viewModel.CleanupSubscribers();
}
}

#if NETCOREAPP3_1
internal static async Task ComposeResponse(HttpContext context, Type[] handlerTypes)
{
context.Request.EnableBuffering();

var request = context.Request;
var routeData = context.GetRouteData();

var requestId = request.Headers.GetComposedRequestIdHeaderOr(() => Guid.NewGuid().ToString());
context.Response.Headers.AddComposedRequestIdHeader(requestId);

var viewModel = new DynamicViewModel(requestId, routeData, request);
request.SetModel(viewModel);

var handlers = handlerTypes.Select(type => context.RequestServices.GetRequiredService(type)).ToArray();

foreach (var subscriber in handlers.OfType<ICompositionEventsSubscriber>())
{
subscriber.Subscribe(viewModel);
}

var pending = new List<Task>();

foreach (var handler in handlers.OfType<ICompositionRequestsHandler>())
{
pending.Add(handler.Handle(request));
}

if (pending.Count == 0)
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
}
else
{
try
{
await Task.WhenAll(pending);
}
catch (Exception ex)
{
var errorHandlers = handlers.OfType<ICompositionErrorsHandler>();
foreach (var handler in errorHandlers)
{
await handler.OnRequestError(request, ex);
}

throw;
}
}

context.Response.StatusCode = StatusCodes.Status200OK;
var json = JsonConvert.SerializeObject(viewModel, GetSettings(context));
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(json);
}

static JsonSerializerSettings GetSettings(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("Accept-Casing", out StringValues casing))
{
casing = "casing/camel";
}

switch (casing)
{
case "casing/pascal":
return new JsonSerializerSettings();

default: // "casing/camel":
return new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
}
}
#endif
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#if NETCOREAPP3_1
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;

namespace ServiceComposer.AspNetCore
{
internal class CompositionOverControllersActionFilter : IAsyncResultFilter
{
private readonly CompositionOverControllersRoutes _compositionOverControllersRoutes;

public CompositionOverControllersActionFilter(CompositionOverControllersRoutes compositionOverControllersRoutes)
{
_compositionOverControllersRoutes = compositionOverControllersRoutes;
}

public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
var endpoint = context.HttpContext.GetEndpoint() as RouteEndpoint;
var handlerTypes = _compositionOverControllersRoutes.HandlersForRoute(endpoint.RoutePattern.RawText,
context.HttpContext.Request.Method);
if (handlerTypes.Any())
{

}

await next();
}
}
}
#endif
30 changes: 30 additions & 0 deletions src/ServiceComposer.AspNetCore/CompositionOverControllersRoutes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;

namespace ServiceComposer.AspNetCore
{
internal class CompositionOverControllersRoutes
{
private Dictionary<string, Type[]> _compositionOverControllerGetComponents;
private readonly Type[] empty = new Type[0];
public void AddGetComponentsSource(Dictionary<string, Type[]> compositionOverControllerGetComponents)
{
_compositionOverControllerGetComponents = compositionOverControllerGetComponents ?? throw new ArgumentNullException(nameof(compositionOverControllerGetComponents));
}

public Type[] HandlersForRoute(string routePatternRawText, string requestMethod)
{
switch (requestMethod.ToLowerInvariant())
{
case "get":
if (_compositionOverControllerGetComponents.ContainsKey(routePatternRawText))
{
return _compositionOverControllerGetComponents[routePatternRawText];
}
break;
}

return empty;
}
}
}
8 changes: 7 additions & 1 deletion src/ServiceComposer.AspNetCore/EndpointsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ namespace ServiceComposer.AspNetCore
{
public static class EndpointsExtensions
{
static Dictionary<string, Type[]> compositionOverControllerGetComponents = new Dictionary<string, Type[]>();

public static void MapCompositionHandlers(this IEndpointRouteBuilder endpoints, bool enableWriteSupport = false)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}

var compositionOverControllersRoutes = endpoints.ServiceProvider.GetRequiredService<CompositionOverControllersRoutes>();
compositionOverControllersRoutes.AddGetComponentsSource(compositionOverControllerGetComponents);

var compositionMetadataRegistry =
endpoints.ServiceProvider.GetRequiredService<CompositionMetadataRegistry>();
Expand All @@ -43,7 +48,8 @@ private static void MapGetComponents(CompositionMetadataRegistry compositionMeta
{
if (ThereIsAlreadyAnEndpointForTheSameTemplate(componentsGroup, dataSources, out var endpoint))
{

var componentTypes = componentsGroup.Select(c => c.ComponentType).ToArray();
compositionOverControllerGetComponents[componentsGroup.Key] = componentTypes;
}
else
{
Expand Down
23 changes: 18 additions & 5 deletions src/ServiceComposer.AspNetCore/ViewModelCompositionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ namespace ServiceComposer.AspNetCore
{
public class ViewModelCompositionOptions
{
readonly CompositionMetadataRegistry compositionMetadataRegistry = new CompositionMetadataRegistry();

readonly CompositionMetadataRegistry _compositionMetadataRegistry = new CompositionMetadataRegistry();
#if NETCOREAPP3_1
readonly CompositionOverControllersRoutes _compositionOverControllersRoutes = new CompositionOverControllersRoutes();
#endif

internal ViewModelCompositionOptions(IServiceCollection services)
{
Services = services;
AssemblyScanner = new AssemblyScanner();

Services.AddSingleton(compositionMetadataRegistry);
Services.AddSingleton(_compositionMetadataRegistry);

#if NETCOREAPP3_1
Services.AddSingleton(_compositionOverControllersRoutes);
#endif
}

List<(Func<Type, bool>, Action<IEnumerable<Type>>)> typesRegistrationHandlers = new List<(Func<Type, bool>, Action<IEnumerable<Type>>)>();
Expand All @@ -38,6 +45,13 @@ public void AddTypesRegistrationHandler(Func<Type, bool> typesFilter, Action<IEn

internal void InitializeServiceCollection()
{
#if NETCOREAPP3_1
Services.Configure<Microsoft.AspNetCore.Mvc.MvcOptions>(options =>
{
options.Filters.Add(typeof(CompositionOverControllersActionFilter));
});
#endif

if (AssemblyScanner.IsEnabled)
{
AddTypesRegistrationHandler(
Expand Down Expand Up @@ -85,7 +99,6 @@ internal void InitializeServiceCollection()
oc.Customize(this);
}


foreach (var (typesFilter, registrationHandler) in typesRegistrationHandlers)
{
var filteredTypes = allTypes.Where(typesFilter);
Expand Down Expand Up @@ -120,7 +133,7 @@ void RegisterCompositionHandler(Type type)
throw new NotSupportedException("Registered types must be ICompositionRequestsHandler or ICompositionEventsSubscriber.");
}

compositionMetadataRegistry.AddComponent(type);
_compositionMetadataRegistry.AddComponent(type);
if (configurationHandlers.TryGetValue(type, out var handler))
{
handler(type, Services);
Expand Down

0 comments on commit 3858ecf

Please sign in to comment.