diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index 3f8efab48e..f944874d6f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -127,18 +127,20 @@ public static IMvcCoreBuilder InitializeTagHelper( // Internal for testing. internal static void AddRazorViewEngineServices(IServiceCollection services) { - services.TryAddEnumerable( - ServiceDescriptor.Transient< - IConfigureOptions, - DependencyContextRazorViewEngineOptionsSetup>()); - // This caches compilation related details that are valid across the lifetime of the application. services.TryAddSingleton(); services.TryAddEnumerable( ServiceDescriptor.Transient, MvcRazorMvcViewOptionsSetup>()); + + // DependencyContextRazorViewEngineOptionsSetup needs to run after RazorViewEngineOptionsSetup. + // The ordering of the following two lines is important to ensure this behavior. services.TryAddEnumerable( ServiceDescriptor.Transient, RazorViewEngineOptionsSetup>()); + services.TryAddEnumerable( + ServiceDescriptor.Transient< + IConfigureOptions, + DependencyContextRazorViewEngineOptionsSetup>()); services.TryAddSingleton< IRazorViewEngineFileProviderAccessor, diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DependencyContextRazorViewEngineOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DependencyContextRazorViewEngineOptionsSetup.cs index e7f4bc5960..41913d8173 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DependencyContextRazorViewEngineOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DependencyContextRazorViewEngineOptionsSetup.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.Extensions.DependencyModel; using Microsoft.Extensions.Options; +using DependencyContextOptions = Microsoft.Extensions.DependencyModel.CompilationOptions; namespace Microsoft.AspNetCore.Mvc.Razor.Internal { @@ -17,27 +18,46 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal /// Sets up compilation and parse option default options for using /// /// - public class DependencyContextRazorViewEngineOptionsSetup : ConfigureOptions + public class DependencyContextRazorViewEngineOptionsSetup : IConfigureOptions { + private readonly IHostingEnvironment _hostingEnvironment; + /// /// Initializes a new instance of . /// public DependencyContextRazorViewEngineOptionsSetup(IHostingEnvironment hostingEnvironment) - : base(options => ConfigureRazor(options, hostingEnvironment)) { + _hostingEnvironment = hostingEnvironment; } - private static void ConfigureRazor(RazorViewEngineOptions options, IHostingEnvironment hostingEnvironment) + /// + public void Configure(RazorViewEngineOptions options) { - var applicationAssembly = Assembly.Load(new AssemblyName(hostingEnvironment.ApplicationName)); - var dependencyContext = DependencyContext.Load(applicationAssembly); - var compilationOptions = dependencyContext?.CompilationOptions ?? Extensions.DependencyModel.CompilationOptions.Default; + var compilationOptions = GetCompilationOptions(); SetParseOptions(options, compilationOptions); SetCompilationOptions(options, compilationOptions); } - private static void SetCompilationOptions(RazorViewEngineOptions options, Extensions.DependencyModel.CompilationOptions compilationOptions) + // Internal for unit testing. + protected internal virtual DependencyContextOptions GetCompilationOptions() + { + if (!string.IsNullOrEmpty(_hostingEnvironment.ApplicationName)) + { + var applicationAssembly = Assembly.Load(new AssemblyName(_hostingEnvironment.ApplicationName)); + var dependencyContext = DependencyContext.Load(applicationAssembly); + if (dependencyContext?.CompilationOptions != null) + { + return dependencyContext.CompilationOptions; + } + } + + return DependencyContextOptions.Default; + } + + private static void SetCompilationOptions( + RazorViewEngineOptions options, + DependencyContextOptions compilationOptions) { var roslynOptions = options.CompilationOptions; @@ -57,17 +77,17 @@ private static void SetCompilationOptions(RazorViewEngineOptions options, Extens if (compilationOptions.Optimize.HasValue) { - var optimizationLevel = compilationOptions.Optimize.Value - ? OptimizationLevel.Debug - : OptimizationLevel.Release; + var optimizationLevel = compilationOptions.Optimize.Value ? + OptimizationLevel.Release : + OptimizationLevel.Debug; roslynOptions = roslynOptions.WithOptimizationLevel(optimizationLevel); } if (compilationOptions.WarningsAsErrors.HasValue) { - var reportDiagnostic = compilationOptions.WarningsAsErrors.Value - ? ReportDiagnostic.Error - : ReportDiagnostic.Default; + var reportDiagnostic = compilationOptions.WarningsAsErrors.Value ? + ReportDiagnostic.Error : + ReportDiagnostic.Default; roslynOptions = roslynOptions.WithGeneralDiagnosticOption(reportDiagnostic); } @@ -76,7 +96,7 @@ private static void SetCompilationOptions(RazorViewEngineOptions options, Extens private static void SetParseOptions( RazorViewEngineOptions options, - Extensions.DependencyModel.CompilationOptions compilationOptions) + DependencyContextOptions compilationOptions) { var parseOptions = options.ParseOptions; parseOptions = parseOptions.WithPreprocessorSymbols( diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptionsSetup.cs index dbba63a6c3..cb2107c61e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptionsSetup.cs @@ -12,51 +12,51 @@ namespace Microsoft.AspNetCore.Mvc /// /// Sets up default options for . /// - public class RazorViewEngineOptionsSetup : ConfigureOptions + public class RazorViewEngineOptionsSetup : IConfigureOptions { + private readonly IHostingEnvironment _hostingEnvironment; + /// /// Initializes a new instance of . /// /// for the application. - public RazorViewEngineOptionsSetup( - IHostingEnvironment hostingEnvironment) - : base(options => ConfigureRazor(options, hostingEnvironment)) + public RazorViewEngineOptionsSetup(IHostingEnvironment hostingEnvironment) { + _hostingEnvironment = hostingEnvironment; } - private static void ConfigureRazor( - RazorViewEngineOptions razorOptions, - IHostingEnvironment hostingEnvironment) + /// + public void Configure(RazorViewEngineOptions options) { - if (hostingEnvironment.ContentRootFileProvider != null) + if (_hostingEnvironment.ContentRootFileProvider != null) { - razorOptions.FileProviders.Add(hostingEnvironment.ContentRootFileProvider); + options.FileProviders.Add(_hostingEnvironment.ContentRootFileProvider); } - var compilationOptions = razorOptions.CompilationOptions; + var compilationOptions = options.CompilationOptions; string configurationSymbol; - if (hostingEnvironment.IsDevelopment()) + if (_hostingEnvironment.IsDevelopment()) { configurationSymbol = "DEBUG"; - razorOptions.CompilationOptions = compilationOptions.WithOptimizationLevel(OptimizationLevel.Debug); + options.CompilationOptions = compilationOptions.WithOptimizationLevel(OptimizationLevel.Debug); } else { configurationSymbol = "RELEASE"; - razorOptions.CompilationOptions = compilationOptions.WithOptimizationLevel(OptimizationLevel.Release); + options.CompilationOptions = compilationOptions.WithOptimizationLevel(OptimizationLevel.Release); } - var parseOptions = razorOptions.ParseOptions; - razorOptions.ParseOptions = parseOptions.WithPreprocessorSymbols( + var parseOptions = options.ParseOptions; + options.ParseOptions = parseOptions.WithPreprocessorSymbols( parseOptions.PreprocessorSymbolNames.Concat(new[] { configurationSymbol })); - razorOptions.ViewLocationFormats.Add("/Views/{1}/{0}" + RazorViewEngine.ViewExtension); - razorOptions.ViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension); + options.ViewLocationFormats.Add("/Views/{1}/{0}" + RazorViewEngine.ViewExtension); + options.ViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension); - razorOptions.AreaViewLocationFormats.Add("/Areas/{2}/Views/{1}/{0}" + RazorViewEngine.ViewExtension); - razorOptions.AreaViewLocationFormats.Add("/Areas/{2}/Views/Shared/{0}" + RazorViewEngine.ViewExtension); - razorOptions.AreaViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension); + options.AreaViewLocationFormats.Add("/Areas/{2}/Views/{1}/{0}" + RazorViewEngine.ViewExtension); + options.AreaViewLocationFormats.Add("/Areas/{2}/Views/Shared/{0}" + RazorViewEngine.ViewExtension); + options.AreaViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DependencyContextRazorViewEngineOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DependencyContextRazorViewEngineOptionsSetupTest.cs new file mode 100644 index 0000000000..e7e9676fc9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DependencyContextRazorViewEngineOptionsSetupTest.cs @@ -0,0 +1,301 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Moq; +using Xunit; +using DependencyContextOptions = Microsoft.Extensions.DependencyModel.CompilationOptions; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class DependencyContextRazorViewEngineOptionsSetupTest + { + [Theory] + [InlineData(null)] + [InlineData("")] + public void GetCompilationOptions_ReturnsDefaultOptionsIfApplicationNameIsNullOrEmpty(string name) + { + // Arrange + var hostingEnvironment = new Mock(); + hostingEnvironment.SetupGet(e => e.ApplicationName) + .Returns(name); + var setup = new TestableDependencyContextOptionsSetup(hostingEnvironment.Object); + + // Act + var options = setup.GetCompilationOptionsPublic(); + + // Assert + Assert.Same(DependencyContextOptions.Default, options); + } + + [Fact] + public void GetCompilationOptions_ReturnsDefaultOptionsIfApplicationDoesNotHaveDependencyContext() + { + // Arrange + var hostingEnvironment = new Mock(); + hostingEnvironment.SetupGet(e => e.ApplicationName) + .Returns(typeof(Controller).GetTypeInfo().Assembly.GetName().Name); + var setup = new TestableDependencyContextOptionsSetup(hostingEnvironment.Object); + + // Act + var options = setup.GetCompilationOptionsPublic(); + + // Assert + Assert.Same(DependencyContextOptions.Default, options); + } + + [Fact] + public void GetCompilationOptions_ReturnsCompilationOptionsFromDependencyContext() + { + // Arrange + var hostingEnvironment = new Mock(); + hostingEnvironment.SetupGet(e => e.ApplicationName) + .Returns(GetType().GetTypeInfo().Assembly.GetName().Name); + var setup = new TestableDependencyContextOptionsSetup(hostingEnvironment.Object); + + // Act + var options = setup.GetCompilationOptionsPublic(); + + // Assert + Assert.Contains("SOME_TEST_DEFINE", options.Defines); + } + + [Fact] + public void Configure_UsesDefaultCompilationOptions() + { + // Arrange + var hostingEnvironment = new Mock(); + var setup = new DependencyContextRazorViewEngineOptionsSetup(hostingEnvironment.Object); + var options = new RazorViewEngineOptions(); + + // Act + setup.Configure(options); + + // Assert + var compilationOptions = options.CompilationOptions; + var parseOptions = options.ParseOptions; + Assert.False(compilationOptions.AllowUnsafe); + Assert.Equal(ReportDiagnostic.Default, compilationOptions.GeneralDiagnosticOption); + Assert.Equal(OptimizationLevel.Debug, compilationOptions.OptimizationLevel); + Assert.Collection(compilationOptions.SpecificDiagnosticOptions.OrderBy(d => d.Key), + item => + { + Assert.Equal("CS1701", item.Key); + Assert.Equal(ReportDiagnostic.Suppress, item.Value); + }, + item => + { + Assert.Equal("CS1702", item.Key); + Assert.Equal(ReportDiagnostic.Suppress, item.Value); + }, + item => + { + Assert.Equal("CS1705", item.Key); + Assert.Equal(ReportDiagnostic.Suppress, item.Value); + }); + + Assert.Empty(parseOptions.PreprocessorSymbolNames); + Assert.Equal(LanguageVersion.CSharp6, parseOptions.LanguageVersion); + } + + [Fact] + public void Configure_SetsAllowUnsafe() + { + // Arrange + var dependencyContextOptions = new DependencyContextOptions( + new[] { "MyDefine" }, + languageVersion: null, + platform: null, + allowUnsafe: true, + warningsAsErrors: null, + optimize: null, + keyFile: null, + delaySign: null, + publicSign: null, + debugType: null, + emitEntryPoint: null, + generateXmlDocumentation: null); + var setup = new TestableDependencyContextOptionsSetup(dependencyContextOptions); + var options = new RazorViewEngineOptions(); + + // Act + setup.Configure(options); + + // Assert + Assert.True(options.CompilationOptions.AllowUnsafe); + Assert.Equal(ReportDiagnostic.Default, options.CompilationOptions.GeneralDiagnosticOption); + Assert.Equal(OptimizationLevel.Debug, options.CompilationOptions.OptimizationLevel); + } + + [Fact] + public void Configure_SetsDiagnosticOption() + { + // Arrange + var dependencyContextOptions = new DependencyContextOptions( + new[] { "MyDefine" }, + languageVersion: null, + platform: null, + allowUnsafe: null, + warningsAsErrors: true, + optimize: null, + keyFile: null, + delaySign: null, + publicSign: null, + debugType: null, + emitEntryPoint: null, + generateXmlDocumentation: null); + var setup = new TestableDependencyContextOptionsSetup(dependencyContextOptions); + var options = new RazorViewEngineOptions(); + + // Act + setup.Configure(options); + + // Assert + Assert.False(options.CompilationOptions.AllowUnsafe); + Assert.Equal(ReportDiagnostic.Error, options.CompilationOptions.GeneralDiagnosticOption); + Assert.Equal(OptimizationLevel.Debug, options.CompilationOptions.OptimizationLevel); + } + + [Fact] + public void Configure_SetsOptimizationLevel() + { + // Arrange + var dependencyContextOptions = new DependencyContextOptions( + new[] { "MyDefine" }, + languageVersion: null, + platform: null, + allowUnsafe: null, + warningsAsErrors: null, + optimize: true, + keyFile: null, + delaySign: null, + publicSign: null, + debugType: null, + emitEntryPoint: null, + generateXmlDocumentation: null); + var setup = new TestableDependencyContextOptionsSetup(dependencyContextOptions); + var options = new RazorViewEngineOptions(); + + // Act + setup.Configure(options); + + // Assert + Assert.False(options.CompilationOptions.AllowUnsafe); + Assert.Equal(ReportDiagnostic.Default, options.CompilationOptions.GeneralDiagnosticOption); + Assert.Equal(OptimizationLevel.Release, options.CompilationOptions.OptimizationLevel); + } + + [Fact] + public void Configure_SetsLanguageVersion() + { + // Arrange + var dependencyContextOptions = new DependencyContextOptions( + new[] { "MyDefine" }, + languageVersion: "csharp4", + platform: null, + allowUnsafe: null, + warningsAsErrors: null, + optimize: true, + keyFile: null, + delaySign: null, + publicSign: null, + debugType: null, + emitEntryPoint: null, + generateXmlDocumentation: null); + var setup = new TestableDependencyContextOptionsSetup(dependencyContextOptions); + var options = new RazorViewEngineOptions(); + + // Act + setup.Configure(options); + + // Assert + Assert.Equal(LanguageVersion.CSharp4, options.ParseOptions.LanguageVersion); + } + + [Fact] + public void Configure_SetsDefines() + { + // Arrange + var dependencyContextOptions = new DependencyContextOptions( + new[] { "MyDefine" }, + languageVersion: "csharp4", + platform: null, + allowUnsafe: null, + warningsAsErrors: null, + optimize: true, + keyFile: null, + delaySign: null, + publicSign: null, + debugType: null, + emitEntryPoint: null, + generateXmlDocumentation: null); + var setup = new TestableDependencyContextOptionsSetup(dependencyContextOptions); + var options = new RazorViewEngineOptions(); + + // Act + setup.Configure(options); + + // Assert + Assert.Equal(new[] { "MyDefine" }, options.ParseOptions.PreprocessorSymbolNames); + } + + [Fact] + public void ConfigureAfterRazorViewEngineOptionsSetupIsExecuted_CorrectlySetsUpOptimizationLevel() + { + // Arrange + var dependencyContextOptions = new DependencyContextOptions( + new[] { "MyDefine" }, + languageVersion: null, + platform: null, + allowUnsafe: null, + warningsAsErrors: null, + optimize: true, + keyFile: null, + delaySign: null, + publicSign: null, + debugType: null, + emitEntryPoint: null, + generateXmlDocumentation: null); + var dependencyContextSetup = new TestableDependencyContextOptionsSetup(dependencyContextOptions); + var options = new RazorViewEngineOptions(); + var hostingEnvironment = new Mock(); + hostingEnvironment.SetupGet(e => e.EnvironmentName) + .Returns("Development"); + var viewEngineSetup = new RazorViewEngineOptionsSetup(hostingEnvironment.Object); + + // Act + viewEngineSetup.Configure(options); + dependencyContextSetup.Configure(options); + + // Assert + Assert.Equal(OptimizationLevel.Release, options.CompilationOptions.OptimizationLevel); + } + + private class TestableDependencyContextOptionsSetup : DependencyContextRazorViewEngineOptionsSetup + { + private readonly DependencyContextOptions _options; + + public TestableDependencyContextOptionsSetup(IHostingEnvironment hostingEnvironment) + : base(hostingEnvironment) + { + } + + public TestableDependencyContextOptionsSetup(DependencyContextOptions options) + : base(Mock.Of()) + { + _options = options; + } + + protected internal override DependencyContextOptions GetCompilationOptions() + { + return _options ?? base.GetCompilationOptions(); + } + + public DependencyContextOptions GetCompilationOptionsPublic() => base.GetCompilationOptions(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Test/RazorViewEngineOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewEngineOptionsSetupTest.cs similarity index 97% rename from test/Microsoft.AspNetCore.Mvc.Test/RazorViewEngineOptionsSetupTest.cs rename to test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewEngineOptionsSetupTest.cs index 5969526041..34a6ca46a3 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/RazorViewEngineOptionsSetupTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewEngineOptionsSetupTest.cs @@ -2,13 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.CodeAnalysis; using Microsoft.Extensions.FileProviders; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Mvc +namespace Microsoft.AspNetCore.Mvc.Razor { public class RazorViewEngineOptionsSetupTest { diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json b/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json index 43f1615198..4395f71f7b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/project.json @@ -10,7 +10,10 @@ "../Microsoft.AspNetCore.Mvc.Razor.Host.Test/TestFileTrigger.cs", "../Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TestViewBufferScope.cs" ] - } + }, + "define": [ + "SOME_TEST_DEFINE" + ] }, "dependencies": { "dotnet-test-xunit": "2.2.0-*",