From 03571cc27b6b4e8927c4e3a41be41dde41abae4e Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 2 Jun 2015 13:15:12 -0700 Subject: [PATCH] Event Notification for MVC Prototype Here's a first take on the pattern for publishing notifications from MVC. --- .../MvcRouteHandler.cs | 21 +++++++++- src/Microsoft.AspNet.Mvc.Core/project.json | 1 + .../MvcServiceCollectionExtensions.cs | 1 + .../MvcRouteHandlerTests.cs | 38 ++++++++++++++++++- .../Notification/IActionDescriptor.cs | 9 +++++ .../Notification/IHttpContext.cs | 9 +++++ .../Notification/IRouteData.cs | 14 +++++++ .../Notification/TestNotificationListener.cs | 33 ++++++++++++++++ .../project.json | 2 +- 9 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.TestCommon/Notification/IActionDescriptor.cs create mode 100644 test/Microsoft.AspNet.Mvc.TestCommon/Notification/IHttpContext.cs create mode 100644 test/Microsoft.AspNet.Mvc.TestCommon/Notification/IRouteData.cs create mode 100644 test/Microsoft.AspNet.Mvc.TestCommon/Notification/TestNotificationListener.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcRouteHandler.cs b/src/Microsoft.AspNet.Mvc.Core/MvcRouteHandler.cs index 602bb1c439..d9ac3b3d2a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/MvcRouteHandler.cs +++ b/src/Microsoft.AspNet.Mvc.Core/MvcRouteHandler.cs @@ -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(); 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(); } } + + private void EnsureNotifier(HttpContext context) + { + if (_notifier == null) + { + _notifier = context.RequestServices.GetRequiredService(); + } + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index 211cf369f0..4763fc94c4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -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" }, diff --git a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs index cf55c99f7b..af55d6cfbd 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs @@ -299,6 +299,7 @@ namespace Microsoft.Framework.DependencyInjection services.AddCors(); services.AddAuthorization(); services.AddWebEncoders(); + services.AddNotifier(); services.Configure( routeOptions => routeOptions.ConstraintMap.Add("exists", typeof(KnownRouteValueConstraint))); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs index fe9eab0269..5a3e99bb90 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/MvcRouteHandlerTests.cs @@ -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 optionsAccessor = null) + IOptions optionsAccessor = null, + object notificationListener = null) { var mockContextAccessor = new Mock>(); @@ -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.Setup(h => h.RequestServices.GetService(typeof(IScopedInstance))) .Returns(mockContextAccessor.Object); @@ -216,6 +250,8 @@ namespace Microsoft.AspNet.Mvc .Returns(new MvcMarkerService()); httpContext.Setup(h => h.RequestServices.GetService(typeof(IOptions))) .Returns(optionsAccessor); + httpContext.Setup(h => h.RequestServices.GetService(typeof(INotifier))) + .Returns(notifier); return new RouteContext(httpContext.Object); } diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/Notification/IActionDescriptor.cs b/test/Microsoft.AspNet.Mvc.TestCommon/Notification/IActionDescriptor.cs new file mode 100644 index 0000000000..7fa0726735 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TestCommon/Notification/IActionDescriptor.cs @@ -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 + { + } +} diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/Notification/IHttpContext.cs b/test/Microsoft.AspNet.Mvc.TestCommon/Notification/IHttpContext.cs new file mode 100644 index 0000000000..46d68faf9b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TestCommon/Notification/IHttpContext.cs @@ -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 + { + } +} diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/Notification/IRouteData.cs b/test/Microsoft.AspNet.Mvc.TestCommon/Notification/IRouteData.cs new file mode 100644 index 0000000000..25d1bae7ac --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TestCommon/Notification/IRouteData.cs @@ -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 Routers { get; } + IDictionary DataTokens { get; } + IDictionary Values { get; } + } +} diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/Notification/TestNotificationListener.cs b/test/Microsoft.AspNet.Mvc.TestCommon/Notification/TestNotificationListener.cs new file mode 100644 index 0000000000..eaf9013029 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TestCommon/Notification/TestNotificationListener.cs @@ -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; } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/project.json b/test/Microsoft.AspNet.Mvc.TestCommon/project.json index 117fd7d664..e024b51f1c 100644 --- a/test/Microsoft.AspNet.Mvc.TestCommon/project.json +++ b/test/Microsoft.AspNet.Mvc.TestCommon/project.json @@ -1,6 +1,6 @@ { "version": "6.0.0-*", - "shared": "*.cs", + "shared": "**/*.cs", "dependencies": { } } \ No newline at end of file