From 870f023aa9a58bd018786fcae70111b9b6e2199b 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. (cherry picked from commit 5b28c06d6453696e98fb7bf32abe3aef09a5c26b) --- .../Properties/Resources.Designer.cs | 14 ++++ .../RazorConfiguration.cs | 39 +++++++++- .../RazorEngine.cs | 2 +- .../RazorExtension.cs | 10 +++ .../RazorLanguageVersion.cs | 78 ++++++++++++++++++- .../RazorParserFeatureFlags.cs | 3 +- .../Resources.resx | 3 + .../RazorConfigurationNameAttribute.cs | 38 +++++++++ .../RazorExtensionAssemblyNameAttribute.cs | 50 ++++++++++++ .../Hosting/RazorLanguageVersionAttribute.cs | 38 +++++++++ .../DefaultTemplateEngineFactoryService.cs | 2 +- 11 files changed, 269 insertions(+), 8 deletions(-) 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 diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs index adb2a92453..1f95e0176c 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs @@ -822,6 +822,20 @@ namespace Microsoft.AspNetCore.Razor.Language internal static string FormatSectionDirective_NameToken_Name() => GetString("SectionDirective_NameToken_Name"); + /// + /// 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 32420721ee..0f00751497 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs @@ -2,25 +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 DefaultRuntime = new RazorConfiguration(RazorLanguageVersion.Latest, designTime: false); - public static readonly RazorConfiguration DefaultDesignTime = new RazorConfiguration(RazorLanguageVersion.Latest, designTime: true); + 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 0fe5e14d19..aa06b9f9ab 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorEngine.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Razor.Language return Create(configure: null); } - public static RazorEngine Create(Action configure) => CreateCore(RazorConfiguration.DefaultRuntime, configure); + public static RazorEngine Create(Action configure) => CreateCore(RazorConfiguration.Default, configure); public static RazorEngine CreateDesignTime() { 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 54df909c15..35b511066d 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx +++ b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx @@ -291,4 +291,7 @@ SectionName + + 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.VisualStudio.Editor.Razor/DefaultTemplateEngineFactoryService.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTemplateEngineFactoryService.cs index a82d1b8e94..ab5518bb46 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTemplateEngineFactoryService.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTemplateEngineFactoryService.cs @@ -43,7 +43,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var project = FindProject(projectPath); var configuration = (project?.Configuration as MvcExtensibilityConfiguration) ?? DefaultConfiguration; var razorLanguageVersion = configuration.LanguageVersion; - var razorConfiguration = new RazorConfiguration(razorLanguageVersion, designTime: true); + var razorConfiguration = new RazorConfiguration(razorLanguageVersion, "unnamed", Array.Empty(), designTime: true); RazorEngine engine; if (razorLanguageVersion.Major == 1)