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.
using System;
using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
@ -12,12 +11,13 @@ using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Internal;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.Notification;
namespace Microsoft.AspNet.Mvc
{
public class MvcRouteHandler : IRouter
{
private INotifier _notifier;
private ILogger _logger;
public VirtualPathData GetVirtualPath([NotNull] VirtualPathContext context)
@ -40,6 +40,8 @@ namespace Microsoft.AspNet.Mvc
MvcServicesHelper.ThrowIfMvcNotRegistered(services);
EnsureLogger(context.HttpContext);
EnsureNotifier(context.HttpContext);
var actionSelector = services.GetRequiredService<IActionSelector>();
var actionDescriptor = await actionSelector.SelectAsync(context);
@ -69,6 +71,13 @@ namespace Microsoft.AspNet.Mvc
{
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))
{
_logger.LogVerbose("Executing action {ActionDisplayName}", actionDescriptor.DisplayName);
@ -115,5 +124,13 @@ namespace Microsoft.AspNet.Mvc
_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.CopyOnWriteDictionary.Sources": { "version": "1.0.0-*", "type": "build" },
"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.PropertyActivator.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.AddAuthorization();
services.AddWebEncoders();
services.AddNotifier();
services.Configure<RouteOptions>(
routeOptions => routeOptions.ConstraintMap.Add("exists", typeof(KnownRouteValueConstraint)));
}

View File

@ -5,9 +5,11 @@ using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Internal;
using Microsoft.AspNet.Mvc.TestCommon.Notification;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.Logging;
using Microsoft.Framework.Logging.Testing;
using Microsoft.Framework.Notification;
using Microsoft.Framework.OptionsModel;
using Moq;
using Xunit;
@ -152,12 +154,38 @@ namespace Microsoft.AspNet.Mvc
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(
ActionDescriptor actionDescriptor = null,
IActionSelector actionSelector = null,
IActionInvokerFactory invokerFactory = null,
ILoggerFactory loggerFactory = null,
IOptions<MvcOptions> optionsAccessor = null)
IOptions<MvcOptions> optionsAccessor = null,
object notificationListener = null)
{
var mockContextAccessor = new Mock<IScopedInstance<ActionContext>>();
@ -203,6 +231,12 @@ namespace Microsoft.AspNet.Mvc
optionsAccessor = options.Object;
}
var notifier = new Notifier(new NotifierMethodAdapter());
if (notificationListener != null)
{
notifier.EnlistTarget(notificationListener);
}
var httpContext = new Mock<HttpContext>();
httpContext.Setup(h => h.RequestServices.GetService(typeof(IScopedInstance<ActionContext>)))
.Returns(mockContextAccessor.Object);
@ -216,6 +250,8 @@ namespace Microsoft.AspNet.Mvc
.Returns(new MvcMarkerService());
httpContext.Setup(h => h.RequestServices.GetService(typeof(IOptions<MvcOptions>)))
.Returns(optionsAccessor);
httpContext.Setup(h => h.RequestServices.GetService(typeof(INotifier)))
.Returns(notifier);
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-*",
"shared": "*.cs",
"shared": "**/*.cs",
"dependencies": {
}
}