Event Notification for MVC Prototype

Here's a first take on the pattern for publishing notifications from MVC.
This commit is contained in:
Ryan Nowak 2015-06-02 13:15:12 -07:00
parent a452b10ba4
commit 03571cc27b
9 changed files with 124 additions and 4 deletions

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Linq;
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
@ -12,12 +11,13 @@ using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Internal; using Microsoft.Framework.Internal;
using Microsoft.Framework.Logging; using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel; using Microsoft.Framework.Notification;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
public class MvcRouteHandler : IRouter public class MvcRouteHandler : IRouter
{ {
private INotifier _notifier;
private ILogger _logger; private ILogger _logger;
public VirtualPathData GetVirtualPath([NotNull] VirtualPathContext context) public VirtualPathData GetVirtualPath([NotNull] VirtualPathContext context)
@ -40,6 +40,8 @@ namespace Microsoft.AspNet.Mvc
MvcServicesHelper.ThrowIfMvcNotRegistered(services); MvcServicesHelper.ThrowIfMvcNotRegistered(services);
EnsureLogger(context.HttpContext); EnsureLogger(context.HttpContext);
EnsureNotifier(context.HttpContext);
var actionSelector = services.GetRequiredService<IActionSelector>(); var actionSelector = services.GetRequiredService<IActionSelector>();
var actionDescriptor = await actionSelector.SelectAsync(context); var actionDescriptor = await actionSelector.SelectAsync(context);
@ -69,6 +71,13 @@ namespace Microsoft.AspNet.Mvc
{ {
context.RouteData = newRouteData; context.RouteData = newRouteData;
if (_notifier.ShouldNotify("Microsoft.AspNet.Mvc.ActionSelected"))
{
_notifier.Notify(
"Microsoft.AspNet.Mvc.ActionSelected",
new { actionDescriptor, httpContext = context.HttpContext, routeData = context.RouteData});
}
using (_logger.BeginScope("ActionId: {ActionId}", actionDescriptor.Id)) using (_logger.BeginScope("ActionId: {ActionId}", actionDescriptor.Id))
{ {
_logger.LogVerbose("Executing action {ActionDisplayName}", actionDescriptor.DisplayName); _logger.LogVerbose("Executing action {ActionDisplayName}", actionDescriptor.DisplayName);
@ -115,5 +124,13 @@ namespace Microsoft.AspNet.Mvc
_logger = factory.CreateLogger<MvcRouteHandler>(); _logger = factory.CreateLogger<MvcRouteHandler>();
} }
} }
private void EnsureNotifier(HttpContext context)
{
if (_notifier == null)
{
_notifier = context.RequestServices.GetRequiredService<INotifier>();
}
}
} }
} }

View File

@ -21,6 +21,7 @@
"Microsoft.Framework.ClosedGenericMatcher.Sources": { "version": "1.0.0-*", "type": "build" }, "Microsoft.Framework.ClosedGenericMatcher.Sources": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.CopyOnWriteDictionary.Sources": { "version": "1.0.0-*", "type": "build" }, "Microsoft.Framework.CopyOnWriteDictionary.Sources": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.Logging.Abstractions": "1.0.0-*", "Microsoft.Framework.Logging.Abstractions": "1.0.0-*",
"Microsoft.Framework.Notification": "1.0.0-*",
"Microsoft.Framework.NotNullAttribute.Sources": { "version": "1.0.0-*", "type": "build" }, "Microsoft.Framework.NotNullAttribute.Sources": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.PropertyActivator.Sources": { "version": "1.0.0-*", "type": "build" }, "Microsoft.Framework.PropertyActivator.Sources": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.PropertyHelper.Sources": { "version": "1.0.0-*", "type": "build" }, "Microsoft.Framework.PropertyHelper.Sources": { "version": "1.0.0-*", "type": "build" },

View File

@ -299,6 +299,7 @@ namespace Microsoft.Framework.DependencyInjection
services.AddCors(); services.AddCors();
services.AddAuthorization(); services.AddAuthorization();
services.AddWebEncoders(); services.AddWebEncoders();
services.AddNotifier();
services.Configure<RouteOptions>( services.Configure<RouteOptions>(
routeOptions => routeOptions.ConstraintMap.Add("exists", typeof(KnownRouteValueConstraint))); routeOptions => routeOptions.ConstraintMap.Add("exists", typeof(KnownRouteValueConstraint)));
} }

View File

@ -5,9 +5,11 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Internal; using Microsoft.AspNet.Mvc.Internal;
using Microsoft.AspNet.Mvc.TestCommon.Notification;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.Framework.Logging; using Microsoft.Framework.Logging;
using Microsoft.Framework.Logging.Testing; using Microsoft.Framework.Logging.Testing;
using Microsoft.Framework.Notification;
using Microsoft.Framework.OptionsModel; using Microsoft.Framework.OptionsModel;
using Moq; using Moq;
using Xunit; using Xunit;
@ -152,12 +154,38 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(initialRouter, Assert.Single(actionRouteData.Routers)); Assert.Equal(initialRouter, Assert.Single(actionRouteData.Routers));
} }
[Fact]
public async Task RouteAsync_Notifies_ActionSelected()
{
// Arrange
var listener = new TestNotificationListener();
var context = CreateRouteContext(notificationListener: listener);
context.RouteData.Values.Add("tag", "value");
var handler = new MvcRouteHandler();
// Act
await handler.RouteAsync(context);
// Assert
Assert.NotNull(listener?.ActionSelected.ActionDescriptor);
Assert.NotNull(listener?.ActionSelected.HttpContext);
var routeValues = listener?.ActionSelected?.RouteData?.Values;
Assert.NotNull(routeValues);
Assert.Equal(1, routeValues.Count);
Assert.Contains(routeValues, kvp => kvp.Key == "tag" && string.Equals(kvp.Value, "value"));
}
private RouteContext CreateRouteContext( private RouteContext CreateRouteContext(
ActionDescriptor actionDescriptor = null, ActionDescriptor actionDescriptor = null,
IActionSelector actionSelector = null, IActionSelector actionSelector = null,
IActionInvokerFactory invokerFactory = null, IActionInvokerFactory invokerFactory = null,
ILoggerFactory loggerFactory = null, ILoggerFactory loggerFactory = null,
IOptions<MvcOptions> optionsAccessor = null) IOptions<MvcOptions> optionsAccessor = null,
object notificationListener = null)
{ {
var mockContextAccessor = new Mock<IScopedInstance<ActionContext>>(); var mockContextAccessor = new Mock<IScopedInstance<ActionContext>>();
@ -203,6 +231,12 @@ namespace Microsoft.AspNet.Mvc
optionsAccessor = options.Object; optionsAccessor = options.Object;
} }
var notifier = new Notifier(new NotifierMethodAdapter());
if (notificationListener != null)
{
notifier.EnlistTarget(notificationListener);
}
var httpContext = new Mock<HttpContext>(); var httpContext = new Mock<HttpContext>();
httpContext.Setup(h => h.RequestServices.GetService(typeof(IScopedInstance<ActionContext>))) httpContext.Setup(h => h.RequestServices.GetService(typeof(IScopedInstance<ActionContext>)))
.Returns(mockContextAccessor.Object); .Returns(mockContextAccessor.Object);
@ -216,6 +250,8 @@ namespace Microsoft.AspNet.Mvc
.Returns(new MvcMarkerService()); .Returns(new MvcMarkerService());
httpContext.Setup(h => h.RequestServices.GetService(typeof(IOptions<MvcOptions>))) httpContext.Setup(h => h.RequestServices.GetService(typeof(IOptions<MvcOptions>)))
.Returns(optionsAccessor); .Returns(optionsAccessor);
httpContext.Setup(h => h.RequestServices.GetService(typeof(INotifier)))
.Returns(notifier);
return new RouteContext(httpContext.Object); return new RouteContext(httpContext.Object);
} }

View File

@ -0,0 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.TestCommon.Notification
{
public interface IActionDescriptor
{
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.TestCommon.Notification
{
public interface IHttpContext
{
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.TestCommon.Notification
{
public interface IRouteData
{
IReadOnlyList<object> Routers { get; }
IDictionary<string, object> DataTokens { get; }
IDictionary<string, object> Values { get; }
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Framework.Notification;
namespace Microsoft.AspNet.Mvc.TestCommon.Notification
{
public class TestNotificationListener
{
public OnActionSelectedEventData ActionSelected { get; set; }
[NotificationName("Microsoft.AspNet.Mvc.ActionSelected")]
public virtual void OnActionSelected(
IHttpContext httpContext,
IRouteData routeData,
IActionDescriptor actionDescriptor)
{
ActionSelected = new OnActionSelectedEventData()
{
ActionDescriptor = actionDescriptor,
HttpContext = httpContext,
RouteData = routeData,
};
}
public class OnActionSelectedEventData
{
public IActionDescriptor ActionDescriptor { get; set; }
public IHttpContext HttpContext { get; set; }
public IRouteData RouteData { get; set; }
}
}
}

View File

@ -1,6 +1,6 @@
{ {
"version": "6.0.0-*", "version": "6.0.0-*",
"shared": "*.cs", "shared": "**/*.cs",
"dependencies": { "dependencies": {
} }
} }