From 639a788ed88610b7e65c1b9e6bb6a8a9e124b49a Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Mon, 6 Oct 2014 20:41:16 -0700 Subject: [PATCH] Tag Helpers: add `ModelExpression` class to support `Expression>` attributes - includes new `RazorPage.CreateModelExpression()` method - #1240 nit: - regenerating the resources reordered Microsoft.AspNet.Mvc.Core's Resources.designer.cs --- .../Properties/Resources.Designer.cs | 32 ++-- .../Rendering/ModelExpression.cs | 47 +++++ .../Properties/Resources.Designer.cs | 16 ++ .../RazorPageOfT.cs | 37 ++++ src/Microsoft.AspNet.Mvc.Razor/Resources.resx | 3 + .../RazorPageCreateModelExpressionTest.cs | 162 ++++++++++++++++++ 6 files changed, 281 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Rendering/ModelExpression.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 8144638ae8..db667803ed 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -1482,22 +1482,6 @@ namespace Microsoft.AspNet.Mvc.Core return GetString("AttributeRoute_NullTemplateRepresentation"); } - /// - /// "The path to the file must be absolute: {0}" - /// - internal static string FileResult_InvalidPathType_RelativeOrVirtualPath - { - get { return GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"); } - } - - /// - /// "The path to the file must be absolute: {0}" - /// - internal static string FormatFileResult_InvalidPathType_RelativeOrVirtualPath(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"), p0); - } - /// /// Multiple actions matched. The following actions matched route data and had all constraints satisfied:{0}{0}{1} /// @@ -1514,6 +1498,22 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("DefaultActionSelector_AmbiguousActions"), p0, p1); } + /// + /// "The path to the file must be absolute: {0}" + /// + internal static string FileResult_InvalidPathType_RelativeOrVirtualPath + { + get { return GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"); } + } + + /// + /// "The path to the file must be absolute: {0}" + /// + internal static string FormatFileResult_InvalidPathType_RelativeOrVirtualPath(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("FileResult_InvalidPathType_RelativeOrVirtualPath"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/ModelExpression.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/ModelExpression.cs new file mode 100644 index 0000000000..52d2faa068 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/ModelExpression.cs @@ -0,0 +1,47 @@ + +using System; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.AspNet.Mvc.ModelBinding; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + /// + /// Describes an passed to a tag helper. + /// + public class ModelExpression + { + /// + /// Initializes a new instance of the class. + /// + /// + /// String representation of the of interest. + /// + /// + /// Metadata about the of interest. + /// + public ModelExpression(string name, [NotNull] ModelMetadata metadata) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(name)); + } + + Name = name; + Metadata = metadata; + } + + /// + /// String representation of the of interest. + /// + public string Name { get; private set; } + + /// + /// Metadata about the of interest. + /// + /// + /// Getting will evaluate a compiled version of the original + /// . + /// + public ModelMetadata Metadata { get; private set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index 848fe0a25a..b18e7753ce 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -186,6 +186,22 @@ namespace Microsoft.AspNet.Mvc.Razor return GetString("RazorPage_YouCannotFlushWhileInAWritingScope"); } + /// + /// The {0} was unable to provide metadata for expression '{1}'. + /// + internal static string RazorPage_NullModelMetadata + { + get { return GetString("RazorPage_NullModelMetadata"); } + } + + /// + /// The {0} was unable to provide metadata for expression '{1}'. + /// + internal static string FormatRazorPage_NullModelMetadata(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("RazorPage_NullModelMetadata"), p0, p1); + } + /// /// {0} can only be called from a layout page. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPageOfT.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPageOfT.cs index f895cb794c..f9c84a659d 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPageOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPageOfT.cs @@ -1,6 +1,13 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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.Linq.Expressions; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Mvc.Rendering.Expressions; +using Microsoft.Framework.DependencyInjection; + namespace Microsoft.AspNet.Mvc.Razor { /// @@ -9,6 +16,8 @@ namespace Microsoft.AspNet.Mvc.Razor /// The type of the view data model. public abstract class RazorPage : RazorPage { + IModelMetadataProvider _provider; + public TModel Model { get @@ -19,5 +28,33 @@ namespace Microsoft.AspNet.Mvc.Razor [Activate] public ViewDataDictionary ViewData { get; set; } + + /// + /// Returns a instance describing the given . + /// + /// The type of the result. + /// An expression to be evaluated against the current model. + /// A new instance describing the given . + /// + /// + /// Compiler normally infers from the given . + /// + public ModelExpression CreateModelExpression([NotNull] Expression> expression) + { + if (_provider == null) + { + _provider = Context.RequestServices.GetService(); + } + + var name = ExpressionHelper.GetExpressionText(expression); + var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, _provider); + if (metadata == null) + { + throw new InvalidOperationException( + Resources.FormatRazorPage_NullModelMetadata(nameof(IModelMetadataProvider), name)); + } + + return new ModelExpression(name, metadata); + } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index 68439f48c4..2492e79319 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -150,6 +150,9 @@ You cannot flush while inside a writing scope. + + The {0} was unable to provide metadata for expression '{1}'. + {0} can only be called from a layout page. diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs new file mode 100644 index 0000000000..ea9ab573f5 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs @@ -0,0 +1,162 @@ + +using System; +using System.IO; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class RazorPageCreateModelExpressionTest + { + public static TheoryData>, string> IntExpressions + { + get + { + var somethingElse = 23; + return new TheoryData>, string> + { + { model => somethingElse, "somethingElse" }, + { model => model.Id, "Id" }, + { model => model.SubModel.Id, "SubModel.Id" }, + { model => model.SubModel.SubSubModel.Id, "SubModel.SubSubModel.Id" }, + }; + } + } + + public static TheoryData>, string> StringExpressions + { + get + { + var somethingElse = "This is something else"; + return new TheoryData>, string> + { + { model => somethingElse, "somethingElse" }, + { model => model.Name, "Name" }, + { model => model.SubModel.Name, "SubModel.Name" }, + { model => model.SubModel.SubSubModel.Name, "SubModel.SubSubModel.Name" }, + }; + } + } + + [Theory] + [MemberData(nameof(IntExpressions))] + public void CreateModelExpression_ReturnsExpectedMetadata_IntExpressions( + Expression> expression, + string expectedName) + { + // Arrange + var viewContext = CreateViewContext(model: null); + var page = CreatePage(viewContext); + + // Act + var result = page.CreateModelExpression(expression); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Metadata); + Assert.Equal(typeof(int), result.Metadata.ModelType); + Assert.Equal(expectedName, result.Name); + } + + [Theory] + [MemberData(nameof(StringExpressions))] + public void CreateModelExpression_ReturnsExpectedMetadata_StringExpressions( + Expression> expression, + string expectedName) + { + // Arrange + var viewContext = CreateViewContext(model: null); + var page = CreatePage(viewContext); + + // Act + var result = page.CreateModelExpression(expression); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Metadata); + Assert.Equal(typeof(string), result.Metadata.ModelType); + Assert.Equal(expectedName, result.Name); + } + + private static TestRazorPage CreatePage(ViewContext viewContext) + { + return new TestRazorPage + { + ViewContext = viewContext, + ViewData = (ViewDataDictionary)viewContext.ViewData, + }; + } + + private static ViewContext CreateViewContext(RazorPageCreateModelExpressionModel model) + { + return CreateViewContext(model, new DataAnnotationsModelMetadataProvider()); + } + + private static ViewContext CreateViewContext( + RazorPageCreateModelExpressionModel model, + IModelMetadataProvider provider) + { + var viewData = new ViewDataDictionary(provider) + { + Model = model, + }; + + var serviceProvider = new Mock(); + serviceProvider + .Setup(real => real.GetService(typeof(IModelMetadataProvider))) + .Returns(provider); + + var httpContext = new Mock(); + httpContext + .SetupGet(real => real.RequestServices) + .Returns(serviceProvider.Object); + + var actionContext = new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); + + return new ViewContext( + actionContext, + view: Mock.Of(), + viewData: viewData, + writer: new StringWriter()); + } + + private class TestRazorPage : RazorPage + { + public override Task ExecuteAsync() + { + throw new NotImplementedException(); + } + } + + public class RazorPageCreateModelExpressionModel + { + public int Id { get; set; } + + public string Name { get; set; } + + public RazorPageCreateModelExpressionSubModel SubModel { get; set; } + } + + public class RazorPageCreateModelExpressionSubModel + { + public int Id { get; set; } + + public string Name { get; set; } + + public RazorPageCreateModelExpressionSubSubModel SubSubModel { get; set; } + } + + public class RazorPageCreateModelExpressionSubSubModel + { + public int Id { get; set; } + + public string Name { get; set; } + } + } +} \ No newline at end of file