diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageModelActivatorProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageModelActivatorProvider.cs new file mode 100644 index 0000000000..8789141087 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageModelActivatorProvider.cs @@ -0,0 +1,27 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + /// + /// Provides methods to create a Razor Page model. + /// + public interface IPageModelActivatorProvider + { + /// + /// Creates a Razor Page model activator. + /// + /// The . + /// The delegate used to activate the page model. + Func CreateActivator(CompiledPageActionDescriptor descriptor); + + /// + /// Releases a Razor Page model. + /// + /// The . + /// The delegate used to dispose the activated Razor Page model. + Action CreateReleaser(CompiledPageActionDescriptor descriptor); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageModelFactoryProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageModelFactoryProvider.cs new file mode 100644 index 0000000000..b99a7b1a2c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/IPageModelFactoryProvider.cs @@ -0,0 +1,27 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + /// + /// Provides methods for creation and disposal of Razor Page models. + /// + public interface IPageModelFactoryProvider + { + /// + /// Creates a factory for producing models for Razor Pages given the specified . + /// + /// The . + /// The Razor Page model factory. + Func CreateModelFactory(CompiledPageActionDescriptor descriptor); + + /// + /// Releases a Razor Page model. + /// + /// The . + /// The delegate used to release the created Razor Page model. + Action CreateModelDisposer(CompiledPageActionDescriptor descriptor); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageActivator.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageActivator.cs similarity index 97% rename from src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageActivator.cs rename to src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageActivator.cs index 53b58d42ff..5b06a51115 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageActivator.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageActivator.cs @@ -5,7 +5,7 @@ using System; using System.Linq.Expressions; using System.Reflection; -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { /// /// that uses type activation to create Pages. diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactory.cs similarity index 98% rename from src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageFactory.cs rename to src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactory.cs index cadd93913d..c8d4ec56d0 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageFactory.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class DefaultPageFactory : IPageFactoryProvider { diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageModelActivatorProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageModelActivatorProvider.cs new file mode 100644 index 0000000000..02d7631561 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageModelActivatorProvider.cs @@ -0,0 +1,69 @@ +// 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; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + /// + /// that uses type activation to create Razor Page instances. + /// + public class DefaultPageModelActivatorProvider : IPageModelActivatorProvider + { + private readonly Action _disposer = Dispose; + + /// + public virtual Func CreateActivator(CompiledPageActionDescriptor actionDescriptor) + { + if (actionDescriptor == null) + { + throw new ArgumentNullException(nameof(actionDescriptor)); + } + + var modelTypeInfo = actionDescriptor.ModelTypeInfo?.AsType(); + if (modelTypeInfo == null) + { + throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull( + nameof(actionDescriptor.ModelTypeInfo), + nameof(actionDescriptor)), + nameof(actionDescriptor)); + } + + var factory = ActivatorUtilities.CreateFactory(modelTypeInfo, Type.EmptyTypes); + return (context) => factory(context.HttpContext.RequestServices, EmptyArray.Instance); + } + + public virtual Action CreateReleaser(CompiledPageActionDescriptor actionDescriptor) + { + if (actionDescriptor == null) + { + throw new ArgumentNullException(nameof(actionDescriptor)); + } + + if (typeof(IDisposable).GetTypeInfo().IsAssignableFrom(actionDescriptor.ModelTypeInfo)) + { + return _disposer; + } + + return null; + } + + private static void Dispose(PageContext context, object page) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (page == null) + { + throw new ArgumentNullException(nameof(page)); + } + + ((IDisposable)page).Dispose(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageModelFactoryProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageModelFactoryProvider.cs new file mode 100644 index 0000000000..6821916a4f --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/DefaultPageModelFactoryProvider.cs @@ -0,0 +1,70 @@ +// 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; +using System.Reflection; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public class DefaultPageModelFactoryProvider : IPageModelFactoryProvider + { + private static readonly Func> _createActivateInfo = + CreateActivateInfo; + private readonly IPageModelActivatorProvider _modelActivator; + + public DefaultPageModelFactoryProvider(IPageModelActivatorProvider modelActivator) + { + _modelActivator = modelActivator; + } + + public virtual Func CreateModelFactory(CompiledPageActionDescriptor descriptor) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + if (descriptor.ModelTypeInfo == null) + { + return null; + } + + var modelActivator = _modelActivator.CreateActivator(descriptor); + var propertyActivator = PropertyActivator.GetPropertiesToActivate( + descriptor.ModelTypeInfo.AsType(), + typeof(PageContextAttribute), + _createActivateInfo, + includeNonPublic: false); + + return pageContext => + { + var model = modelActivator(pageContext); + for (var i = 0; i < propertyActivator.Length; i++) + { + propertyActivator[i].Activate(model, pageContext); + } + + return model; + }; + } + + public virtual Action CreateModelDisposer(CompiledPageActionDescriptor descriptor) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + if (descriptor.ModelTypeInfo == null) + { + return null; + } + + return _modelActivator.CreateReleaser(descriptor); + } + + private static PropertyActivator CreateActivateInfo(PropertyInfo property) => + new PropertyActivator(property, pageContext => pageContext); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index 728c38da5b..e28e564f9e 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private const string ModelPropertyName = "Model"; private readonly IPageLoader _loader; private readonly IPageFactoryProvider _pageFactoryProvider; + private readonly IPageModelFactoryProvider _modelFactoryProvider; private readonly IActionDescriptorCollectionProvider _collectionProvider; private readonly IFilterProvider[] _filterProviders; private readonly IReadOnlyList _valueProviderFactories; @@ -38,6 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal public PageActionInvokerProvider( IPageLoader loader, IPageFactoryProvider pageFactoryProvider, + IPageModelFactoryProvider modelFactoryProvider, IActionDescriptorCollectionProvider collectionProvider, IEnumerable filterProviders, IEnumerable valueProviderFactories, @@ -49,8 +51,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal ILoggerFactory loggerFactory) { _loader = loader; - _collectionProvider = collectionProvider; _pageFactoryProvider = pageFactoryProvider; + _modelFactoryProvider = modelFactoryProvider; + _collectionProvider = collectionProvider; _filterProviders = filterProviders.ToArray(); _valueProviderFactories = valueProviderFactories.ToArray(); _modelMetadataProvider = modelMetadataProvider; @@ -157,12 +160,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal PageTypeInfo = compiledType, }; + var pageFactory = _pageFactoryProvider.CreatePageFactory(compiledActionDescriptor); + var pageDisposer = _pageFactoryProvider.CreatePageDisposer(compiledActionDescriptor); + + Func modelFactory = null; + Action modelReleaser = null; + if (modelType != null) + { + modelFactory = _modelFactoryProvider.CreateModelFactory(compiledActionDescriptor); + modelReleaser = _modelFactoryProvider.CreateModelDisposer(compiledActionDescriptor); + } + return new PageActionInvokerCacheEntry( compiledActionDescriptor, - _pageFactoryProvider.CreatePageFactory(compiledActionDescriptor), - _pageFactoryProvider.CreatePageDisposer(compiledActionDescriptor), - c => { throw new NotImplementedException(); }, - (_, __) => { throw new NotImplementedException(); }, + pageFactory, + pageDisposer, + modelFactory, + modelReleaser, cachedFilters); } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContextAttribute.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContextAttribute.cs new file mode 100644 index 0000000000..d92ec99c26 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/PageContextAttribute.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.RazorPages +{ + /// + /// Specifies that a Razor Page model property should be set with the current when creating + /// the model instance. The property must have a public set method. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class PageContextAttribute : Attribute + { + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageActivatorTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageActivatorTest.cs similarity index 98% rename from test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageActivatorTest.cs rename to test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageActivatorTest.cs index 6d15679713..27b1da83c2 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageActivatorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageActivatorTest.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Xunit; -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class DefaultPageActivatorTest { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryTest.cs similarity index 99% rename from test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageFactoryTest.cs rename to test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryTest.cs index eadb731cb4..b54fd794a1 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageFactoryTest.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class DefaultPageFactoryProviderTest { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageModelActivatorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageModelActivatorProviderTest.cs new file mode 100644 index 0000000000..4e953e3dfe --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageModelActivatorProviderTest.cs @@ -0,0 +1,155 @@ +// 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; +using System.Reflection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public class DefaultPageModelActivatorProviderTest + { + [Fact] + public void CreateActivator_ThrowsIfModelTypeInfoOnActionDescriptorIsNull() + { + // Arrange + var activatorProvider = new DefaultPageModelActivatorProvider(); + var actionDescriptor = new CompiledPageActionDescriptor(); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => activatorProvider.CreateActivator(actionDescriptor), + "actionDescriptor", + "The 'ModelTypeInfo' property of 'actionDescriptor' must not be null."); + } + + [Fact] + public void CreateActivator_CreatesModelInstance() + { + // Arrange + var activatorProvider = new DefaultPageModelActivatorProvider(); + var actionDescriptor = new CompiledPageActionDescriptor + { + ModelTypeInfo = typeof(SimpleModel).GetTypeInfo(), + }; + var serviceCollection = new ServiceCollection(); + var generator = Mock.Of(); + serviceCollection.AddSingleton(generator); + var httpContext = new DefaultHttpContext + { + RequestServices = serviceCollection.BuildServiceProvider(), + }; + var pageContext = new PageContext + { + HttpContext = httpContext + }; + + // Act + var activator = activatorProvider.CreateActivator(actionDescriptor); + var model = activator(pageContext); + + // Assert + var simpleModel = Assert.IsType(model); + Assert.NotNull(simpleModel); + } + + [Fact] + public void CreateActivator_TypeActivatesModelType() + { + // Arrange + var activatorProvider = new DefaultPageModelActivatorProvider(); + var actionDescriptor = new CompiledPageActionDescriptor + { + ModelTypeInfo = typeof(ModelWithServices).GetTypeInfo(), + }; + var serviceCollection = new ServiceCollection(); + var generator = Mock.Of(); + serviceCollection.AddSingleton(generator); + var httpContext = new DefaultHttpContext + { + RequestServices = serviceCollection.BuildServiceProvider(), + }; + var pageContext = new PageContext + { + HttpContext = httpContext + }; + + // Act + var activator = activatorProvider.CreateActivator(actionDescriptor); + var model = activator(pageContext); + + // Assert + var modelWithServices = Assert.IsType(model); + Assert.Same(generator, modelWithServices.Generator); + } + + [Theory] + [InlineData(typeof(SimpleModel))] + [InlineData(typeof(object))] + public void CreateReleaser_ReturnsNullForModelsThatDoNotImplementDisposable(Type pageType) + { + // Arrange + var context = new PageContext(); + var activator = new DefaultPageActivator(); + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = pageType.GetTypeInfo(), + }; + + // Act + var releaser = activator.CreateReleaser(actionDescriptor); + + // Assert + Assert.Null(releaser); + } + + [Fact] + public void CreateReleaser_CreatesDelegateThatDisposesDisposableTypes() + { + // Arrange + var context = new PageContext(); + var activator = new DefaultPageActivator(); + var actionDescriptor = new CompiledPageActionDescriptor + { + PageTypeInfo = typeof(DisposableModel).GetTypeInfo(), + }; + var model = new DisposableModel(); + + // Act & Assert + var releaser = activator.CreateReleaser(actionDescriptor); + releaser(context, model); + + // Assert + Assert.True(model.Disposed); + } + + private class SimpleModel + { + } + + private class ModelWithServices + { + public ModelWithServices(IHtmlGenerator generator) + { + Generator = generator; + } + + public IHtmlGenerator Generator { get; } + } + + private class DisposableModel : IDisposable + { + public bool Disposed { get; private set; } + + public void Dispose() + { + Disposed = true; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageModelFactoryProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageModelFactoryProviderTest.cs new file mode 100644 index 0000000000..b405f17e2a --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/DefaultPageModelFactoryProviderTest.cs @@ -0,0 +1,136 @@ +// 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; +using System.Reflection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public class DefaultPageModelFactoryProviderTest + { + [Fact] + public void CreateModelFactory_ReturnsNullFactoryIfModelTypeIsNull() + { + // Arrange + var descriptor = new CompiledPageActionDescriptor(); + var pageContext = new PageContext(); + var factoryProvider = CreateModelFactoryProvider(); + + // Act + var factory = factoryProvider.CreateModelFactory(descriptor); + + // Assert + Assert.Null(factory); + } + + [Fact] + public void CreateModelDisposer_ReturnsNullFactoryIfModelTypeIsNull() + { + // Arrange + var descriptor = new CompiledPageActionDescriptor(); + var pageContext = new PageContext(); + var factoryProvider = CreateModelFactoryProvider(); + + // Act + var disposer = factoryProvider.CreateModelDisposer(descriptor); + + // Assert + Assert.Null(disposer); + } + + [Fact] + public void ModelFactory_InitializesModelInstances() + { + // Arrange + var descriptor = new CompiledPageActionDescriptor + { + ModelTypeInfo = typeof(SimpleModel).GetTypeInfo(), + }; + var pageContext = new PageContext(); + var factoryProvider = CreateModelFactoryProvider(); + + // Act + var factory = factoryProvider.CreateModelFactory(descriptor); + var instance = factory(pageContext); + + // Assert + var model = Assert.IsType(instance); + Assert.NotNull(model); + } + + [Fact] + public void ModelFactory_InjectsPropertiesWithPageContextAttribute() + { + // Arrange + var descriptor = new CompiledPageActionDescriptor + { + ModelTypeInfo = typeof(ModelWithPageContext).GetTypeInfo(), + }; + var pageContext = new PageContext(); + var factoryProvider = CreateModelFactoryProvider(); + + // Act + var factory = factoryProvider.CreateModelFactory(descriptor); + var instance = factory(pageContext); + + // Assert + var testModel = Assert.IsType(instance); + Assert.Same(pageContext, testModel.ContextWithAttribute); + Assert.Null(testModel.ContextWithoutAttribute); + } + + [Fact] + public void CreateModelDisposer_ReturnsDisposerFromModelActivatorProvider() + { + // Arrange + var descriptor = new CompiledPageActionDescriptor + { + ModelTypeInfo = typeof(SimpleModel).GetTypeInfo() + }; + var pageContext = new PageContext(); + var modelActivatorProvider = new Mock(); + Action disposer = (_, __) => { }; + modelActivatorProvider.Setup(p => p.CreateReleaser(descriptor)) + .Returns(disposer); + var factoryProvider = CreateModelFactoryProvider(modelActivatorProvider.Object); + + // Act + var actual = factoryProvider.CreateModelDisposer(descriptor); + + // Assert + Assert.Same(disposer, actual); + } + + private static DefaultPageModelFactoryProvider CreateModelFactoryProvider( + IPageModelActivatorProvider modelActivator = null) + { + if (modelActivator == null) + { + var mockActivator = new Mock(); + mockActivator.Setup(a => a.CreateActivator(It.IsAny())) + .Returns((CompiledPageActionDescriptor descriptor) => + { + return (context) => Activator.CreateInstance(descriptor.ModelTypeInfo.AsType()); + }); + + modelActivator = mockActivator.Object; + } + + return new DefaultPageModelFactoryProvider(modelActivator); + } + + private class SimpleModel + { + } + + private class ModelWithPageContext + { + [PageContext] + public PageContext ContextWithAttribute { get; set; } + + public PageContext ContextWithoutAttribute { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index 38c5876b99..acc3aa8c75 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -3,24 +3,24 @@ using System; using System.Diagnostics; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class PageInvokerProviderTest { [Fact] - public void OnProvidersExecuting_PopulatesCacheEntry() + public void OnProvidersExecuting_WithEmptyModel_PopulatesCacheEntry() { // Arrange var descriptor = new PageActionDescriptor @@ -30,22 +30,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal }; Func factory = _ => null; Action releaser = (_, __) => { }; + var loader = new Mock(); loader.Setup(l => l.Load(It.IsAny())) .Returns(typeof(object)); var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); var actionDescriptorProvider = new Mock(); actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); - var factoryProvider = new Mock(); - factoryProvider.Setup(f => f.CreatePageFactory(It.IsAny())) + var pageFactoryProvider = new Mock(); + pageFactoryProvider.Setup(f => f.CreatePageFactory(It.IsAny())) .Returns(factory); - factoryProvider.Setup(f => f.CreatePageDisposer(It.IsAny())) + pageFactoryProvider.Setup(f => f.CreatePageDisposer(It.IsAny())) .Returns(releaser); var invokerProvider = CreateInvokerProvider( loader.Object, actionDescriptorProvider.Object, - factoryProvider.Object); + pageFactoryProvider.Object); var context = new ActionInvokerProviderContext( new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); @@ -60,6 +61,63 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Equal(descriptor.RelativePath, compiledPageActionDescriptor.RelativePath); Assert.Same(factory, entry.PageFactory); Assert.Same(releaser, entry.ReleasePage); + Assert.Null(entry.ModelFactory); + Assert.Null(entry.ReleaseModel); + } + + [Fact] + public void OnProvidersExecuting_WithModel_PopulatesCacheEntry() + { + // Arrange + var descriptor = new PageActionDescriptor + { + RelativePath = "Path1", + FilterDescriptors = new FilterDescriptor[0], + }; + Func factory = _ => null; + Action releaser = (_, __) => { }; + Func modelFactory = _ => null; + Action modelDisposer = (_, __) => { }; + + var loader = new Mock(); + loader.Setup(l => l.Load(It.IsAny())) + .Returns(typeof(PageWithModel)); + var descriptorCollection = new ActionDescriptorCollection(new[] { descriptor }, version: 1); + var actionDescriptorProvider = new Mock(); + actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); + var pageFactoryProvider = new Mock(); + pageFactoryProvider.Setup(f => f.CreatePageFactory(It.IsAny())) + .Returns(factory); + pageFactoryProvider.Setup(f => f.CreatePageDisposer(It.IsAny())) + .Returns(releaser); + + var modelFactoryProvider = new Mock(); + modelFactoryProvider.Setup(f => f.CreateModelFactory(It.IsAny())) + .Returns(modelFactory); + modelFactoryProvider.Setup(f => f.CreateModelDisposer(It.IsAny())) + .Returns(modelDisposer); + + var invokerProvider = CreateInvokerProvider( + loader.Object, + actionDescriptorProvider.Object, + pageFactoryProvider.Object, + modelFactoryProvider.Object); + var context = new ActionInvokerProviderContext( + new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); + + // Act + invokerProvider.OnProvidersExecuting(context); + + // Assert + Assert.NotNull(context.Result); + var actionInvoker = Assert.IsType(context.Result); + var entry = actionInvoker.CacheEntry; + var compiledPageActionDescriptor = Assert.IsType(entry.ActionDescriptor); + Assert.Equal(descriptor.RelativePath, compiledPageActionDescriptor.RelativePath); + Assert.Same(factory, entry.PageFactory); + Assert.Same(releaser, entry.ReleasePage); + Assert.Same(modelFactory, entry.ModelFactory); + Assert.Same(modelDisposer, entry.ReleaseModel); } [Fact] @@ -80,8 +138,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var invokerProvider = CreateInvokerProvider( loader.Object, - actionDescriptorProvider.Object, - Mock.Of()); + actionDescriptorProvider.Object); var context = new ActionInvokerProviderContext( new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); @@ -124,8 +181,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal .Returns(typeof(object)); var invokerProvider = CreateInvokerProvider( loader.Object, - actionDescriptorProvider.Object, - Mock.Of()); + actionDescriptorProvider.Object); var context = new ActionInvokerProviderContext( new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor)); @@ -150,7 +206,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private static PageActionInvokerProvider CreateInvokerProvider( IPageLoader loader, IActionDescriptorCollectionProvider actionDescriptorProvider, - IPageFactoryProvider factoryProvider) + IPageFactoryProvider pageProvider = null, + IPageModelFactoryProvider modelProvider = null) { var tempDataFactory = new Mock(); tempDataFactory.Setup(t => t.GetTempData(It.IsAny())) @@ -158,7 +215,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal return new PageActionInvokerProvider( loader, - factoryProvider, + pageProvider ?? Mock.Of(), + modelProvider ?? Mock.Of(), actionDescriptorProvider, new IFilterProvider[0], new IValueProviderFactory[0], @@ -169,5 +227,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal new DiagnosticListener("Microsoft.AspNetCore"), NullLoggerFactory.Instance); } + + private class PageWithModel + { + public object Model { get; set; } + } } }