From 771a7e35a41bc5d884fa5a71dfbffe13f8adf36b Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Mon, 22 Jan 2018 18:10:21 -0800 Subject: [PATCH] Add MVC support for RazorProjectEngine. - Make `RazorProjectEngine` call paths for all feature registrations. - Add `DefaultMvcImportFeature` for latest and 1.X MVC. - Ported `AddTargetExtension` and `AddDirective` to `RazorProjectEngineBuilderExtensions`. - Added tests and a test file system project type. - Moved obsolete `IRazorEngineBuilder` methods to the bottom of each file. Will actually obsolete the methods once `RazorProjectEngine` is working end-to-end. #1828 --- .../DefaultMvcImportFeature.cs | 84 ++++++++++++++++++ .../InjectDirective.cs | 22 ++++- .../ModelDirective.cs | 21 ++++- .../RazorExtensions.cs | 66 ++++++++++++++ .../DefaultMvcImportFeature.cs | 86 +++++++++++++++++++ .../InjectDirective.cs | 22 ++++- .../ModelDirective.cs | 21 ++++- .../NamespaceDirective.cs | 15 +++- .../PageDirective.cs | 20 ++++- .../RazorExtensions.cs | 45 ++++++++++ .../DefaultRazorImportFeature.cs | 4 +- .../Extensions/FunctionsDirective.cs | 21 ++++- .../Extensions/InheritsDirective.cs | 21 ++++- .../Extensions/SectionDirective.cs | 22 ++++- .../RazorProjectEngineBuilderExtensions.cs | 73 ++++++++++++++++ .../RazorProjectEngineFeatureBase.cs | 31 +++++++ .../DefaultMvcImportFeatureTest.cs | 77 +++++++++++++++++ .../DefaultMvcImportFeatureTest.cs | 77 +++++++++++++++++ ...RazorProjectEngineBuilderExtensionsTest.cs | 73 ++++++++++++++++ .../RazorProjectEngineFeatureBaseTest.cs | 34 ++++++++ .../Language}/TestRazorProjectFileSystem.cs | 0 21 files changed, 823 insertions(+), 12 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/DefaultMvcImportFeature.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor.Extensions/DefaultMvcImportFeature.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineFeatureBase.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/DefaultMvcImportFeatureTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/DefaultMvcImportFeatureTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineFeatureBaseTest.cs rename test/{Microsoft.AspNetCore.Razor.Language.Test => Microsoft.AspNetCore.Razor.Test.Common/Language}/TestRazorProjectFileSystem.cs (100%) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/DefaultMvcImportFeature.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/DefaultMvcImportFeature.cs new file mode 100644 index 0000000000..d630a70c72 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/DefaultMvcImportFeature.cs @@ -0,0 +1,84 @@ +// 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.IO; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X +{ + internal class DefaultMvcImportFeature : RazorProjectEngineFeatureBase, IRazorImportFeature + { + private const string ImportsFileName = "_ViewImports.cshtml"; + + public IReadOnlyList GetImports(string sourceFilePath) + { + if (string.IsNullOrEmpty(sourceFilePath)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpy, nameof(sourceFilePath)); + } + + var imports = new List(); + AddDefaultDirectivesImport(imports); + + // We add hierarchical imports second so any default directive imports can be overridden. + AddHierarchicalImports(sourceFilePath, imports); + + return imports; + } + + // Internal for testing + internal static void AddDefaultDirectivesImport(List imports) + { + using (var stream = new MemoryStream()) + using (var writer = new StreamWriter(stream, Encoding.UTF8)) + { + writer.WriteLine("@using System"); + writer.WriteLine("@using System.Collections.Generic"); + writer.WriteLine("@using System.Linq"); + writer.WriteLine("@using System.Threading.Tasks"); + writer.WriteLine("@using Microsoft.AspNetCore.Mvc"); + writer.WriteLine("@using Microsoft.AspNetCore.Mvc.Rendering"); + writer.WriteLine("@using Microsoft.AspNetCore.Mvc.ViewFeatures"); + writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html"); + writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json"); + writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component"); + writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IUrlHelper Url"); + writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider"); + writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor"); + writer.Flush(); + + stream.Position = 0; + var defaultMvcImports = RazorSourceDocument.ReadFrom(stream, fileName: null, encoding: Encoding.UTF8); + imports.Add(defaultMvcImports); + } + } + + // Internal for testing + internal void AddHierarchicalImports(string sourceFilePath, List imports) + { + // We want items in descending order. FindHierarchicalItems returns items in ascending order. + var importProjectItems = ProjectEngine.FileSystem.FindHierarchicalItems(sourceFilePath, ImportsFileName).Reverse(); + foreach (var importProjectItem in importProjectItems) + { + RazorSourceDocument importSourceDocument; + + if (importProjectItem.Exists) + { + importSourceDocument = RazorSourceDocument.ReadFrom(importProjectItem); + } + else + { + // File doesn't exist on disk so just add a marker source document as an identifier for "there could be something here". + var sourceDocumentProperties = new RazorSourceDocumentProperties(importProjectItem.FilePath, importProjectItem.RelativePhysicalPath); + importSourceDocument = RazorSourceDocument.Create(string.Empty, sourceDocumentProperties); + } + + imports.Add(importSourceDocument); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/InjectDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/InjectDirective.cs index c5e84d3ede..c1c348f8a9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/InjectDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/InjectDirective.cs @@ -24,8 +24,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X builder.Description = Resources.InjectDirective_Description; }); - public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder) { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.AddDirective(Directive); builder.Features.Add(new Pass()); builder.AddTargetExtension(new InjectTargetExtension()); @@ -99,5 +104,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X } } } + + #region Obsolete + public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new Pass()); + builder.AddTargetExtension(new InjectTargetExtension()); + return builder; + } + #endregion } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ModelDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ModelDirective.cs index 572d1b9ccb..2b67228fae 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ModelDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/ModelDirective.cs @@ -21,8 +21,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X builder.Description = Resources.ModelDirective_Description; }); - public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder) { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.AddDirective(Directive); builder.Features.Add(new Pass(builder.DesignTime)); return builder; @@ -128,5 +133,19 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X } } } + + #region Obsolete + public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new Pass(builder.DesignTime)); + return builder; + } + #endregion } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs index 341bb5d47a..c9b0642ad5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs @@ -9,8 +9,68 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X { public static class RazorExtensions { + public static void Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + EnsureDesignTime(builder); + + InjectDirective.Register(builder); + ModelDirective.Register(builder); + + FunctionsDirective.Register(builder); + InheritsDirective.Register(builder); + + // Register section directive with the 1.x compatible target extension. + builder.AddDirective(SectionDirective.Directive); + builder.Features.Add(new SectionDirectivePass()); + builder.AddTargetExtension(new LegacySectionTargetExtension()); + + builder.AddTargetExtension(new TemplateTargetExtension() + { + TemplateTypeName = "global::Microsoft.AspNetCore.Mvc.Razor.HelperResult", + }); + + builder.Features.Add(new ModelExpressionPass()); + builder.Features.Add(new MvcViewDocumentClassifierPass()); + + builder.SetImportFeature(new DefaultMvcImportFeature()); + } + + public static void RegisterViewComponentTagHelpers(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + EnsureDesignTime(builder); + + builder.Features.Add(new ViewComponentTagHelperPass()); + builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); + } + + private static void EnsureDesignTime(RazorProjectEngineBuilder builder) + { + if (builder.DesignTime) + { + return; + } + + throw new NotSupportedException(Resources.RuntimeCodeGenerationNotSupported); + } + + #region Obsolete public static void Register(IRazorEngineBuilder builder) { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + EnsureDesignTime(builder); InjectDirective.Register(builder); @@ -35,6 +95,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X public static void RegisterViewComponentTagHelpers(IRazorEngineBuilder builder) { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + EnsureDesignTime(builder); builder.Features.Add(new ViewComponentTagHelperPass()); @@ -50,5 +115,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X throw new NotSupportedException(Resources.RuntimeCodeGenerationNotSupported); } + #endregion } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/DefaultMvcImportFeature.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/DefaultMvcImportFeature.cs new file mode 100644 index 0000000000..8cbf0f11a1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/DefaultMvcImportFeature.cs @@ -0,0 +1,86 @@ +// 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.IO; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions +{ + internal class DefaultMvcImportFeature : RazorProjectEngineFeatureBase, IRazorImportFeature + { + private const string ImportsFileName = "_ViewImports.cshtml"; + + public IReadOnlyList GetImports(string sourceFilePath) + { + if (string.IsNullOrEmpty(sourceFilePath)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpy, nameof(sourceFilePath)); + } + + var imports = new List(); + AddDefaultDirectivesImport(imports); + + // We add hierarchical imports second so any default directive imports can be overridden. + AddHierarchicalImports(sourceFilePath, imports); + + return imports; + } + + // Internal for testing + internal static void AddDefaultDirectivesImport(List imports) + { + using (var stream = new MemoryStream()) + using (var writer = new StreamWriter(stream, Encoding.UTF8)) + { + writer.WriteLine("@using System"); + writer.WriteLine("@using System.Collections.Generic"); + writer.WriteLine("@using System.Linq"); + writer.WriteLine("@using System.Threading.Tasks"); + writer.WriteLine("@using Microsoft.AspNetCore.Mvc"); + writer.WriteLine("@using Microsoft.AspNetCore.Mvc.Rendering"); + writer.WriteLine("@using Microsoft.AspNetCore.Mvc.ViewFeatures"); + writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html"); + writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json"); + writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component"); + writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IUrlHelper Url"); + writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider"); + writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor"); + writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.HeadTagHelper, Microsoft.AspNetCore.Mvc.Razor"); + writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper, Microsoft.AspNetCore.Mvc.Razor"); + writer.Flush(); + + stream.Position = 0; + var defaultMvcImports = RazorSourceDocument.ReadFrom(stream, fileName: null, encoding: Encoding.UTF8); + imports.Add(defaultMvcImports); + } + } + + // Internal for testing + internal void AddHierarchicalImports(string sourceFilePath, List imports) + { + // We want items in descending order. FindHierarchicalItems returns items in ascending order. + var importProjectItems = ProjectEngine.FileSystem.FindHierarchicalItems(sourceFilePath, ImportsFileName).Reverse(); + foreach (var importProjectItem in importProjectItems) + { + RazorSourceDocument importSourceDocument; + + if (importProjectItem.Exists) + { + importSourceDocument = RazorSourceDocument.ReadFrom(importProjectItem); + } + else + { + // File doesn't exist on disk so just add a marker source document as an identifier for "there could be something here". + var sourceDocumentProperties = new RazorSourceDocumentProperties(importProjectItem.FilePath, importProjectItem.RelativePhysicalPath); + importSourceDocument = RazorSourceDocument.Create(string.Empty, sourceDocumentProperties); + } + + imports.Add(importSourceDocument); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs index 81f1d5b834..41bb56b57d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs @@ -24,8 +24,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions builder.Description = Resources.InjectDirective_Description; }); - public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder) { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.AddDirective(Directive); builder.Features.Add(new Pass()); builder.AddTargetExtension(new InjectTargetExtension()); @@ -99,5 +104,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions } } } + + #region Obsolete + public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new Pass()); + builder.AddTargetExtension(new InjectTargetExtension()); + return builder; + } + #endregion } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs index 17d98d72b4..a5d77b401e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs @@ -21,8 +21,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions builder.Description = Resources.ModelDirective_Description; }); - public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder) { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.AddDirective(Directive); builder.Features.Add(new Pass(builder.DesignTime)); return builder; @@ -135,5 +140,19 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions } } } + + #region Obsolete + public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new Pass(builder.DesignTime)); + return builder; + } + #endregion } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs index dec8c7894c..b0c197f42b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions builder.Description = Resources.NamespaceDirective_Description; }); - public static void Register(IRazorEngineBuilder builder) + public static void Register(RazorProjectEngineBuilder builder) { if (builder == null) { @@ -186,5 +186,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions base.VisitDirective(node); } } + + #region Obsolete + public static void Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(); + } + + builder.AddDirective(Directive); + builder.Features.Add(new Pass()); + } + #endregion } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/PageDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/PageDirective.cs index 5f617a1ebd..cd3624f435 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/PageDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/PageDirective.cs @@ -32,8 +32,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public IntermediateNode DirectiveNode { get; } - public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder) { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.AddDirective(Directive); return builder; } @@ -98,5 +103,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions } } } + + #region Obsolete + public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + return builder; + } + #endregion } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs index fc1ad0c162..19aa2ced3c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Extensions; @@ -8,8 +9,51 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { public static class RazorExtensions { + public static void Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + InjectDirective.Register(builder); + ModelDirective.Register(builder); + NamespaceDirective.Register(builder); + PageDirective.Register(builder); + + FunctionsDirective.Register(builder); + InheritsDirective.Register(builder); + SectionDirective.Register(builder); + + builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); + builder.AddTargetExtension(new TemplateTargetExtension() + { + TemplateTypeName = "global::Microsoft.AspNetCore.Mvc.Razor.HelperResult", + }); + + builder.Features.Add(new ModelExpressionPass()); + builder.Features.Add(new PagesPropertyInjectionPass()); + builder.Features.Add(new ViewComponentTagHelperPass()); + builder.Features.Add(new RazorPageDocumentClassifierPass()); + builder.Features.Add(new MvcViewDocumentClassifierPass()); + + if (!builder.DesignTime) + { + builder.Features.Add(new AssemblyAttributeInjectionPass()); + builder.Features.Add(new InstrumentationPass()); + } + + builder.SetImportFeature(new DefaultMvcImportFeature()); + } + + #region Obsolete public static void Register(IRazorEngineBuilder builder) { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + InjectDirective.Register(builder); ModelDirective.Register(builder); NamespaceDirective.Register(builder); @@ -37,5 +81,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions builder.Features.Add(new InstrumentationPass()); } } + #endregion } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorImportFeature.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorImportFeature.cs index ed25827c62..3d5c015a26 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorImportFeature.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorImportFeature.cs @@ -6,10 +6,8 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Razor.Language { - internal class DefaultRazorImportFeature : IRazorImportFeature + internal class DefaultRazorImportFeature : RazorProjectEngineFeatureBase, IRazorImportFeature { - public RazorProjectEngine ProjectEngine { get; set; } - public IReadOnlyList GetImports(string sourceFilePath) => Array.Empty(); } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/FunctionsDirective.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/FunctionsDirective.cs index f9033b8a83..0adb20aefb 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/FunctionsDirective.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/FunctionsDirective.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.Razor.Language.Legacy; namespace Microsoft.AspNetCore.Razor.Language.Extensions @@ -15,10 +16,28 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions builder.Description = Resources.FunctionsDirective_Description; }); - public static void Register(IRazorEngineBuilder builder) + public static void Register(RazorProjectEngineBuilder builder) { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.AddDirective(Directive); builder.Features.Add(new FunctionsDirectivePass()); } + + #region Obsolete + public static void Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new FunctionsDirectivePass()); + } + #endregion } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/InheritsDirective.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/InheritsDirective.cs index 289b5d853c..6cef321c73 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/InheritsDirective.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/InheritsDirective.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.Razor.Language.Legacy; namespace Microsoft.AspNetCore.Razor.Language.Extensions @@ -17,10 +18,28 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions builder.Description = Resources.InheritsDirective_Description; }); - public static void Register(IRazorEngineBuilder builder) + public static void Register(RazorProjectEngineBuilder builder) { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.AddDirective(Directive); builder.Features.Add(new InheritsDirectivePass()); } + + #region Obsolete + public static void Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new InheritsDirectivePass()); + } + #endregion } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/SectionDirective.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/SectionDirective.cs index 1542525a70..14c461d08f 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/SectionDirective.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/SectionDirective.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.Razor.Language.Legacy; namespace Microsoft.AspNetCore.Razor.Language.Extensions @@ -16,11 +17,30 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions builder.Description = Resources.SectionDirective_Description; }); - public static void Register(IRazorEngineBuilder builder) + public static void Register(RazorProjectEngineBuilder builder) { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.AddDirective(Directive); builder.Features.Add(new SectionDirectivePass()); builder.AddTargetExtension(new SectionTargetExtension()); } + + #region Obsolete + public static void Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new SectionDirectivePass()); + builder.AddTargetExtension(new SectionTargetExtension()); + } + #endregion } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilderExtensions.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilderExtensions.cs index 7001e288a5..8bd17e0dd6 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineBuilderExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; namespace Microsoft.AspNetCore.Razor.Language { @@ -29,5 +30,77 @@ namespace Microsoft.AspNetCore.Razor.Language builder.Features.Add(feature); } + + /// + /// Adds the specified . + /// + /// The . + /// The to add. + /// The . + public static RazorProjectEngineBuilder AddTargetExtension(this RazorProjectEngineBuilder builder, ICodeTargetExtension extension) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (extension == null) + { + throw new ArgumentNullException(nameof(extension)); + } + + var targetExtensionFeature = GetTargetExtensionFeature(builder); + targetExtensionFeature.TargetExtensions.Add(extension); + + return builder; + } + + /// + /// Adds the specified . + /// + /// The . + /// The to add. + /// The . + public static RazorProjectEngineBuilder AddDirective(this RazorProjectEngineBuilder builder, DirectiveDescriptor directive) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (directive == null) + { + throw new ArgumentNullException(nameof(directive)); + } + + var directiveFeature = GetDirectiveFeature(builder); + directiveFeature.Directives.Add(directive); + + return builder; + } + + private static IRazorDirectiveFeature GetDirectiveFeature(RazorProjectEngineBuilder builder) + { + var directiveFeature = builder.Features.OfType().FirstOrDefault(); + if (directiveFeature == null) + { + directiveFeature = new DefaultRazorDirectiveFeature(); + builder.Features.Add(directiveFeature); + } + + return directiveFeature; + } + + private static IRazorTargetExtensionFeature GetTargetExtensionFeature(RazorProjectEngineBuilder builder) + { + var targetExtensionFeature = builder.Features.OfType().FirstOrDefault(); + if (targetExtensionFeature == null) + { + targetExtensionFeature = new DefaultRazorTargetExtensionFeature(); + builder.Features.Add(targetExtensionFeature); + } + + return targetExtensionFeature; + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineFeatureBase.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineFeatureBase.cs new file mode 100644 index 0000000000..f5944a83ff --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngineFeatureBase.cs @@ -0,0 +1,31 @@ +// 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.Language +{ + public abstract class RazorProjectEngineFeatureBase : IRazorProjectEngineFeature + { + private RazorProjectEngine _projectEngine; + + public RazorProjectEngine ProjectEngine + { + get => _projectEngine; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _projectEngine = value; + OnInitialized(); + } + } + + protected virtual void OnInitialized() + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/DefaultMvcImportFeatureTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/DefaultMvcImportFeatureTest.cs new file mode 100644 index 0000000000..32e254a45b --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/DefaultMvcImportFeatureTest.cs @@ -0,0 +1,77 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions +{ + public class DefaultMvcImportFeatureTest + { + [Fact] + public void AddDefaultDirectivesImport_AddsSingleDynamicImport() + { + // Arrange + var imports = new List(); + + // Act + DefaultMvcImportFeature.AddDefaultDirectivesImport(imports); + + // Assert + var import = Assert.Single(imports); + Assert.Null(import.FilePath); + } + + [Fact] + public void AddHierarchicalImports_AddsViewImportSourceDocumentsOnDisk() + { + // Arrange + var imports = new List(); + var testFileSystem = new TestRazorProjectFileSystem(new[] + { + new TestRazorProjectItem("/Index.cshtml"), + new TestRazorProjectItem("/_ViewImports.cshtml"), + new TestRazorProjectItem("/Contact/_ViewImports.cshtml"), + new TestRazorProjectItem("/Contact/Index.cshtml"), + }); + var mvcImportFeature = new DefaultMvcImportFeature() + { + ProjectEngine = Mock.Of(projectEngine => projectEngine.FileSystem == testFileSystem) + }; + + // Act + mvcImportFeature.AddHierarchicalImports("/Contact/Index.cshtml", imports); + + // Assert + Assert.Collection(imports, + import => Assert.Equal("/_ViewImports.cshtml", import.FilePath), + import => Assert.Equal("/Contact/_ViewImports.cshtml", import.FilePath)); + } + + [Fact] + public void AddHierarchicalImports_AddsViewImportSourceDocumentsNotOnDisk() + { + // Arrange + var imports = new List(); + var testFileSystem = new TestRazorProjectFileSystem(new[] + { + new TestRazorProjectItem("/Pages/Contact/Index.cshtml"), + }); + var mvcImportFeature = new DefaultMvcImportFeature() + { + ProjectEngine = Mock.Of(projectEngine => projectEngine.FileSystem == testFileSystem) + }; + + // Act + mvcImportFeature.AddHierarchicalImports("/Pages/Contact/Index.cshtml", imports); + + // Assert + Assert.Collection(imports, + import => Assert.Equal("/_ViewImports.cshtml", import.FilePath), + import => Assert.Equal("/Pages/_ViewImports.cshtml", import.FilePath), + import => Assert.Equal("/Pages/Contact/_ViewImports.cshtml", import.FilePath)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/DefaultMvcImportFeatureTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/DefaultMvcImportFeatureTest.cs new file mode 100644 index 0000000000..31d66517a1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test/DefaultMvcImportFeatureTest.cs @@ -0,0 +1,77 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X +{ + public class DefaultMvcImportFeatureTest + { + [Fact] + public void AddDefaultDirectivesImport_AddsSingleDynamicImport() + { + // Arrange + var imports = new List(); + + // Act + DefaultMvcImportFeature.AddDefaultDirectivesImport(imports); + + // Assert + var import = Assert.Single(imports); + Assert.Null(import.FilePath); + } + + [Fact] + public void AddHierarchicalImports_AddsViewImportSourceDocumentsOnDisk() + { + // Arrange + var imports = new List(); + var testFileSystem = new TestRazorProjectFileSystem(new[] + { + new TestRazorProjectItem("/Index.cshtml"), + new TestRazorProjectItem("/_ViewImports.cshtml"), + new TestRazorProjectItem("/Contact/_ViewImports.cshtml"), + new TestRazorProjectItem("/Contact/Index.cshtml"), + }); + var mvcImportFeature = new DefaultMvcImportFeature() + { + ProjectEngine = Mock.Of(projectEngine => projectEngine.FileSystem == testFileSystem) + }; + + // Act + mvcImportFeature.AddHierarchicalImports("/Contact/Index.cshtml", imports); + + // Assert + Assert.Collection(imports, + import => Assert.Equal("/_ViewImports.cshtml", import.FilePath), + import => Assert.Equal("/Contact/_ViewImports.cshtml", import.FilePath)); + } + + [Fact] + public void AddHierarchicalImports_AddsViewImportSourceDocumentsNotOnDisk() + { + // Arrange + var imports = new List(); + var testFileSystem = new TestRazorProjectFileSystem(new[] + { + new TestRazorProjectItem("/Pages/Contact/Index.cshtml"), + }); + var mvcImportFeature = new DefaultMvcImportFeature() + { + ProjectEngine = Mock.Of(projectEngine => projectEngine.FileSystem == testFileSystem) + }; + + // Act + mvcImportFeature.AddHierarchicalImports("/Pages/Contact/Index.cshtml", imports); + + // Assert + Assert.Collection(imports, + import => Assert.Equal("/_ViewImports.cshtml", import.FilePath), + import => Assert.Equal("/Pages/_ViewImports.cshtml", import.FilePath), + import => Assert.Equal("/Pages/Contact/_ViewImports.cshtml", import.FilePath)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineBuilderExtensionsTest.cs index 44f93a970a..d9c0630005 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineBuilderExtensionsTest.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.Razor.Language.CodeGeneration; using Moq; using Xunit; @@ -26,5 +27,77 @@ namespace Microsoft.AspNetCore.Razor.Language var feature = Assert.Single(builder.Features); Assert.Same(newFeature, feature); } + + [Fact] + public void AddTargetExtension_CreatesAndAddsToTargetExtensionFeatureIfItDoesNotExist() + { + // Arrange + var builder = new DefaultRazorProjectEngineBuilder(false, Mock.Of()); + var expectedExtension = Mock.Of(); + + // Act + builder.AddTargetExtension(expectedExtension); + + // Assert + var feature = Assert.Single(builder.Features); + var codeTargetExtensionFeature = Assert.IsAssignableFrom(feature); + var extensions = Assert.Single(codeTargetExtensionFeature.TargetExtensions); + Assert.Same(expectedExtension, extensions); + } + + [Fact] + public void AddTargetExtension_UsesExistingFeatureIfExistsAndAddsTo() + { + // Arrange + var builder = new DefaultRazorProjectEngineBuilder(false, Mock.Of()); + var codeTargetExtensionFeature = new DefaultRazorTargetExtensionFeature(); + builder.Features.Add(codeTargetExtensionFeature); + var expectedExtension = Mock.Of(); + + // Act + builder.AddTargetExtension(expectedExtension); + + // Assert + var feature = Assert.Single(builder.Features); + Assert.Same(codeTargetExtensionFeature, feature); + var extensions = Assert.Single(codeTargetExtensionFeature.TargetExtensions); + Assert.Same(expectedExtension, extensions); + } + + [Fact] + public void AddDirective_CreatesAndAddsToDirectiveFeatureIfItDoesNotExist() + { + // Arrange + var builder = new DefaultRazorProjectEngineBuilder(false, Mock.Of()); + var expectedDirective = Mock.Of(); + + // Act + builder.AddDirective(expectedDirective); + + // Assert + var feature = Assert.Single(builder.Features); + var directiveFeature = Assert.IsAssignableFrom(feature); + var directive = Assert.Single(directiveFeature.Directives); + Assert.Same(expectedDirective, directive); + } + + [Fact] + public void AddDirective_UsesExistingFeatureIfExistsAndAddsTo() + { + // Arrange + var builder = new DefaultRazorProjectEngineBuilder(false, Mock.Of()); + var directiveFeature = new DefaultRazorDirectiveFeature(); + builder.Features.Add(directiveFeature); + var expecteDirective = Mock.Of(); + + // Act + builder.AddDirective(expecteDirective); + + // Assert + var feature = Assert.Single(builder.Features); + Assert.Same(directiveFeature, feature); + var directive = Assert.Single(directiveFeature.Directives); + Assert.Same(expecteDirective, directive); + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineFeatureBaseTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineFeatureBaseTest.cs new file mode 100644 index 0000000000..8344e787c7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorProjectEngineFeatureBaseTest.cs @@ -0,0 +1,34 @@ +// 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 Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public class RazorProjectEngineFeatureBaseTest + { + [Fact] + public void ProjectEngineSetter_CallsOnInitialized() + { + // Arrange + var testFeature = new TestFeature(); + + // Act + testFeature.ProjectEngine = Mock.Of(); + + // Assert + Assert.Equal(1, testFeature.InitializationCount); + } + + private class TestFeature : RazorProjectEngineFeatureBase + { + public int InitializationCount { get; private set; } + + protected override void OnInitialized() + { + InitializationCount++; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestRazorProjectFileSystem.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs similarity index 100% rename from test/Microsoft.AspNetCore.Razor.Language.Test/TestRazorProjectFileSystem.cs rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs