diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageOfT.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageOfT.cs index 29b9bf3792..7f54d0e730 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageOfT.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageOfT.cs @@ -37,6 +37,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor [RazorInject] public ViewDataDictionary ViewData { get; set; } + /// + /// Gets or sets the expression text cache for model expressions. + /// + [RazorInject] + private ExpressionTextCache ExpressionTextCache { get; set; } + /// /// Returns a instance describing the given . /// @@ -59,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor _provider = Context.RequestServices.GetRequiredService(); } - var name = ExpressionHelper.GetExpressionText(expression); + var name = ExpressionHelper.GetExpressionText(expression, ExpressionTextCache); var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, ViewData, _provider); if (modelExplorer == null) { diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index b6f1ae818e..178b9f6265 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -103,6 +103,7 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddTransient(); services.TryAddTransient(typeof(IHtmlHelper<>), typeof(HtmlHelper<>)); services.TryAddSingleton(); + services.TryAddSingleton(); // // JSON Helper diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ExpressionHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionHelper.cs similarity index 86% rename from src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ExpressionHelper.cs rename to src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionHelper.cs index 31fdf860b7..0d51a990e5 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ExpressionHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionHelper.cs @@ -7,9 +7,8 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; -namespace Microsoft.AspNetCore.Mvc.ViewFeatures +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { public static class ExpressionHelper { @@ -20,12 +19,25 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } public static string GetExpressionText(LambdaExpression expression) + { + return GetExpressionText(expression, expressionTextCache: null); + } + + public static string GetExpressionText(LambdaExpression expression, ExpressionTextCache expressionTextCache) { if (expression == null) { throw new ArgumentNullException(nameof(expression)); } + string expressionText; + if (expressionTextCache != null && + expressionTextCache.Entries.TryGetValue(expression, out expressionText)) + { + return expressionText; + } + + var containsIndexers = false; // Split apart the expression string for property/field accessors to create its name var nameParts = new Stack(); var part = expression.Body; @@ -34,6 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures { if (part.NodeType == ExpressionType.Call) { + containsIndexers = true; var methodExpression = (MethodCallExpression)part; if (!IsSingleArgumentIndexer(methodExpression)) { @@ -50,6 +63,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } else if (part.NodeType == ExpressionType.ArrayIndex) { + containsIndexers = true; var binaryExpression = (BinaryExpression)part; nameParts.Push( @@ -89,7 +103,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } else { - // Unsupported. break; } } @@ -100,12 +113,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures nameParts.Pop(); } + expressionText = string.Empty; if (nameParts.Count > 0) { - return nameParts.Aggregate((left, right) => left + right).TrimStart('.'); + expressionText = nameParts.Aggregate((left, right) => left + right).TrimStart('.'); } - return string.Empty; + if (expressionTextCache != null && !containsIndexers) + { + expressionTextCache.Entries.TryAdd(expression, expressionText); + } + + return expressionText; } private static string GetIndexerInvocation( diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionTextCache.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionTextCache.cs new file mode 100644 index 0000000000..1ebd526880 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionTextCache.cs @@ -0,0 +1,109 @@ +// 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.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq.Expressions; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +{ + /// + /// This class holds the cache for the expression text that is computed by ExpressionHelper. + /// + public class ExpressionTextCache + { + /// + public ConcurrentDictionary Entries { get; } = + new ConcurrentDictionary(LambdaExpressionComparer.Instance); + + // This comparer is tightly coupled with the logic of ExpressionHelper.GetExpressionText. + // It is not designed to accurately compare any two arbitrary LambdaExpressions. + private class LambdaExpressionComparer : IEqualityComparer + { + public static readonly LambdaExpressionComparer Instance = new LambdaExpressionComparer(); + + public bool Equals(LambdaExpression lambdaExpression1, LambdaExpression lambdaExpression2) + { + if (ReferenceEquals(lambdaExpression1,lambdaExpression2)) + { + return true; + } + // We will cache only pure member access expressions. Hence we compare two expressions + // to be equal only if they are identical member access expressions. + var expression1 = lambdaExpression1.Body; + var expression2 = lambdaExpression2.Body; + + while (true) + { + if (expression1 == null || expression2 == null) + { + return false; + } + + if (expression1.NodeType != expression2.NodeType) + { + return false; + } + + if (expression1.NodeType == ExpressionType.MemberAccess) + { + var memberExpression1 = (MemberExpression)expression1; + var memberName1 = memberExpression1.Member.Name; + expression1 = memberExpression1.Expression; + + var memberExpression2 = (MemberExpression)expression2; + var memberName2 = memberExpression2.Member.Name; + expression2 = memberExpression2.Expression; + + // If identifier contains "__", it is "reserved for use by the implementation" and likely compiler- + // or Razor-generated e.g. the name of a field in a delegate's generated class. + if (memberName1.Contains("__") && memberName2.Contains("__")) + { + return true; + } + + if (!string.Equals(memberName1, memberName2, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + else + { + return true; + } + } + } + + public int GetHashCode(LambdaExpression lambdaExpression) + { + var expression = lambdaExpression.Body; + var hashCodeCombiner = HashCodeCombiner.Start(); + + while (true) + { + if (expression != null && expression.NodeType == ExpressionType.MemberAccess) + { + var memberExpression = (MemberExpression)expression; + var memberName = memberExpression.Member.Name; + + if (memberName.Contains("__")) + { + break; + } + + hashCodeCombiner.Add(memberName, StringComparer.OrdinalIgnoreCase); + expression = memberExpression.Expression; + } + else + { + break; + } + } + + return hashCodeCombiner.CombinedHash; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs index 9c78d12614..006e47771c 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs @@ -4,7 +4,7 @@ using System; using System.Linq; using System.Linq.Expressions; -using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; namespace Microsoft.AspNetCore.Mvc.ModelBinding { diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs index a059d28ae5..d8625dc0b0 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelperOfT.cs @@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures { public class HtmlHelper : HtmlHelper, IHtmlHelper { + private readonly ExpressionTextCache _expressionTextCache; + /// /// Initializes a new instance of the class. /// @@ -24,7 +26,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures IModelMetadataProvider metadataProvider, IViewBufferScope bufferScope, HtmlEncoder htmlEncoder, - UrlEncoder urlEncoder) + UrlEncoder urlEncoder, + ExpressionTextCache expressionTextCache) : base( htmlGenerator, viewEngine, @@ -33,6 +36,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures htmlEncoder, urlEncoder) { + if (expressionTextCache == null) + { + throw new ArgumentNullException(nameof(expressionTextCache)); + } + + _expressionTextCache = expressionTextCache; } /// @@ -152,7 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures new ViewDataDictionary(ViewData, model: null), MetadataProvider); - var expressionText = ExpressionHelper.GetExpressionText(expression); + var expressionText = ExpressionHelper.GetExpressionText(expression, _expressionTextCache); if (modelExplorer == null) { throw new InvalidOperationException(Resources.FormatHtmlHelper_NullModelMetadata(expressionText)); @@ -352,7 +361,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures throw new ArgumentNullException(nameof(expression)); } - return ExpressionHelper.GetExpressionText(expression); + return ExpressionHelper.GetExpressionText(expression, _expressionTextCache); } protected ModelExplorer GetModelExplorer(Expression> expression) diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/project.json b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/project.json index 4b4d785d5e..600597d78d 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/project.json +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/project.json @@ -36,6 +36,10 @@ "version": "1.0.0-*", "type": "build" }, + "Microsoft.Extensions.HashCodeCombiner.Sources": { + "version": "1.0.0-*", + "type": "build" + }, "Microsoft.Extensions.WebEncoders": "1.0.0-*", "Newtonsoft.Json": "8.0.3", "System.Buffers": "4.0.0-*" diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageActivatorTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageActivatorTest.cs index 87b4fe7639..0dec02e66e 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageActivatorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageActivatorTest.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.WebEncoders.Testing; @@ -39,6 +40,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor .AddSingleton(myService) .AddSingleton(helper) .AddSingleton(htmlEncoder) + .AddSingleton(new ExpressionTextCache()) .AddSingleton(diagnosticSource) .BuildServiceProvider(); var httpContext = new DefaultHttpContext @@ -110,6 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor .AddSingleton(myService) .AddSingleton(helper) .AddSingleton(htmlEncoder) + .AddSingleton(new ExpressionTextCache()) .AddSingleton(new DiagnosticListener("Microsoft.AspNetCore.Mvc")) .BuildServiceProvider(); var httpContext = new DefaultHttpContext @@ -150,6 +153,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor .AddSingleton(myService) .AddSingleton(helper) .AddSingleton(htmlEncoder) + .AddSingleton(new ExpressionTextCache()) .AddSingleton(new DiagnosticListener("Microsoft.AspNetCore.Mvc")) .BuildServiceProvider(); var httpContext = new DefaultHttpContext @@ -190,6 +194,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor .AddSingleton(myService) .AddSingleton(helper) .AddSingleton(htmlEncoder) + .AddSingleton(new ExpressionTextCache()) .AddSingleton(new DiagnosticListener("Microsoft.AspNetCore.Mvc")) .BuildServiceProvider(); var httpContext = new DefaultHttpContext @@ -224,6 +229,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor var collection = new ServiceCollection(); collection .AddSingleton(new HtmlTestEncoder()) + .AddSingleton(new ExpressionTextCache()) .AddSingleton(new DiagnosticListener("Microsoft.AspNetCore.Mvc")); var httpContext = new DefaultHttpContext { @@ -256,6 +262,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor .AddSingleton() .AddSingleton(new HtmlTestEncoder()) .AddSingleton(new DiagnosticListener("Microsoft.AspNetCore.Mvc")) + .AddSingleton(new ExpressionTextCache()) .AddSingleton(); var httpContext = new DefaultHttpContext { diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs index 6e7831e82f..2396740c06 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorPageCreateModelExpressionTest.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Routing; using Moq; using Xunit; @@ -116,6 +117,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor serviceProvider .Setup(real => real.GetService(typeof(IModelMetadataProvider))) .Returns(provider); + serviceProvider + .Setup(real => real.GetService(typeof(ExpressionTextCache))) + .Returns(new ExpressionTextCache()); var httpContext = new Mock(); httpContext diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionHelperTest.cs new file mode 100644 index 0000000000..0a9841b094 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionHelperTest.cs @@ -0,0 +1,242 @@ +// 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.Collections.Generic; +using System.Linq.Expressions; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +{ + public class ExpressionHelperTest + { + private readonly ExpressionTextCache _expressionTextCache = new ExpressionTextCache(); + + public static IEnumerable CachedExpressions + { + get + { + var key = "TestModel"; + var myModel = new TestModel(); + + return new TheoryData + { + (Expression>)(model => model.SelectedCategory), + (Expression>)(model => model.SelectedCategory.CategoryName), + (Expression>)(testModel => testModel.SelectedCategory.CategoryId), + (Expression>)(model => model.SelectedCategory.CategoryName.MainCategory), + (Expression>)(testModel => key), + (Expression>)(m => m), + (Expression>)(m => myModel.SelectedCategory), + }; + } + } + + public static IEnumerable IndexerExpressions + { + get + { + var i = 3; + var key = "TestModel"; + var myModels = new List(); + + return new TheoryData + { + (Expression, Category>>)(model => model[2].SelectedCategory), + (Expression, Category>>)(model => myModels[i].SelectedCategory), + (Expression, CategoryName>>)(testModel => testModel[i].SelectedCategory.CategoryName), + (Expression>)(model => model.PreferredCategories[i].CategoryId), + (Expression, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory), + }; + } + } + + public static IEnumerable EquivalentExpressions + { + get + { + var value = "Test"; + var Model = "Test"; + + return new TheoryData + { + { + (Expression>)(model => model.SelectedCategory), + (Expression>)(model => model.SelectedCategory) + }, + { + (Expression>)(model => model.SelectedCategory.CategoryName), + (Expression>)(model => model.SelectedCategory.CategoryName) + }, + { + (Expression>)(testModel => testModel.SelectedCategory.CategoryId), + (Expression>)(testModel => testModel.SelectedCategory.CategoryId) + }, + { + (Expression>)(model => model.SelectedCategory.CategoryName.MainCategory), + (Expression>)(model => model.SelectedCategory.CategoryName.MainCategory) + }, + { + (Expression>)(model => model), + (Expression>)(m => m) + }, + { + (Expression>)(model => value), + (Expression>)(m => value) + }, + { + // These two expressions are not actually equivalent. However ExpressionHelper returns + // string.Empty for these two expressions and hence they are considered as equivalent by the + // cache. + (Expression>)(m => Model), + (Expression>)(m => m) + }, + }; + } + } + + public static IEnumerable NonEquivalentExpressions + { + get + { + var value = "test"; + var key = "TestModel"; + var Model = "Test"; + var myModel = new TestModel(); + + return new TheoryData + { + { + (Expression>)(model => model.SelectedCategory), + (Expression>)(model => model.SelectedCategory.CategoryName) + }, + { + (Expression>)(model => model.Model), + (Expression>)(model => model.Name) + }, + { + (Expression>)(model => model.SelectedCategory.CategoryName), + (Expression>)(model => value) + }, + { + (Expression>)(testModel => testModel.SelectedCategory.CategoryName.MainCategory), + (Expression>)(testModel => value) + }, + { + (Expression, Category>>)(model => model[2].SelectedCategory), + (Expression>)(model => model.SelectedCategory.CategoryName.MainCategory) + }, + { + (Expression>)(testModel => testModel.SelectedCategory.CategoryId), + (Expression>)(model => model.SelectedCategory) + }, + { + (Expression, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory), + (Expression>)(model => model.SelectedCategory) + }, + { + (Expression, Category>>)(model => model[2].SelectedCategory), + (Expression, Category>>)(model => model[2].SelectedCategory) + }, + { + (Expression>)(m => Model), + (Expression>)(m => m.Model) + }, + { + (Expression>)(m => m), + (Expression>)(m => m.Model) + }, + { + (Expression>)(m => myModel.Name), + (Expression>)(m => m.Name) + }, + { + (Expression>)(m => key), + (Expression>)(m => value) + }, + { + (Expression, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory), + (Expression, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory) + }, + }; + } + } + + [Theory] + [MemberData(nameof(CachedExpressions))] + public void GetExpressionText_CachesExpression(LambdaExpression expression) + { + // Act - 1 + var text1 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache); + + // Act - 2 + var text2 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache); + + // Assert + Assert.Same(text1, text2); // Cached + } + + [Theory] + [MemberData(nameof(IndexerExpressions))] + public void GetExpressionText_DoesNotCacheIndexerExpression(LambdaExpression expression) + { + // Act - 1 + var text1 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache); + + // Act - 2 + var text2 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache); + + // Assert + Assert.NotSame(text1, text2); // not cached + } + + [Theory] + [MemberData(nameof(EquivalentExpressions))] + public void GetExpressionText_CacheEquivalentExpressions(LambdaExpression expression1, LambdaExpression expression2) + { + // Act - 1 + var text1 = ExpressionHelper.GetExpressionText(expression1, _expressionTextCache); + + // Act - 2 + var text2 = ExpressionHelper.GetExpressionText(expression2, _expressionTextCache); + + // Assert + Assert.Same(text1, text2); + } + + [Theory] + [MemberData(nameof(NonEquivalentExpressions))] + public void GetExpressionText_CheckNonEquivalentExpressions(LambdaExpression expression1, LambdaExpression expression2) + { + // Act - 1 + var text1 = ExpressionHelper.GetExpressionText(expression1, _expressionTextCache); + + // Act - 2 + var text2 = ExpressionHelper.GetExpressionText(expression2, _expressionTextCache); + + // Assert + Assert.NotSame(text1, text2); + } + + private class TestModel + { + public string Name { get; set; } + public string Model { get; set; } + public Category SelectedCategory { get; set; } + + public IList PreferredCategories { get; set; } + } + + private class Category + { + public int CategoryId { get; set; } + public CategoryName CategoryName { get; set; } + } + + private class CategoryName + { + public string MainCategory { get; set; } + public string SubCategory { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs index 8f3d5148c0..8bae37c883 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs @@ -243,6 +243,8 @@ namespace Microsoft.AspNetCore.Mvc.Rendering .Setup(f => f.GetUrlHelper(It.IsAny())) .Returns(urlHelper); + var expressionTextCache = new ExpressionTextCache(); + if (htmlGenerator == null) { htmlGenerator = new DefaultHtmlGenerator( @@ -284,7 +286,8 @@ namespace Microsoft.AspNetCore.Mvc.Rendering provider, new TestViewBufferScope(), new HtmlTestEncoder(), - UrlEncoder.Default); + UrlEncoder.Default, + expressionTextCache); var viewContext = new ViewContext( actionContext, diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs index c7edc18b2e..14ae39fcd1 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs @@ -519,6 +519,7 @@ namespace Microsoft.AspNetCore.Mvc var services = new ServiceCollection(); services.AddSingleton(diagnosticSource); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton, TestOptionsManager>(); services.AddTransient(); services.AddSingleton();