From 5b28c06d6453696e98fb7bf32abe3aef09a5c26b Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 2 Feb 2018 17:41:14 -0800 Subject: [PATCH] Add prelimianry support for extensions to Razor (#2012) * Add prelimianry support for extensions to Razor This PR adds MSBuild insfrastructure to the SDK that can understand concepts we need to expose to the project, code generator and runtime like: - Language version - Configuration - Extensions (plugins) As an example of how this works, I've done the wireup for MVC. This will now generate assembly attributes in your application that can act as a source-of-truth for what should be included in runtime compilation, and it's all based on the project-file. This means that it can be delivered and configured by packages. The next step here is to implement a loader for RazorProjectEngine based on these primitives, and then use it in our CLI tools and MVC. The next step after that is to expose it in VS and VS4Mac through the project system. --- ...oft.AspNetCore.Mvc.Razor.Extensions.csproj | 5 + .../Microsoft.AspNetCore.Mvc.Razor.props | 32 ++++++ .../Microsoft.AspNetCore.Mvc.Razor.targets | 9 ++ .../Microsoft.AspNetCore.Razor.Design.props | 20 +++- .../Properties/Resources.Designer.cs | 14 +++ .../RazorConfiguration.cs | 38 ++++++- .../RazorEngine.cs | 3 +- .../RazorExtension.cs | 10 ++ .../RazorLanguageVersion.cs | 78 ++++++++++++- .../RazorParserFeatureFlags.cs | 3 +- .../Resources.resx | 3 + .../RazorConfigurationNameAttribute.cs | 38 +++++++ .../RazorExtensionAssemblyNameAttribute.cs | 50 +++++++++ .../Hosting/RazorLanguageVersionAttribute.cs | 38 +++++++ .../Sdk.Razor.CurrentVersion.targets | 40 +++++++ .../DefaultTemplateEngineFactoryService.cs | 2 +- .../IntegrationTests/Assert.cs | 104 ++++++++++++++++++ .../ConfigurationMetadataIntegrationTest.cs | 90 +++++++++++++++ .../AppWithP2PReference.csproj | 12 +- .../testapps/ClassLibrary/ClassLibrary.csproj | 12 +- test/testapps/Directory.Build.targets | 1 - test/testapps/SimpleMvc/SimpleMvc.csproj | 11 +- test/testapps/SimplePages/SimplePages.csproj | 11 +- 23 files changed, 607 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.props create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.targets create mode 100644 src/Microsoft.AspNetCore.Razor.Language/RazorExtension.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorConfigurationNameAttribute.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorExtensionAssemblyNameAttribute.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorLanguageVersionAttribute.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/ConfigurationMetadataIntegrationTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj index 37eafe0684..401d07c030 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj @@ -12,6 +12,11 @@ + + + + + diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.props b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.props new file mode 100644 index 0000000000..8d2ac3b630 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.props @@ -0,0 +1,32 @@ + + + + + MVC-2.1 + + + + + + MVC-2.1;$(CustomRazorExtension) + + + + + + Microsoft.AspNetCore.Mvc.Razor.Extensions + $(MSBuildThisFileDirectory)..\..\lib\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll + + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.targets b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.targets new file mode 100644 index 0000000000..61a0e7a8dc --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.targets @@ -0,0 +1,9 @@ + + + + + true + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.props b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.props index 6549731539..c25a2ab174 100644 --- a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.props +++ b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.props @@ -1,15 +1,29 @@  true $(MSBuildThisFileDirectory)Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets + + + 2.1 diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs index 6193fcb066..13c56c698e 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs @@ -1856,6 +1856,20 @@ namespace Microsoft.AspNetCore.Razor.Language internal static string FormatRazorProjectEngineMissingFeatureDependency(object p0, object p1) => string.Format(CultureInfo.CurrentCulture, GetString("RazorProjectEngineMissingFeatureDependency"), p0, p1); + /// + /// The Razor language version '{0}' is unrecognized or not supported by this version of Razor. + /// + internal static string RazorLanguageVersion_InvalidVersion + { + get => GetString("RazorLanguageVersion_InvalidVersion"); + } + + /// + /// The Razor language version '{0}' is unrecognized or not supported by this version of Razor. + /// + internal static string FormatRazorLanguageVersion_InvalidVersion(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("RazorLanguageVersion_InvalidVersion"), p0); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs index 103c3fbec5..0f00751497 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs @@ -2,24 +2,58 @@ // 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; namespace Microsoft.AspNetCore.Razor.Language { public sealed class RazorConfiguration { - public static readonly RazorConfiguration Default = new RazorConfiguration(RazorLanguageVersion.Latest, designTime: false); + public static readonly RazorConfiguration Default = new RazorConfiguration( + RazorLanguageVersion.Latest, + "unnamed", + Array.Empty(), + designTime: false); - public RazorConfiguration(RazorLanguageVersion languageVersion, bool designTime) + // This is used only in some back-compat scenarios. We don't expose it because there's no + // use case for anyone else to use it. + internal static readonly RazorConfiguration DefaultDesignTime = new RazorConfiguration( + RazorLanguageVersion.Latest, + "unnamed", + Array.Empty(), + designTime: true); + + public RazorConfiguration( + RazorLanguageVersion languageVersion, + string configurationName, + IEnumerable extensions, + bool designTime) { if (languageVersion == null) { throw new ArgumentNullException(nameof(languageVersion)); } + if (configurationName == null) + { + throw new ArgumentNullException(nameof(configurationName)); + } + + if (extensions == null) + { + throw new ArgumentNullException(nameof(extensions)); + } + LanguageVersion = languageVersion; + ConfigurationName = configurationName; + Extensions = extensions.ToArray(); DesignTime = designTime; } + public string ConfigurationName { get; } + + public IReadOnlyList Extensions { get; } + public RazorLanguageVersion LanguageVersion { get; } public bool DesignTime { get; } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs index 184afd70ac..a48c31893a 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs @@ -24,8 +24,7 @@ namespace Microsoft.AspNetCore.Razor.Language return CreateDesignTime(configure: null); } - public static RazorEngine CreateDesignTime(Action configure) - => CreateCore(new RazorConfiguration(RazorLanguageVersion.Latest, designTime: true), configure); + public static RazorEngine CreateDesignTime(Action configure) => CreateCore(RazorConfiguration.DefaultDesignTime, configure); // Internal since RazorEngine APIs are going to be obsolete. internal static RazorEngine CreateCore(RazorConfiguration configuration, Action configure) diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorExtension.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorExtension.cs new file mode 100644 index 0000000000..feb94b0ede --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorExtension.cs @@ -0,0 +1,10 @@ +// 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.AspNetCore.Razor.Language +{ + public abstract class RazorExtension + { + public abstract string ExtensionName { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorLanguageVersion.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorLanguageVersion.cs index 721c84ff00..c200290878 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorLanguageVersion.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorLanguageVersion.cs @@ -2,10 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; namespace Microsoft.AspNetCore.Razor.Language { - public sealed class RazorLanguageVersion : IEquatable + [DebuggerDisplay("{" + nameof(DebuggerToString) + "(),nq}")] + public sealed class RazorLanguageVersion : IEquatable, IComparable { public static readonly RazorLanguageVersion Version_1_0 = new RazorLanguageVersion(1, 0); @@ -17,6 +19,60 @@ namespace Microsoft.AspNetCore.Razor.Language public static readonly RazorLanguageVersion Latest = Version_2_1; + public static bool TryParse(string languageVersion, out RazorLanguageVersion version) + { + if (languageVersion == null) + { + throw new ArgumentNullException(nameof(languageVersion)); + } + + if (string.Equals(languageVersion, "latest", StringComparison.OrdinalIgnoreCase)) + { + version = Version_2_1; + return true; + } + else if (languageVersion == "2.1") + { + version = Version_2_1; + return true; + } + else if (languageVersion == "2.0") + { + version = Version_2_0; + return true; + } + else if (languageVersion == "1.1") + { + version = Version_1_1; + return true; + } + else if (languageVersion == "1.0") + { + version = Version_1_0; + return true; + } + + version = null; + return false; + } + + public static RazorLanguageVersion Parse(string languageVersion) + { + if (languageVersion == null) + { + throw new ArgumentNullException(nameof(languageVersion)); + } + + if (TryParse(languageVersion, out var parsed)) + { + return parsed; + } + + throw new ArgumentException( + Resources.FormatRazorLanguageVersion_InvalidVersion(languageVersion), + nameof(languageVersion)); + } + // Don't want anyone else constructing language versions. private RazorLanguageVersion(int major, int minor) { @@ -28,6 +84,22 @@ namespace Microsoft.AspNetCore.Razor.Language public int Minor { get; } + public int CompareTo(RazorLanguageVersion other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + var result = Major.CompareTo(other.Major); + if (result != 0) + { + return result; + } + + return Minor.CompareTo(other.Minor); + } + public bool Equals(RazorLanguageVersion other) { if (other == null) @@ -44,7 +116,9 @@ namespace Microsoft.AspNetCore.Razor.Language // We don't need to do anything special for our hash code since reference equality is what we're going for. return base.GetHashCode(); } + + public override string ToString() => $"{Major}.{Minor}"; - public override string ToString() => $"Razor '{Major}.{Minor}'"; + private string DebuggerToString() => $"Razor '{Major}.{Minor}'"; } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs index 19b1d54e80..0629eb9af8 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs @@ -9,8 +9,9 @@ namespace Microsoft.AspNetCore.Razor.Language { var allowMinimizedBooleanTagHelperAttributes = false; - if (version == RazorLanguageVersion.Version_2_1) + if (version.CompareTo(RazorLanguageVersion.Version_2_1) >= 0) { + // Added in 2.1 allowMinimizedBooleanTagHelperAttributes = true; } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx index d281042046..6413f1038b 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx +++ b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx @@ -533,4 +533,7 @@ Instead, wrap the contents of the block in "{{}}": The '{0}' is missing feature '{1}'. + + The Razor language version '{0}' is unrecognized or not supported by this version of Razor. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorConfigurationNameAttribute.cs b/src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorConfigurationNameAttribute.cs new file mode 100644 index 0000000000..034e64c309 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorConfigurationNameAttribute.cs @@ -0,0 +1,38 @@ +// 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.Razor.Hosting +{ + /// + /// Specifies the name of a Razor configuration as defined by the Razor SDK. + /// + /// + /// This attribute is applied to an application's entry point assembly by the Razor SDK during the build, + /// so that the Razor configuration can be loaded at runtime based on the settings provided by the project + /// file. + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] + public sealed class RazorConfigurationNameAttribute : Attribute + { + /// + /// Creates a new instance of . + /// + /// The name of the Razor configuration. + public RazorConfigurationNameAttribute(string configurationName) + { + if (configurationName == null) + { + throw new ArgumentNullException(nameof(configurationName)); + } + + ConfigurationName = configurationName; + } + + /// + /// Gets the name of the Razor configuration. + /// + public string ConfigurationName { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorExtensionAssemblyNameAttribute.cs b/src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorExtensionAssemblyNameAttribute.cs new file mode 100644 index 0000000000..92a9d1c6ec --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorExtensionAssemblyNameAttribute.cs @@ -0,0 +1,50 @@ +// 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.Razor.Hosting +{ + /// + /// Specifies the name of a Razor extension as defined by the Razor SDK. + /// + /// + /// This attribute is applied to an application's entry point assembly by the Razor SDK during the build, + /// so that the Razor configuration can be loaded at runtime based on the settings provided by the project + /// file. + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] + public sealed class RazorExtensionAssemblyNameAttribute : Attribute + { + /// + /// Creates a new instance of . + /// + /// The name of the extension. + /// The assembly name of the extension. + public RazorExtensionAssemblyNameAttribute(string extensionName, string assemblyName) + { + if (extensionName == null) + { + throw new ArgumentNullException(nameof(extensionName)); + } + + if (assemblyName == null) + { + throw new ArgumentNullException(nameof(assemblyName)); + } + + ExtensionName = extensionName; + AssemblyName = assemblyName; + } + + /// + /// Gets the assembly name of the extension. + /// + public string AssemblyName { get; } + + /// + /// Gets the name of the extension. + /// + public string ExtensionName { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorLanguageVersionAttribute.cs b/src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorLanguageVersionAttribute.cs new file mode 100644 index 0000000000..8f261143d0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Runtime/Hosting/RazorLanguageVersionAttribute.cs @@ -0,0 +1,38 @@ +// 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.Razor.Hosting +{ + /// + /// Specifies the name of a Razor configuration as defined by the Razor SDK. + /// + /// + /// This attribute is part of a set of metadata attributes that can be applied to an assembly at build + /// time by the Razor SDK. These attributes allow the Razor configuration to be loaded at runtime based + /// on the settings originally provided by the project file. + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] + public sealed class RazorLanguageVersionAttribute : Attribute + { + /// + /// Creates a new instance of . + /// + /// The language version of Razor + public RazorLanguageVersionAttribute(string languageVersion) + { + if (languageVersion == null) + { + throw new ArgumentNullException(nameof(languageVersion)); + } + + LanguageVersion = languageVersion; + } + + /// + /// Gets the Razor language version. + /// + public string LanguageVersion { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets index a081f46aaf..99109a78d4 100644 --- a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets +++ b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets @@ -161,6 +161,46 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + + <_ResolvedRazorConfiguration Include="@(RazorConfiguration)" Condition="'%(RazorConfiguration.Identity)'=='$(RazorDefaultConfiguration)'" /> + <_ResolvedRazorExtensionName Include="%(_ResolvedRazorConfiguration.Extensions)"/> + + + + + + + + + <_Parameter1>$(RazorLangVersion) + + + <_Parameter1>$(RazorDefaultConfiguration) + + + <_Parameter1>%(_ResolvedRazorExtension.Identity) + <_Parameter2>%(_ResolvedRazorExtension.AssemblyName) + + + + + + + + + + diff --git a/test/testapps/SimplePages/SimplePages.csproj b/test/testapps/SimplePages/SimplePages.csproj index 4525726119..12865d018c 100644 --- a/test/testapps/SimplePages/SimplePages.csproj +++ b/test/testapps/SimplePages/SimplePages.csproj @@ -1,10 +1,14 @@ - + + + + <_RazorMSBuildRoot>$(SolutionRoot)src\Microsoft.AspNetCore.Razor.Design\bin\$(Configuration)\netstandard2.0\ + netcoreapp2.0 @@ -15,4 +19,9 @@ + + + + +