diff --git a/RazorLight.sln b/RazorLight.sln
index 5db857b3..682a8107 100644
--- a/RazorLight.sln
+++ b/RazorLight.sln
@@ -1,88 +1,95 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.29609.76
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{25F57564-58FD-4FD2-8CDA-98261E5BEEEC}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{CDB1D407-71F7-44D6-8B35-F0013D1717A6}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorLight", "src\RazorLight\RazorLight.csproj", "{CA780663-2714-43B0-89BC-BEB26BC5405A}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sandbox", "sandbox", "{D8281EA5-1C64-4C9F-9537-967D685C1918}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorLight.Sandbox", "sandbox\RazorLight.Sandbox\RazorLight.Sandbox.csproj", "{DC81E072-5571-4E4A-A7EC-4122BF4A012E}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorLight.Tests", "tests\RazorLight.Tests\RazorLight.Tests.csproj", "{63EE1F3F-C71E-48DA-8135-9E602D6B7206}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{61D04B29-43F8-4FE3-A12E-BB16743BFB65}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.EntityFrameworkProject", "samples\RazorLight.Samples\Samples.EntityFrameworkProject.csproj", "{C9B26DF6-F8E6-481A-B497-C8999641A99D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorLight.Precompile", "src\RazorLight.Precompile\RazorLight.Precompile.csproj", "{AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{634B4E56-928A-4132-AB3F-F2DD43756350}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- LICENSE = LICENSE
- README.md = README.md
- README.source.md = README.source.md
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{DF8F9896-63C0-43AC-8834-1D13AA095928}"
- ProjectSection(SolutionItems) = preProject
- Directory.Build.props = Directory.Build.props
- .github\workflows\dotnet.yml = .github\workflows\dotnet.yml
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Publish", "Publish", "{03C519A7-C5B1-4421-893B-D2995D4A92BC}"
- ProjectSection(SolutionItems) = preProject
- makeNuget.cmd = makeNuget.cmd
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EC25D85F-CCCC-43E1-AACD-2D710C1B760B}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- EndProjectSection
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {CA780663-2714-43B0-89BC-BEB26BC5405A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CA780663-2714-43B0-89BC-BEB26BC5405A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CA780663-2714-43B0-89BC-BEB26BC5405A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CA780663-2714-43B0-89BC-BEB26BC5405A}.Release|Any CPU.Build.0 = Release|Any CPU
- {DC81E072-5571-4E4A-A7EC-4122BF4A012E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DC81E072-5571-4E4A-A7EC-4122BF4A012E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DC81E072-5571-4E4A-A7EC-4122BF4A012E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DC81E072-5571-4E4A-A7EC-4122BF4A012E}.Release|Any CPU.Build.0 = Release|Any CPU
- {63EE1F3F-C71E-48DA-8135-9E602D6B7206}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {63EE1F3F-C71E-48DA-8135-9E602D6B7206}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {63EE1F3F-C71E-48DA-8135-9E602D6B7206}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {63EE1F3F-C71E-48DA-8135-9E602D6B7206}.Release|Any CPU.Build.0 = Release|Any CPU
- {C9B26DF6-F8E6-481A-B497-C8999641A99D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C9B26DF6-F8E6-481A-B497-C8999641A99D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C9B26DF6-F8E6-481A-B497-C8999641A99D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C9B26DF6-F8E6-481A-B497-C8999641A99D}.Release|Any CPU.Build.0 = Release|Any CPU
- {AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {CA780663-2714-43B0-89BC-BEB26BC5405A} = {25F57564-58FD-4FD2-8CDA-98261E5BEEEC}
- {DC81E072-5571-4E4A-A7EC-4122BF4A012E} = {D8281EA5-1C64-4C9F-9537-967D685C1918}
- {63EE1F3F-C71E-48DA-8135-9E602D6B7206} = {CDB1D407-71F7-44D6-8B35-F0013D1717A6}
- {C9B26DF6-F8E6-481A-B497-C8999641A99D} = {61D04B29-43F8-4FE3-A12E-BB16743BFB65}
- {AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76} = {25F57564-58FD-4FD2-8CDA-98261E5BEEEC}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {590C5541-E5A7-41ED-AFC7-D4A52E594191}
- EndGlobalSection
-EndGlobal
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32616.157
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{25F57564-58FD-4FD2-8CDA-98261E5BEEEC}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{CDB1D407-71F7-44D6-8B35-F0013D1717A6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorLight", "src\RazorLight\RazorLight.csproj", "{CA780663-2714-43B0-89BC-BEB26BC5405A}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sandbox", "sandbox", "{D8281EA5-1C64-4C9F-9537-967D685C1918}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorLight.Sandbox", "sandbox\RazorLight.Sandbox\RazorLight.Sandbox.csproj", "{DC81E072-5571-4E4A-A7EC-4122BF4A012E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorLight.Tests", "tests\RazorLight.Tests\RazorLight.Tests.csproj", "{63EE1F3F-C71E-48DA-8135-9E602D6B7206}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{61D04B29-43F8-4FE3-A12E-BB16743BFB65}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.EntityFrameworkProject", "samples\RazorLight.Samples\Samples.EntityFrameworkProject.csproj", "{C9B26DF6-F8E6-481A-B497-C8999641A99D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorLight.Precompile", "src\RazorLight.Precompile\RazorLight.Precompile.csproj", "{AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{634B4E56-928A-4132-AB3F-F2DD43756350}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ LICENSE = LICENSE
+ README.md = README.md
+ README.source.md = README.source.md
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{DF8F9896-63C0-43AC-8834-1D13AA095928}"
+ ProjectSection(SolutionItems) = preProject
+ Directory.Build.props = Directory.Build.props
+ .github\workflows\dotnet.yml = .github\workflows\dotnet.yml
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Publish", "Publish", "{03C519A7-C5B1-4421-893B-D2995D4A92BC}"
+ ProjectSection(SolutionItems) = preProject
+ makeNuget.cmd = makeNuget.cmd
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EC25D85F-CCCC-43E1-AACD-2D710C1B760B}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RazorLight.Precompile.Tests", "tests\RazorLight.Precompile.Tests\RazorLight.Precompile.Tests.csproj", "{C9BA26CD-C20E-493C-A55A-C5498778184B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {CA780663-2714-43B0-89BC-BEB26BC5405A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CA780663-2714-43B0-89BC-BEB26BC5405A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CA780663-2714-43B0-89BC-BEB26BC5405A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CA780663-2714-43B0-89BC-BEB26BC5405A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DC81E072-5571-4E4A-A7EC-4122BF4A012E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DC81E072-5571-4E4A-A7EC-4122BF4A012E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DC81E072-5571-4E4A-A7EC-4122BF4A012E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DC81E072-5571-4E4A-A7EC-4122BF4A012E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {63EE1F3F-C71E-48DA-8135-9E602D6B7206}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {63EE1F3F-C71E-48DA-8135-9E602D6B7206}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {63EE1F3F-C71E-48DA-8135-9E602D6B7206}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {63EE1F3F-C71E-48DA-8135-9E602D6B7206}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C9B26DF6-F8E6-481A-B497-C8999641A99D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C9B26DF6-F8E6-481A-B497-C8999641A99D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C9B26DF6-F8E6-481A-B497-C8999641A99D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C9B26DF6-F8E6-481A-B497-C8999641A99D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C9BA26CD-C20E-493C-A55A-C5498778184B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C9BA26CD-C20E-493C-A55A-C5498778184B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C9BA26CD-C20E-493C-A55A-C5498778184B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C9BA26CD-C20E-493C-A55A-C5498778184B}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {CA780663-2714-43B0-89BC-BEB26BC5405A} = {25F57564-58FD-4FD2-8CDA-98261E5BEEEC}
+ {DC81E072-5571-4E4A-A7EC-4122BF4A012E} = {D8281EA5-1C64-4C9F-9537-967D685C1918}
+ {63EE1F3F-C71E-48DA-8135-9E602D6B7206} = {CDB1D407-71F7-44D6-8B35-F0013D1717A6}
+ {C9B26DF6-F8E6-481A-B497-C8999641A99D} = {61D04B29-43F8-4FE3-A12E-BB16743BFB65}
+ {AD9A02F6-8078-4DFF-AFA6-4DE8674D5C76} = {25F57564-58FD-4FD2-8CDA-98261E5BEEEC}
+ {C9BA26CD-C20E-493C-A55A-C5498778184B} = {CDB1D407-71F7-44D6-8B35-F0013D1717A6}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {590C5541-E5A7-41ED-AFC7-D4A52E594191}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index ea13c86e..add16e6a 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,7 +1,7 @@
- 2.1.0
+ 2.1.1
diff --git a/src/RazorLight.Precompile/AssemblyMetadataGenerator.cs b/src/RazorLight.Precompile/AssemblyMetadataGenerator.cs
deleted file mode 100644
index e767cc20..00000000
--- a/src/RazorLight.Precompile/AssemblyMetadataGenerator.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.Text;
-using RazorLight.Compilation;
-using System.Reflection;
-
-namespace RazorLight.Precompile
-{
- internal static class AssemblyMetadataGenerator
- {
- public static CSharpCompilation AddAssemblyMetadata(
- RoslynCompilationService compiler,
- CSharpCompilation compilation,
- CompilationOptions compilationOptions)
- {
- var applicationAssemblyName = Assembly.Load(new AssemblyName(compilationOptions.ApplicationName)).GetName();
- var assemblyVersionContent = $"[assembly:{typeof(AssemblyVersionAttribute).FullName}(\"{applicationAssemblyName.Version}\")]";
- var syntaxTree = compiler.CreateSyntaxTree(SourceText.From(assemblyVersionContent));
- return compilation.AddSyntaxTrees(syntaxTree);
- }
- }
-}
diff --git a/src/RazorLight.Precompile/CompilationOptions.cs b/src/RazorLight.Precompile/CompilationOptions.cs
deleted file mode 100644
index 0d198fb7..00000000
--- a/src/RazorLight.Precompile/CompilationOptions.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using Microsoft.Extensions.CommandLineUtils;
-
-namespace RazorLight.Precompile
-{
- internal class CompilationOptions
- {
- public static readonly string ContentRootTemplate = "--content-root";
- public static readonly string ApplicationNameTemplate = "--application-name";
- public static readonly string OutputPathTemplate = "--output-path";
- public static readonly string Extension = "--extension";
-
- public CompilationOptions(CommandLineApplication app)
- {
- OutputPathOption = app.Option(
- OutputPathTemplate,
- "Path to the emit the precompiled assembly to.",
- CommandOptionType.SingleValue);
-
- ApplicationNameOption = app.Option(
- ApplicationNameTemplate,
- "Name of the application to produce precompiled assembly for.",
- CommandOptionType.SingleValue);
-
- ProjectArgument = app.Argument(
- "project",
- "The path to the project file.");
-
- ContentRootOption = app.Option(
- ContentRootTemplate,
- "The application's content root.",
- CommandOptionType.SingleValue);
-
- TemplatesExtension = app.Option(
- Extension,
- "Templates extension",
- CommandOptionType.SingleValue);
- }
-
- public CommandArgument ProjectArgument { get; }
-
- public CommandOption ContentRootOption { get; }
-
- public CommandOption OutputPathOption { get; }
-
- public CommandOption ApplicationNameOption { get; }
-
- public CommandOption TemplatesExtension { get; }
-
- public string OutputPath => OutputPathOption.Value();
-
- public string ApplicationName => ApplicationNameOption.Value();
- }
-}
diff --git a/src/RazorLight.Precompile/JsonModel.cs b/src/RazorLight.Precompile/JsonModel.cs
new file mode 100644
index 00000000..43c4f834
--- /dev/null
+++ b/src/RazorLight.Precompile/JsonModel.cs
@@ -0,0 +1,40 @@
+using Newtonsoft.Json.Linq;
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Linq;
+
+namespace RazorLight.Precompile
+{
+ public class JsonModel : DynamicObject
+ {
+ private readonly Dictionary m_properties = new();
+
+ public static object New(JToken jsonToken)
+ {
+ return jsonToken switch
+ {
+ JObject o => new JsonModel(o),
+ JArray a => a.Select(New).ToList(),
+ JValue v => v.Value,
+ _ => jsonToken,
+ };
+ }
+
+ public JsonModel(JObject o)
+ {
+ foreach (var (key, value) in o)
+ {
+ m_properties[key] = New(value);
+ }
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ if (!m_properties.TryGetValue(binder.Name, out result))
+ {
+ result = null;
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/RazorLight.Precompile/PrecompilationApplication.cs b/src/RazorLight.Precompile/PrecompilationApplication.cs
deleted file mode 100644
index 61dcc211..00000000
--- a/src/RazorLight.Precompile/PrecompilationApplication.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using Microsoft.Extensions.CommandLineUtils;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Reflection;
-
-namespace RazorLight.Precompile
-{
- internal class PrecompilationApplication : CommandLineApplication
- {
- private Type _callingType;
-
- public PrecompilationApplication(Type callingType)
- {
- _callingType = callingType;
-
- Name = "razorlight-precompile";
- FullName = "RazorLight Precompilation Utility";
- Description = "Precompiles RazorLight templates.";
- ShortVersionGetter = GetInformationalVersion;
-
- HelpOption("-?|-h|--help");
-
- OnExecute(() =>
- {
- ShowHelp();
- return 2;
- });
- }
-
- public new int Execute(params string[] args)
- {
- try
- {
- return base.Execute(ExpandResponseFiles(args));
- }
- catch (AggregateException ex) when (ex.InnerException != null)
- {
- Error.WriteLine(ex.InnerException.Message);
- Error.WriteLine(ex.InnerException.StackTrace);
- return 1;
- }
- catch (Exception ex)
- {
- Error.WriteLine(ex.Message);
- Error.WriteLine(ex.StackTrace);
- return 1;
- }
- }
-
- private static string[] ExpandResponseFiles(string[] args)
- {
- var expandedArgs = new List();
- foreach (var arg in args)
- {
- if (!arg.StartsWith("@", StringComparison.Ordinal))
- {
- expandedArgs.Add(arg);
- }
- else
- {
- var fileName = arg.Substring(1);
- expandedArgs.AddRange(File.ReadLines(fileName));
- }
- }
-
- return expandedArgs.ToArray();
- }
-
- private string GetInformationalVersion()
- {
- var assembly = _callingType.GetTypeInfo().Assembly;
- var attribute = assembly.GetCustomAttribute();
- return attribute.InformationalVersion;
- }
- }
-}
diff --git a/src/RazorLight.Precompile/PrecompileCmd.cs b/src/RazorLight.Precompile/PrecompileCmd.cs
new file mode 100644
index 00000000..7eb4a822
--- /dev/null
+++ b/src/RazorLight.Precompile/PrecompileCmd.cs
@@ -0,0 +1,129 @@
+using ManyConsole;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using RazorLight.Caching;
+using System.Collections.Generic;
+using System.IO;
+
+namespace RazorLight.Precompile
+{
+ public class PrecompileCmd : ConsoleCommand
+ {
+ private enum StrategyName
+ {
+ Simple,
+ FileHash
+ }
+
+ private static readonly Dictionary s_strategyMap = new()
+ {
+ [StrategyName.Simple] = SimpleFileCachingStrategy.Instance,
+ [StrategyName.FileHash] = FileHashCachingStrategy.Instance
+ };
+
+ private string m_templateFile;
+ private string m_cacheDir;
+ private string m_baseDir;
+ private StrategyName m_strategyName = StrategyName.FileHash;
+ private string m_modelFilePath;
+ private string m_jsonQuery;
+
+ public PrecompileCmd()
+ {
+ IsCommand("precompile", "Precompiles the given razor template.");
+
+ HasRequiredOption("t|template=", "The path to a razor template. " +
+ "A relative path is based off the current directory or the base directory, if the latter is given.", v => m_templateFile = v);
+ HasOption("c|cache=", "The cache directory where precompiled assemblies are stored. Will be created, if does not exist. " +
+ "Defaults to the directory containing the template files.", v => m_cacheDir = v);
+ HasOption("b|base=", "The razor template base directory. Defaults to the home directory of the given template. " +
+ "If given and the template file path is relative, then it is relative to this base directory.", v => m_baseDir = v);
+ HasOption("s|strategy=", "The file system caching strategy. The default strategy is " + m_strategyName, (StrategyName v) => m_strategyName = v);
+ HasOption("m|model=", "The path to a JSON file representing the model object to be rendered against the given template.", v => m_modelFilePath = v);
+ HasOption("q|jsonQuery=", "Renders the first item returned by the given JSON query.", v => m_jsonQuery = v);
+
+ HasLongDescription(
+ "Precompiles the given razor template into the given cache directory. " +
+ "When the --model argument is given the command also renders the given model and outputs the result to the stdout. " +
+ "Otherwise the command outputs the path to the precompiled assembly.");
+ }
+
+ public override int? OverrideAfterHandlingArgumentsBeforeRun(string[] remainingArguments)
+ {
+ if (remainingArguments.Length > 0)
+ {
+ throw new ConsoleHelpAsException("Unrecognized command line arguments - " + string.Join(' ', remainingArguments));
+ }
+ if (!s_strategyMap.ContainsKey(m_strategyName))
+ {
+ throw new ConsoleHelpAsException("Unsupported strategy " + m_strategyName);
+ }
+ return base.OverrideAfterHandlingArgumentsBeforeRun(remainingArguments);
+ }
+
+ public override int Run(string[] remainingArguments)
+ {
+ string templateKey;
+ if (m_baseDir == null)
+ {
+ m_templateFile = Path.GetFullPath(m_templateFile);
+ m_baseDir = Path.GetDirectoryName(m_templateFile);
+ templateKey = Path.GetFileName(m_templateFile);
+ }
+ else
+ {
+ if (!Directory.Exists(m_baseDir))
+ {
+ throw new RazorLightException($"The razor template base directory {m_baseDir} does not exist.");
+ }
+ m_baseDir = Path.GetFullPath(m_baseDir);
+ if (Path.IsPathRooted(m_templateFile))
+ {
+ templateKey = Path.GetRelativePath(m_baseDir, m_templateFile);
+ }
+ else
+ {
+ templateKey = m_templateFile;
+ m_templateFile = Path.GetFullPath(Path.Combine(m_baseDir, m_templateFile));
+ }
+ }
+
+ if (!File.Exists(m_templateFile))
+ {
+ throw new RazorLightException($"The razor template file {m_templateFile} does not exist.");
+ }
+
+ if (m_cacheDir == null)
+ {
+ m_cacheDir = Path.GetDirectoryName(m_templateFile);
+ }
+ else if (!Directory.Exists(m_cacheDir))
+ {
+ Directory.CreateDirectory(m_cacheDir);
+ }
+
+ var provider = new FileSystemCachingProvider(m_baseDir, m_cacheDir, s_strategyMap[m_strategyName]);
+ var engine = new RazorLightEngineBuilder()
+ .UseFileSystemProject(m_baseDir, "")
+ .UseCachingProvider(provider)
+ .Build();
+
+ if (m_modelFilePath == null)
+ {
+ engine.CompileTemplateAsync(templateKey).GetAwaiter().GetResult();
+ Program.ConsoleOut.WriteLine(provider.GetAssemblyFilePath(templateKey, m_templateFile));
+ }
+ else
+ {
+ var o = JsonConvert.DeserializeObject(File.ReadAllText(m_modelFilePath));
+ if (m_jsonQuery != null)
+ {
+ o = o.SelectToken(m_jsonQuery);
+ }
+ var model = JsonModel.New(o);
+ Program.ConsoleOut.WriteLine(engine.CompileRenderAsync(templateKey, model).GetAwaiter().GetResult());
+ }
+ return 0;
+ }
+ }
+}
diff --git a/src/RazorLight.Precompile/PrecompileRunCommand.cs b/src/RazorLight.Precompile/PrecompileRunCommand.cs
deleted file mode 100644
index d656773c..00000000
--- a/src/RazorLight.Precompile/PrecompileRunCommand.cs
+++ /dev/null
@@ -1,231 +0,0 @@
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.Emit;
-using Microsoft.CodeAnalysis.Text;
-using Microsoft.Extensions.CommandLineUtils;
-using RazorLight.Compilation;
-using RazorLight.Internal;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace RazorLight.Precompile
-{
- public class PrecompileRunCommand
- {
- private static readonly ParallelOptions ParallelOptions = new ParallelOptions
- {
- MaxDegreeOfParallelism = 4
- };
-
- private TemplateFactoryProvider factoryProvider;
- private RoslynCompilationService compiler;
-
- private CommandLineApplication Application { get; set; }
- private CompilationOptions Options { get; set; }
- private string Extension { get; set; }
-
- public void Configure(CommandLineApplication app)
- {
- Application = app;
- Options = new CompilationOptions(app);
-
- app.OnExecute(() => Execute());
- }
-
- private int Execute()
- {
- // if (!ParseArguments())
- // {
- // return 1;
- // }
-
- //var engine = new RazorLightEngineBuilder()
- // .UseFileSystemProject(Options.ContentRootOption.Value())
- // .Build();
-
- // factoryProvider = (TemplateFactoryProvider)engine.TemplateFactoryProvider;
- //compiler = engine.TemplateCompiler;
- // ViewCompilationInfo[] results = GenerateCode();
- // bool success = true;
-
- // foreach (var result in results)
- // {
- // if (result.CSharpDocument.Diagnostics.Count > 0)
- // {
- // success = false;
- // foreach (var error in result.CSharpDocument.Diagnostics)
- // {
- // Application.Error.WriteLine($"{result.TemplateFileInfo.FullPath} ({error.Span.LineIndex}): {error.GetMessage()}");
- // }
- // }
- // }
-
- // if (!success)
- // {
- // return 1;
- // }
-
- // string precompileAssemblyName = $"{Options.ApplicationName}_Precompiled";
- // CSharpCompilation compilation = CompileViews(results, precompileAssemblyName);
-
- // string assemblyPath = Path.Combine(Options.OutputPath, precompileAssemblyName + ".dll");
- // EmitResult emitResult = EmitAssembly(
- // compilation,
- // compiler.EmitOptions,
- // assemblyPath);
-
- // if (!emitResult.Success)
- // {
- // foreach (var diagnostic in emitResult.Diagnostics)
- // {
- // Application.Error.WriteLine(CSharpDiagnosticFormatter.Instance.Format(diagnostic));
- // }
-
- // return 1;
- // }
-
- return 0;
- }
-
- public EmitResult EmitAssembly(
- CSharpCompilation compilation,
- EmitOptions emitOptions,
- string assemblyPath)
- {
- EmitResult emitResult;
- using (var assemblyStream = new MemoryStream())
- {
- using (var pdbStream = new MemoryStream())
- {
- emitResult = compilation.Emit(
- assemblyStream,
- pdbStream,
- options: emitOptions);
-
- if (emitResult.Success)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(assemblyPath));
- var pdbPath = Path.ChangeExtension(assemblyPath, ".pdb");
- assemblyStream.Position = 0;
- pdbStream.Position = 0;
-
- // Avoid writing to disk unless the compilation is successful.
- using (var assemblyFileStream = File.OpenWrite(assemblyPath))
- {
- assemblyStream.CopyTo(assemblyFileStream);
- }
-
- using (var pdbFileStream = File.OpenWrite(pdbPath))
- {
- pdbStream.CopyTo(pdbFileStream);
- }
- }
- }
- }
-
- return emitResult;
- }
-
- private CSharpCompilation CompileViews(ViewCompilationInfo[] results, string assemblyName)
- {
- var compilation = compiler.CreateCompilation(assemblyName);
- var syntaxTrees = new SyntaxTree[results.Length];
-
- Parallel.For(0, results.Length, ParallelOptions, i =>
- {
- ViewCompilationInfo result = results[i];
- SourceText sourceText = SourceText.From(result.CSharpDocument.GeneratedCode, Encoding.UTF8);
-
- TemplateFileInfo fileInfo = result.TemplateFileInfo;
- SyntaxTree syntaxTree = compiler.CreateSyntaxTree(sourceText).WithFilePath(fileInfo.FullPath ?? fileInfo.ViewEnginePath);
- syntaxTrees[i] = syntaxTree;
- });
-
- compilation = compilation.AddSyntaxTrees(syntaxTrees);
- compilation = ExpressionRewriter.Rewrite(compilation);
-
- compilation = AssemblyMetadataGenerator.AddAssemblyMetadata(
- compiler,
- compilation,
- Options);
-
- return compilation;
- }
-
- private ViewCompilationInfo[] GenerateCode()
- {
- var files = GetFiles();
- var results = new ViewCompilationInfo[files.Count];
- //TODO: finish
- //Parallel.For(0, results.Length, ParallelOptions, i =>
- //{
- // TemplateFileInfo fileInfo = files[i];
- // ViewCompilationInfo compilationInfo;
- // using (var fileStream = fileInfo.CreateReadStream())
- // {
- // var razorTemplate = factoryProvider.SourceGenerator.GenerateCodeAsync(fileInfo.ViewEnginePath).Result;
- // compilationInfo = new ViewCompilationInfo(fileInfo, razorTemplate.CSharpDocument);
- // }
-
- // results[i] = compilationInfo;
- //});
-
- return results;
- }
-
- private List GetFiles()
- {
- string contentRoot = Options.ContentRootOption.Value();
- int trimLength = contentRoot.EndsWith("/") ? contentRoot.Length - 1 : contentRoot.Length;
-
- var files = new List();
- foreach (string file in Directory.EnumerateFiles(contentRoot, "*", SearchOption.AllDirectories))
- {
- if (file.EndsWith(Extension))
- {
- var viewEnginePath = file.Substring(trimLength).Replace('\\', '/');
- files.Add(new TemplateFileInfo(file, viewEnginePath));
- }
- }
-
- return files;
- }
-
- private bool ParseArguments()
- {
- Extension = Options.TemplatesExtension.Value();
- if (string.IsNullOrEmpty(Extension))
- {
- Extension = ".cshtml";
- }
-
- //if (!Options.ProjectArgument))
- //{
- // Application.Error.WriteLine("Project path not specified.");
- // return false;
- //}
-
- if (!Options.OutputPathOption.HasValue())
- {
- Application.Error.WriteLine($"Option {CompilationOptions.OutputPathTemplate} does not specify a value.");
- return false;
- }
-
- if (!Options.ApplicationNameOption.HasValue())
- {
- Application.Error.WriteLine($"Option {CompilationOptions.ApplicationNameTemplate} does not specify a value.");
- return false;
- }
-
- if (!Options.ContentRootOption.HasValue())
- {
- Application.Error.WriteLine($"Option {CompilationOptions.ContentRootTemplate} does not specify a value.");
- return false;
- }
-
- return true;
- }
- }
-}
diff --git a/src/RazorLight.Precompile/PrecompiledCachingProvider.cs b/src/RazorLight.Precompile/PrecompiledCachingProvider.cs
new file mode 100644
index 00000000..368dce89
--- /dev/null
+++ b/src/RazorLight.Precompile/PrecompiledCachingProvider.cs
@@ -0,0 +1,90 @@
+using Microsoft.Extensions.Primitives;
+using Mono.Cecil;
+using RazorLight.Caching;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace RazorLight.Precompile
+{
+ public class PrecompiledCachingProvider : ICachingProvider
+ {
+ public readonly IReadOnlyDictionary Map;
+ private readonly MemoryCachingProvider m_cache = new();
+
+ public PrecompiledCachingProvider(IEnumerable precompiledTemplateFilePaths, StreamWriter log)
+ {
+ var map = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
+ foreach (var (templateKey, filePath) in precompiledTemplateFilePaths
+ .Select(filePath => (templateKey: GetPrecompiledTemplateKey(filePath, log), filePath))
+ .Where(o => o.templateKey != null))
+ {
+ if (map.TryGetValue(templateKey, out var dupe))
+ {
+ throw new RazorLightException($"The key {templateKey} is associated with at least two precompiled templates - {dupe} and {filePath}");
+ }
+ map.Add(templateKey, filePath);
+ }
+ if (map.Count == 0)
+ {
+ throw new RazorLightException($"Found no precompiled templates.");
+ }
+ Map = map;
+ }
+
+ private static string GetPrecompiledTemplateKey(string filePath, StreamWriter log)
+ {
+ try
+ {
+ using var asmDef = AssemblyDefinition.ReadAssembly(filePath);
+ var razorLightAttr = asmDef.CustomAttributes.SingleOrDefault(o => o.AttributeType.FullName == "RazorLight.Razor.RazorLightTemplateAttribute");
+ if (razorLightAttr != null)
+ {
+ string templateKey = (string)razorLightAttr.ConstructorArguments[0].Value;
+ log?.WriteLine("GetPrecompiledTemplateKey(\"{0}\") = \"{1}\"", filePath, templateKey);
+ return templateKey;
+ }
+ }
+ catch { }
+ log?.WriteLine("GetPrecompiledTemplateKey(\"{0}\") = null", filePath);
+ return null;
+ }
+
+ public void CacheTemplate(string key, Func pageFactory, IChangeToken expirationToken) => throw new NotImplementedException();
+
+ public bool Contains(string key) => Map.ContainsKey(key);
+
+ public void Remove(string key) => throw new NotImplementedException();
+
+ public TemplateCacheLookupResult RetrieveTemplate(string key)
+ {
+ key = key.Replace('\\', '/');
+ if (key[0] != '/')
+ {
+ key = '/' + key;
+ }
+
+ var res = m_cache.RetrieveTemplate(key);
+ if (res.Success)
+ {
+ return res;
+ }
+
+ if (Map.TryGetValue(key, out var filePath))
+ {
+ return new TemplateCacheLookupResult(new TemplateCacheItem(key, CreateTemplatePage));
+
+ ITemplatePage CreateTemplatePage()
+ {
+ var templatePageType = FileSystemCachingProvider.GetTemplatePageType(filePath);
+ m_cache.CacheTemplate(key, CreateTemplatePage2);
+ return CreateTemplatePage2();
+
+ ITemplatePage CreateTemplatePage2() => FileSystemCachingProvider.NewTemplatePage(templatePageType);
+ }
+ }
+ throw new RazorLightException($"No precompiled template found for the key {key}");
+ }
+ }
+}
diff --git a/src/RazorLight.Precompile/Program.cs b/src/RazorLight.Precompile/Program.cs
index 146d0c9b..b96fed03 100644
--- a/src/RazorLight.Precompile/Program.cs
+++ b/src/RazorLight.Precompile/Program.cs
@@ -1,16 +1,34 @@
-using System;
-
-namespace RazorLight.Precompile
-{
- class Program
- {
- private readonly static Type ProgramType = typeof(Program);
-
- static int Main(string[] args)
- {
- var app = new PrecompilationApplication(ProgramType);
- new PrecompileRunCommand().Configure(app);
- return app.Execute(args);
- }
- }
-}
+using ManyConsole;
+using System;
+using System.IO;
+
+namespace RazorLight.Precompile
+{
+ public class Program
+ {
+ public static TextWriter ConsoleOut { get; set; } = Console.Out;
+
+ public static int Main(string[] args)
+ {
+ try
+ {
+ return DoRun(args);
+ }
+ catch (Exception exc)
+ {
+ Console.Error.WriteLine(exc);
+ return 1;
+ }
+ }
+
+ public static int DoRun(string[] args)
+ {
+ var commands = ConsoleCommandDispatcher.FindCommandsInSameAssemblyAs(typeof(Program));
+ foreach (var c in commands)
+ {
+ c.SkipsCommandSummaryBeforeRunning();
+ }
+ return ConsoleCommandDispatcher.DispatchCommand(commands, args, Console.Out);
+ }
+ }
+}
diff --git a/src/RazorLight.Precompile/RazorLight.Precompile.args.json b/src/RazorLight.Precompile/RazorLight.Precompile.args.json
new file mode 100644
index 00000000..bd5e2f68
--- /dev/null
+++ b/src/RazorLight.Precompile/RazorLight.Precompile.args.json
@@ -0,0 +1,10 @@
+{
+ "FileVersion": 2,
+ "Id": "ad9a02f6-8078-4dff-afa6-4de8674d5c76",
+ "Items": [
+ {
+ "Id": "774f5eb8-c718-4f33-a70a-42f6eca8eae3",
+ "Command": "precompile -t .\\Samples\\FullMessage.cshtml -c .cache -m .\\Samples\\FindingsWithSourceCodeInfo.json -s Simple"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/RazorLight.Precompile/RazorLight.Precompile.csproj b/src/RazorLight.Precompile/RazorLight.Precompile.csproj
index 6714cc93..fe6ba5da 100644
--- a/src/RazorLight.Precompile/RazorLight.Precompile.csproj
+++ b/src/RazorLight.Precompile/RazorLight.Precompile.csproj
@@ -1,17 +1,21 @@
-
-
-
- Exe
- netcoreapp2.1;netcoreapp3.1;net5.0
- false
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Exe
+ net6.0
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/RazorLight.Precompile/RenderCmd.cs b/src/RazorLight.Precompile/RenderCmd.cs
new file mode 100644
index 00000000..3dfb0270
--- /dev/null
+++ b/src/RazorLight.Precompile/RenderCmd.cs
@@ -0,0 +1,126 @@
+using GlobExpressions;
+using ManyConsole;
+using Mono.Cecil;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using RazorLight.Caching;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace RazorLight.Precompile
+{
+ internal class RenderCmd : ConsoleCommand
+ {
+ private enum StrategyName
+ {
+ Simple,
+ FileHash
+ }
+
+ private static readonly Dictionary s_strategyMap = new()
+ {
+ [StrategyName.Simple] = SimpleFileCachingStrategy.Instance,
+ [StrategyName.FileHash] = FileHashCachingStrategy.Instance
+ };
+
+ private string m_path;
+ private string m_modelFilePath;
+ private string m_jsonQuery;
+ private SearchOption m_searchOption = SearchOption.TopDirectoryOnly;
+ private string m_key;
+ private string m_logFilePath;
+
+ public RenderCmd()
+ {
+ IsCommand("render", "Renders the given precompiled razor template.");
+
+ HasRequiredOption("p|path=", "A comma separated list of folders and/or files. Plain files must be dlls previously produced by the precompile command. " +
+ "The given folders are assumed to contain such dlls. By default the folders are scanned non recursively. Minimatch patterns are supported too.", v => m_path = v);
+ HasRequiredOption("m|model=", "The path to a JSON file representing the model object to be rendered against the given template.", v => m_modelFilePath = v);
+ HasOption("k|key=", "The key of the template to be used to render the given model. Only required when there are more than one precompiled template.", v => m_key = v);
+ HasOption("q|jsonQuery=", "Renders the first item returned by the given JSON query.", v => m_jsonQuery = v);
+ HasOption("r|recurse", "Instructs the tool to scan the given folders recursively.", _ => m_searchOption = SearchOption.AllDirectories);
+ HasOption("l|log=", "An optional log file path", v => m_logFilePath = v);
+ }
+
+ public override int? OverrideAfterHandlingArgumentsBeforeRun(string[] remainingArguments)
+ {
+ if (remainingArguments.Length > 0)
+ {
+ throw new ConsoleHelpAsException("Unrecognized command line arguments - " + string.Join(' ', remainingArguments));
+ }
+ return base.OverrideAfterHandlingArgumentsBeforeRun(remainingArguments);
+ }
+
+ public override int Run(string[] remainingArguments)
+ {
+ var o = JsonConvert.DeserializeObject(File.ReadAllText(m_modelFilePath));
+ if (m_jsonQuery != null)
+ {
+ o = o.SelectToken(m_jsonQuery);
+ }
+ var model = JsonModel.New(o);
+
+ using var log = m_logFilePath == null ? null : new StreamWriter(m_logFilePath);
+ var cachingProvider = new PrecompiledCachingProvider(YieldFiles(), log);
+
+ if (m_key == null)
+ {
+ if (cachingProvider.Map.Count > 1)
+ {
+ throw new RazorLightException($"Found {cachingProvider.Map.Count} precompiled templates and no --key argument was given.");
+ }
+ m_key = cachingProvider.Map.First().Key;
+ }
+ else if (m_key[0] != '/')
+ {
+ m_key = '/' + m_key;
+ }
+
+ var engine = new RazorLightEngineBuilder()
+ .UseCachingProvider(cachingProvider)
+ .Build();
+
+ var templatePage = cachingProvider.RetrieveTemplate(m_key).Template.TemplatePageFactory();
+ Program.ConsoleOut.WriteLine(engine.Handler.RenderTemplateAsync(templatePage, model).GetAwaiter().GetResult());
+ return 0;
+ }
+
+ private IEnumerable YieldFiles()
+ {
+ if (m_path.Contains(','))
+ {
+ return m_path.Split(',').SelectMany(DoYieldFiles);
+ }
+
+ return DoYieldFiles(m_path);
+
+ IEnumerable DoYieldFiles(string fileOrFolderPath)
+ {
+ if (fileOrFolderPath.Contains('*') || fileOrFolderPath.Contains('?') || fileOrFolderPath.Contains('['))
+ {
+ return Glob.Files(Directory.GetCurrentDirectory(), fileOrFolderPath);
+ }
+
+ if (File.Exists(fileOrFolderPath))
+ {
+ if (fileOrFolderPath.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase))
+ {
+ return new[] { fileOrFolderPath };
+ }
+
+ throw new RazorLightException($"{fileOrFolderPath} is not a valid precompiled template assembly.");
+ }
+
+ if (Directory.Exists(fileOrFolderPath))
+ {
+ return Directory.EnumerateFiles(fileOrFolderPath, "*.dll", m_searchOption);
+ }
+
+ throw new RazorLightException($"{fileOrFolderPath} is not found.");
+ }
+ }
+ }
+}
diff --git a/src/RazorLight.Precompile/TemplateFileInfo.cs b/src/RazorLight.Precompile/TemplateFileInfo.cs
deleted file mode 100644
index 122f0aa3..00000000
--- a/src/RazorLight.Precompile/TemplateFileInfo.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System.IO;
-
-namespace RazorLight.Precompile
-{
- internal class TemplateFileInfo
- {
- public TemplateFileInfo(string fullPath, string viewEnginePath)
- {
- FullPath = fullPath;
- ViewEnginePath = viewEnginePath;
- }
-
- public string FullPath { get; }
-
- public string ViewEnginePath { get; }
-
- public Stream CreateReadStream()
- {
- // We are setting buffer size to 1 to prevent FileStream from allocating it's internal buffer
- // 0 causes constructor to throw
- var bufferSize = 1;
- return new FileStream(
- FullPath,
- FileMode.Open,
- FileAccess.Read,
- FileShare.ReadWrite,
- bufferSize,
- FileOptions.Asynchronous | FileOptions.SequentialScan);
- }
- }
-}
diff --git a/src/RazorLight.Precompile/ViewCompilationInfo.cs b/src/RazorLight.Precompile/ViewCompilationInfo.cs
deleted file mode 100644
index 6ddd6026..00000000
--- a/src/RazorLight.Precompile/ViewCompilationInfo.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Microsoft.AspNetCore.Razor.Language;
-
-namespace RazorLight.Precompile
-{
- internal struct ViewCompilationInfo
- {
- public ViewCompilationInfo(
- TemplateFileInfo viewFileInfo,
- RazorCSharpDocument cSharpDocument)
- {
- TemplateFileInfo = viewFileInfo;
- CSharpDocument = cSharpDocument;
- }
-
- public TemplateFileInfo TemplateFileInfo { get; }
-
- public RazorCSharpDocument CSharpDocument { get; }
- }
-}
diff --git a/src/RazorLight/Caching/FileHashCachingStrategy.cs b/src/RazorLight/Caching/FileHashCachingStrategy.cs
new file mode 100644
index 00000000..42a2755d
--- /dev/null
+++ b/src/RazorLight/Caching/FileHashCachingStrategy.cs
@@ -0,0 +1,47 @@
+using RazorLight.Razor;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace RazorLight.Caching
+{
+ public class FileHashCachingStrategy : IFileSystemCachingStrategy
+ {
+ public static readonly IFileSystemCachingStrategy Instance = new FileHashCachingStrategy();
+
+ public string Name => "FileHash";
+
+ private static string GetFileHash(string key, string filePath)
+ {
+ using (var md5Hash = MD5.Create())
+ {
+ // Byte array representation of source string
+ var sourceBytes = File.ReadAllBytes(filePath);
+ var keyBytes = Encoding.UTF8.GetBytes(FileSystemRazorProjectHelper.NormalizeKey(key));
+
+ var finalBytes = new byte[sourceBytes.Length + keyBytes.Length];
+ sourceBytes.CopyTo(finalBytes, 0);
+ keyBytes.CopyTo(finalBytes, sourceBytes.Length);
+
+ // Generate hash value(Byte Array) for input data
+ var hashBytes = md5Hash.ComputeHash(finalBytes);
+
+ // Convert hash byte array to string
+ var sb = new StringBuilder(hashBytes.Length * 2);
+ foreach (byte v in hashBytes)
+ {
+ sb.Append(v.ToString("x2"));
+ }
+ return sb.ToString();
+ }
+ }
+
+ public CachedFileInfo GetCachedFileInfo(string key, string templateFilePath, string cacheDir)
+ {
+ var srcFileHash = GetFileHash(key, templateFilePath);
+ var asmFilePath = Path.Combine(cacheDir, srcFileHash + ".dll");
+ var pdbFilePath = Path.Combine(cacheDir, srcFileHash + ".pdb");
+ return new CachedFileInfo(File.Exists(asmFilePath), asmFilePath, pdbFilePath);
+ }
+ }
+}
diff --git a/src/RazorLight/Caching/FileSystemCachingProvider.cs b/src/RazorLight/Caching/FileSystemCachingProvider.cs
new file mode 100644
index 00000000..6767c09d
--- /dev/null
+++ b/src/RazorLight/Caching/FileSystemCachingProvider.cs
@@ -0,0 +1,97 @@
+using Microsoft.Extensions.Primitives;
+using RazorLight.Compilation;
+using RazorLight.Generation;
+using RazorLight.Razor;
+using System;
+using System.IO;
+using System.Reflection;
+
+namespace RazorLight.Caching
+{
+ public class FileSystemCachingProvider : ICachingProvider, IPrecompileCallback
+ {
+ private readonly MemoryCachingProvider m_cache = new MemoryCachingProvider();
+ private readonly string m_baseDir;
+ private readonly string m_cacheDir;
+ private readonly IFileSystemCachingStrategy m_fileSystemCachingStrategy;
+
+ public FileSystemCachingProvider(string baseDir, string cacheDir, IFileSystemCachingStrategy fileSystemCachingStrategy)
+ {
+ m_baseDir = baseDir;
+ m_cacheDir = cacheDir;
+ m_fileSystemCachingStrategy = fileSystemCachingStrategy;
+ }
+
+ public string GetAssemblyFilePath(string key, string templateFilePath) => m_fileSystemCachingStrategy.GetCachedFileInfo(key, templateFilePath, m_cacheDir).AssemblyFilePath;
+
+ void IPrecompileCallback.Invoke(IGeneratedRazorTemplate generatedRazorTemplate, byte[] rawAssembly, byte[] rawSymbolStore)
+ {
+ var srcFilePath = Path.Combine(m_baseDir, generatedRazorTemplate.TemplateKey.Substring(1));
+ var (_, asmFilePath, pdbFilePath) = m_fileSystemCachingStrategy.GetCachedFileInfo(generatedRazorTemplate.TemplateKey, srcFilePath, m_cacheDir);
+ Directory.CreateDirectory(Path.GetDirectoryName(asmFilePath));
+ File.WriteAllBytes(asmFilePath, rawAssembly);
+ if (rawSymbolStore != null)
+ {
+ File.WriteAllBytes(pdbFilePath, rawSymbolStore);
+ }
+ }
+
+ public void CacheTemplate(string key, Func pageFactory, IChangeToken expirationToken)
+ {
+ m_cache.CacheTemplate(key, pageFactory, expirationToken);
+ }
+
+ public bool Contains(string key) => m_fileSystemCachingStrategy.GetCachedFileInfo(key, Path.Combine(m_baseDir, key), m_cacheDir).UpToDate;
+
+ public void Remove(string key)
+ {
+ var srcFilePath = Path.Combine(m_baseDir, key);
+ var (_, asmFilePath, pdbFilePath) = m_fileSystemCachingStrategy.GetCachedFileInfo(key, srcFilePath, m_cacheDir);
+ File.Delete(asmFilePath);
+ File.Delete(pdbFilePath);
+ }
+
+ public TemplateCacheLookupResult RetrieveTemplate(string key)
+ {
+ var res = m_cache.RetrieveTemplate(key);
+ if (res.Success)
+ {
+ return res;
+ }
+
+ var srcFilePath = Path.Combine(m_baseDir, key);
+ var (upToDate, asmFilePath, pdbFilePath) = m_fileSystemCachingStrategy.GetCachedFileInfo(key, srcFilePath, m_cacheDir);
+ if (upToDate)
+ {
+ var rawAssembly = File.ReadAllBytes(asmFilePath);
+ var rawSymbolStore = File.Exists(pdbFilePath) ? File.ReadAllBytes(pdbFilePath) : null;
+ return new TemplateCacheLookupResult(new TemplateCacheItem(key, CreateTemplatePage));
+
+ ITemplatePage CreateTemplatePage()
+ {
+ var templatePageType = GetTemplatePageType(rawAssembly, rawSymbolStore);
+ m_cache.CacheTemplate(key, CreateTemplatePage2);
+ return CreateTemplatePage2();
+
+ ITemplatePage CreateTemplatePage2() => NewTemplatePage(templatePageType);
+ }
+ }
+ return new TemplateCacheLookupResult();
+ }
+
+ public static Type GetTemplatePageType(string asmFilePath)
+ {
+ var rawAssembly = File.ReadAllBytes(asmFilePath);
+ var pdbFilePath = asmFilePath.Replace(".dll", ".pdb");
+ var rawSymbolStore = File.Exists(pdbFilePath) ? File.ReadAllBytes(pdbFilePath) : null;
+ return GetTemplatePageType(rawAssembly, rawSymbolStore);
+ }
+
+ public static ITemplatePage NewTemplatePage(Type templatePageType) => (ITemplatePage)Activator.CreateInstance(templatePageType);
+
+ public static Type GetTemplatePageType(byte[] rawAssembly, byte[] rawSymbolStore) => Assembly
+ .Load(rawAssembly, rawSymbolStore)
+ .GetCustomAttribute()
+ .TemplateType;
+ }
+}
diff --git a/src/RazorLight/Caching/IFileSystemCachingStrategy.cs b/src/RazorLight/Caching/IFileSystemCachingStrategy.cs
new file mode 100644
index 00000000..c92b5449
--- /dev/null
+++ b/src/RazorLight/Caching/IFileSystemCachingStrategy.cs
@@ -0,0 +1,29 @@
+namespace RazorLight.Caching
+{
+ public struct CachedFileInfo
+ {
+ public readonly bool UpToDate;
+ public readonly string AssemblyFilePath;
+ public readonly string PdbFilePath;
+
+ public CachedFileInfo(bool upToDate, string assemblyFilePath, string pdbFilePath)
+ {
+ UpToDate = upToDate;
+ AssemblyFilePath = assemblyFilePath;
+ PdbFilePath = pdbFilePath;
+ }
+
+ public void Deconstruct(out bool upToDate, out string assemblyFilePath, out string pdbFilePath)
+ {
+ upToDate = UpToDate;
+ assemblyFilePath = AssemblyFilePath;
+ pdbFilePath = PdbFilePath;
+ }
+ }
+
+ public interface IFileSystemCachingStrategy
+ {
+ string Name { get; }
+ CachedFileInfo GetCachedFileInfo(string key, string templateFilePath, string cacheDir);
+ }
+}
diff --git a/src/RazorLight/Caching/SimpleFileCachingStrategy.cs b/src/RazorLight/Caching/SimpleFileCachingStrategy.cs
new file mode 100644
index 00000000..a646b955
--- /dev/null
+++ b/src/RazorLight/Caching/SimpleFileCachingStrategy.cs
@@ -0,0 +1,30 @@
+using System.IO;
+
+namespace RazorLight.Caching
+{
+ public class SimpleFileCachingStrategy : IFileSystemCachingStrategy
+ {
+ public static readonly IFileSystemCachingStrategy Instance = new SimpleFileCachingStrategy();
+
+ public string Name => "Simple";
+
+ public CachedFileInfo GetCachedFileInfo(string key, string templateFilePath, string cacheDir)
+ {
+ if (key[0] == '/' || key[0] == '\\')
+ {
+ key = key.Substring(1);
+ }
+
+ var asmFilePath = Path.Combine(cacheDir, key + ".dll");
+ var pdbFilePath = Path.Combine(cacheDir, key + ".pdb");
+ var upToDate = false;
+ if (File.Exists(asmFilePath))
+ {
+ var templateFileTime = File.GetLastWriteTimeUtc(templateFilePath);
+ var asmFileTime = File.GetLastWriteTimeUtc(asmFilePath);
+ upToDate = templateFileTime < asmFileTime;
+ }
+ return new CachedFileInfo(upToDate, asmFilePath, pdbFilePath);
+ }
+ }
+}
diff --git a/src/RazorLight/Compilation/IPrecompileCallback.cs b/src/RazorLight/Compilation/IPrecompileCallback.cs
new file mode 100644
index 00000000..ac9f6425
--- /dev/null
+++ b/src/RazorLight/Compilation/IPrecompileCallback.cs
@@ -0,0 +1,9 @@
+using RazorLight.Generation;
+
+namespace RazorLight.Compilation
+{
+ public interface IPrecompileCallback
+ {
+ void Invoke(IGeneratedRazorTemplate generatedRazorTemplate, byte[] rawAssembly, byte[] rawSymbolStore);
+ }
+}
diff --git a/src/RazorLight/Compilation/RazorTemplateCompiler.cs b/src/RazorLight/Compilation/RazorTemplateCompiler.cs
index a727bb41..6f2df1eb 100644
--- a/src/RazorLight/Compilation/RazorTemplateCompiler.cs
+++ b/src/RazorLight/Compilation/RazorTemplateCompiler.cs
@@ -150,7 +150,7 @@ private async Task OnCacheMissAsync(string templateK
taskSource.SetResult(item.Descriptor);
}
- _cache.Set(item.NormalizedKey, taskSource.Task, cacheEntryOptions);
+ _ = _cache.Set(item.NormalizedKey, taskSource.Task, cacheEntryOptions);
}
finally
{
@@ -255,52 +255,12 @@ internal string GetNormalizedKey(string templateKey)
if (_normalizedKeysCache.TryGetValue(templateKey, out var normalizedPath))
return normalizedPath;
- normalizedPath = NormalizeKey(templateKey);
+ normalizedPath = _razorProject.NormalizeKey(templateKey);
_normalizedKeysCache[templateKey] = normalizedPath;
return normalizedPath;
}
- protected string NormalizeKey(string templateKey)
- {
- if (!(_razorProject is FileSystemRazorProject))
- {
- return templateKey;
- }
-
- var addLeadingSlash = templateKey[0] != '\\' && templateKey[0] != '/';
- var transformSlashes = templateKey.IndexOf('\\') != -1;
-
- if (!addLeadingSlash && !transformSlashes)
- {
- return templateKey;
- }
-
- var length = templateKey.Length;
- if (addLeadingSlash)
- {
- length++;
- }
-
- var builder = new StringBuilder(length);
- if (addLeadingSlash)
- {
- builder.Append('/');
- }
-
- for (var i = 0; i < templateKey.Length; i++)
- {
- var ch = templateKey[i];
- if (ch == '\\')
- {
- ch = '/';
- }
- builder.Append(ch);
- }
-
- return builder.ToString();
- }
-
internal async Task CreateTemplateNotFoundException(RazorLightProjectItem projectItem)
{
var msg = $"{nameof(RazorLightProjectItem)} of type {projectItem.GetType().FullName} with key {projectItem.Key} could not be found by the " +
diff --git a/src/RazorLight/Compilation/RoslynCompilationService.cs b/src/RazorLight/Compilation/RoslynCompilationService.cs
index 47051bfc..2d9c1bf2 100644
--- a/src/RazorLight/Compilation/RoslynCompilationService.cs
+++ b/src/RazorLight/Compilation/RoslynCompilationService.cs
@@ -22,11 +22,13 @@ public class RoslynCompilationService : ICompilationService
private readonly IMetadataReferenceManager metadataReferenceManager;
private readonly bool isDevelopment;
private readonly List metadataReferences = new List();
+ private readonly IPrecompileCallback precompileCallback;
- public RoslynCompilationService(IMetadataReferenceManager referenceManager, Assembly operatingAssembly)
+ public RoslynCompilationService(IMetadataReferenceManager referenceManager, Assembly operatingAssembly, IPrecompileCallback precompileCallback = null)
{
this.metadataReferenceManager = referenceManager ?? throw new ArgumentNullException(nameof(referenceManager));
this.OperatingAssembly = operatingAssembly ?? throw new ArgumentNullException(nameof(operatingAssembly));
+ this.precompileCallback = precompileCallback;
isDevelopment = AssemblyDebugModeUtility.IsAssemblyDebugBuild(OperatingAssembly);
var pdbFormat = SymbolsUtility.SupportsFullPdbGeneration() ?
@@ -36,7 +38,8 @@ public RoslynCompilationService(IMetadataReferenceManager referenceManager, Asse
EmitOptions = new EmitOptions(debugInformationFormat: pdbFormat);
}
- public RoslynCompilationService(IMetadataReferenceManager referenceManager, IOptions options) :this(referenceManager, options.Value.OperatingAssembly)
+ public RoslynCompilationService(IMetadataReferenceManager referenceManager, IOptions options, IPrecompileCallback precompileCallback = null) :
+ this(referenceManager, options.Value.OperatingAssembly, precompileCallback)
{
}
@@ -136,9 +139,12 @@ public Assembly CompileAndEmit(IGeneratedRazorTemplate razorTemplate)
}
assemblyStream.Seek(0, SeekOrigin.Begin);
- pdbStream.Seek(0, SeekOrigin.Begin);
-
- var assembly = Assembly.Load(assemblyStream.ToArray(), pdbStream.ToArray());
+ pdbStream.Seek(0, SeekOrigin.Begin);
+
+ var rawAssembly = assemblyStream.ToArray();
+ var rawSymbolStore = pdbStream.ToArray();
+ precompileCallback?.Invoke(razorTemplate, rawAssembly, rawSymbolStore);
+ var assembly = Assembly.Load(rawAssembly, rawSymbolStore);
return assembly;
}
diff --git a/src/RazorLight/Razor/FileSystemRazorProject.cs b/src/RazorLight/Razor/FileSystemRazorProject.cs
index 350914ac..46e4a248 100644
--- a/src/RazorLight/Razor/FileSystemRazorProject.cs
+++ b/src/RazorLight/Razor/FileSystemRazorProject.cs
@@ -3,10 +3,11 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
namespace RazorLight.Razor
-{
+{
///
/// Specifies RazorProject where templates are located in files
///
@@ -47,7 +48,7 @@ public override Task GetItemAsync(string templateKey)
templateKey = templateKey + Extension;
}
- string absolutePath = NormalizeKey(templateKey);
+ string absolutePath = GetAbsoluteFilePathFromKey(templateKey);
var item = new FileSystemRazorProjectItem(templateKey, new FileInfo(absolutePath));
if (item.Exists)
@@ -63,7 +64,7 @@ public override Task GetItemAsync(string templateKey)
///
public string Root { get; }
- protected string NormalizeKey(string templateKey)
+ protected string GetAbsoluteFilePathFromKey(string templateKey)
{
if (string.IsNullOrEmpty(templateKey))
{
@@ -100,5 +101,6 @@ public override Task> GetKnownKeysAsync()
return Task.FromResult(files);
}
+ public override string NormalizeKey(string templateKey) => FileSystemRazorProjectHelper.NormalizeKey(templateKey);
}
}
diff --git a/src/RazorLight/Razor/FileSystemRazorProjectHelper.cs b/src/RazorLight/Razor/FileSystemRazorProjectHelper.cs
new file mode 100644
index 00000000..888eeae8
--- /dev/null
+++ b/src/RazorLight/Razor/FileSystemRazorProjectHelper.cs
@@ -0,0 +1,42 @@
+using System.Text;
+
+namespace RazorLight.Razor
+{
+ public static class FileSystemRazorProjectHelper
+ {
+ public static string NormalizeKey(string templateKey)
+ {
+ var addLeadingSlash = templateKey[0] != '\\' && templateKey[0] != '/';
+ var transformSlashes = templateKey.IndexOf('\\') != -1;
+
+ if (!addLeadingSlash && !transformSlashes)
+ {
+ return templateKey;
+ }
+
+ var length = templateKey.Length;
+ if (addLeadingSlash)
+ {
+ length++;
+ }
+
+ var builder = new StringBuilder(length);
+ if (addLeadingSlash)
+ {
+ builder.Append('/');
+ }
+
+ for (var i = 0; i < templateKey.Length; i++)
+ {
+ var ch = templateKey[i];
+ if (ch == '\\')
+ {
+ ch = '/';
+ }
+ builder.Append(ch);
+ }
+
+ return builder.ToString();
+ }
+ }
+}
diff --git a/src/RazorLight/Razor/NoRazorProjectItem.cs b/src/RazorLight/Razor/NoRazorProjectItem.cs
index 956f1acb..6400187a 100644
--- a/src/RazorLight/Razor/NoRazorProjectItem.cs
+++ b/src/RazorLight/Razor/NoRazorProjectItem.cs
@@ -26,7 +26,7 @@ public override bool Equals(object obj)
return string.Equals(Key, other?.Key);
}
- protected bool Equals(NoRazorProjectItem other)
+ private bool Equals(NoRazorProjectItem other)
{
return Key == other?.Key;
}
diff --git a/src/RazorLight/Razor/RazorLightProject.cs b/src/RazorLight/Razor/RazorLightProject.cs
index 26689964..454a43da 100644
--- a/src/RazorLight/Razor/RazorLightProject.cs
+++ b/src/RazorLight/Razor/RazorLightProject.cs
@@ -28,5 +28,7 @@ public virtual Task> GetKnownKeysAsync()
{
return Task.FromResult(Enumerable.Empty());
}
+
+ public virtual string NormalizeKey(string templateKey) => templateKey;
}
}
diff --git a/src/RazorLight/RazorLightEngineBuilder.cs b/src/RazorLight/RazorLightEngineBuilder.cs
index 00961108..d5d696bb 100644
--- a/src/RazorLight/RazorLightEngineBuilder.cs
+++ b/src/RazorLight/RazorLightEngineBuilder.cs
@@ -350,7 +350,7 @@ public virtual RazorLightEngine Build()
var metadataReferenceManager = new DefaultMetadataReferenceManager(options.AdditionalMetadataReferences, options.ExcludedAssemblies);
var assembly = operatingAssembly ?? Assembly.GetEntryAssembly();
- var compiler = new RoslynCompilationService(metadataReferenceManager, assembly);
+ var compiler = new RoslynCompilationService(metadataReferenceManager, assembly, cachingProvider as IPrecompileCallback);
var sourceGenerator = new RazorSourceGenerator(DefaultRazorEngine.Instance, project, options.Namespaces);
var templateCompiler = new RazorTemplateCompiler(sourceGenerator, compiler, project, options);
diff --git a/src/RazorLight/TagHelpers/DefaultTagHelperFactory.cs b/src/RazorLight/TagHelpers/DefaultTagHelperFactory.cs
index 24a6b705..b64ee28a 100644
--- a/src/RazorLight/TagHelpers/DefaultTagHelperFactory.cs
+++ b/src/RazorLight/TagHelpers/DefaultTagHelperFactory.cs
@@ -13,7 +13,7 @@ public class DefaultTagHelperFactory : ITagHelperFactory
{
private readonly ITagHelperActivator _activator;
private readonly ConcurrentDictionary[]> _injectActions;
- private readonly Func[]> _getPropertiesToActivate;
+ //private readonly Func[]> _getPropertiesToActivate;
private static readonly Func> _createActivateInfo = CreateActivateInfo;
///
@@ -53,7 +53,7 @@ public TTagHelper CreateTagHelper(PageContext context)
var propertiesToActivate = _injectActions.GetOrAdd(
tagHelper.GetType(),
- _getPropertiesToActivate);
+ default(PropertyActivator[]));// _getPropertiesToActivate);
for (var i = 0; i < propertiesToActivate.Length; i++)
{
diff --git a/tests/RazorLight.Precompile.Tests/FileSystemCachingStrategyTests.cs b/tests/RazorLight.Precompile.Tests/FileSystemCachingStrategyTests.cs
new file mode 100644
index 00000000..a47c7f3a
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/FileSystemCachingStrategyTests.cs
@@ -0,0 +1,77 @@
+using NUnit.Framework;
+using RazorLight.Caching;
+
+namespace RazorLight.Precompile.Tests
+{
+ public class FileSystemCachingStrategyTests
+ {
+ private static readonly object[] s_testCases = new object[]
+ {
+ FileHashCachingStrategy.Instance,
+ SimpleFileCachingStrategy.Instance,
+ };
+
+ private static readonly string[] s_firstSepOptions = { "", "/", "\\" };
+ private static readonly string[] s_secondSepOptions = { "/", "\\" };
+ private static readonly IEnumerable s_sepCombinations = GetSeparatorCombinations();
+
+ private static IEnumerable GetSeparatorCombinations()
+ {
+ foreach (var s11 in s_firstSepOptions)
+ {
+ foreach (var s12 in s_firstSepOptions)
+ {
+ foreach (var s21 in s_secondSepOptions)
+ {
+ foreach (var s22 in s_secondSepOptions)
+ {
+ if (s11 != s12 || s21 != s22)
+ {
+ yield return new[] { s11, s21, s12, s22 };
+ }
+ }
+ }
+ }
+ }
+ }
+
+ [TestCaseSource(nameof(s_testCases))]
+ public void DifferentKey(IFileSystemCachingStrategy s)
+ {
+ var templateFilePath = "Samples\\folder\\MessageItem.cshtml";
+ var o1 = s.GetCachedFileInfo("folder\\MessageItem.cshtml", templateFilePath, "X:\\");
+ var o2 = s.GetCachedFileInfo("MessageItem.cshtml", templateFilePath, "X:\\");
+ Assert.AreNotEqual(o1.AssemblyFilePath, o2.AssemblyFilePath);
+ }
+
+ [TestCaseSource(nameof(s_sepCombinations))]
+ public void EquivalentKeyFileHashCachingStrategy(string[] sepCombination)
+ {
+ var (asmFilePath1, asmFilePath2) = GetAsmFilePaths(FileHashCachingStrategy.Instance, sepCombination);
+ Assert.AreEqual(asmFilePath1, asmFilePath2);
+ }
+
+ [TestCaseSource(nameof(s_sepCombinations))]
+ public void EquivalentKeySimpleFileCachingStrategy(string[] sepCombination)
+ {
+ var (asmFilePath1, asmFilePath2) = GetAsmFilePaths(SimpleFileCachingStrategy.Instance, sepCombination);
+ if (asmFilePath1 != asmFilePath2)
+ {
+ asmFilePath1 = Path.GetFullPath(asmFilePath1);
+ asmFilePath2 = Path.GetFullPath(asmFilePath2);
+ }
+ Assert.AreEqual(asmFilePath1, asmFilePath2);
+ }
+
+ private static (string, string) GetAsmFilePaths(IFileSystemCachingStrategy s, string[] sepCombination)
+ {
+ var templateFilePath = "Samples\\folder\\MessageItem.cshtml";
+ string key1 = $"{sepCombination[0]}folder{sepCombination[1]}MessageItem.cshtml";
+ string key2 = $"{sepCombination[2]}folder{sepCombination[3]}MessageItem.cshtml";
+ Assert.AreNotEqual(key1, key2);
+ var asmFilePath1 = s.GetCachedFileInfo(key1, templateFilePath, "X:\\").AssemblyFilePath;
+ var asmFilePath2 = s.GetCachedFileInfo(key2, templateFilePath, "X:\\").AssemblyFilePath;
+ return (asmFilePath1, asmFilePath2);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/RazorLight.Precompile.Tests/Helper.cs b/tests/RazorLight.Precompile.Tests/Helper.cs
new file mode 100644
index 00000000..fe047913
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/Helper.cs
@@ -0,0 +1,26 @@
+using NUnit.Framework;
+using System.Text;
+
+namespace RazorLight.Precompile.Tests
+{
+ internal static class Helper
+ {
+ public static string RunCommandTrimNewline(params string[] args)
+ {
+ var sb = RunCommand(args);
+ sb.Replace("\r\n", "");
+ return sb.ToString();
+ }
+
+ public static StringBuilder RunCommand(params string[] args)
+ {
+ var sw = new StringWriter();
+ Program.ConsoleOut = sw;
+ var exitCode = Program.DoRun(args);
+ Assert.Zero(exitCode);
+ sw.Close();
+
+ return sw.GetStringBuilder();
+ }
+ }
+}
diff --git a/tests/RazorLight.Precompile.Tests/PrecompileTestCases.cs b/tests/RazorLight.Precompile.Tests/PrecompileTestCases.cs
new file mode 100644
index 00000000..98445413
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/PrecompileTestCases.cs
@@ -0,0 +1,126 @@
+using NUnit.Framework;
+using RazorLight.Caching;
+
+namespace RazorLight.Precompile.Tests
+{
+ public static class PrecompileTestCases
+ {
+ public static void CleanupDlls(string dir)
+ {
+ foreach (var filePath in Directory.GetFiles(dir, "*.dll", SearchOption.AllDirectories))
+ {
+ File.Delete(filePath);
+ }
+ }
+
+ public const string CACHE_DIR = "Cache";
+
+ public static readonly TestScenario AllDefaults = new(
+ "{m}({0})",
+ FileHashCachingStrategy.Instance,
+ Path.GetDirectoryName,
+ Path.GetFullPath,
+ Path.GetFileName,
+ Array.Empty(),
+ () => CleanupDlls("Samples"));
+
+ public static readonly TestScenario WithCache = new(
+ "{m}_cache({0})",
+ FileHashCachingStrategy.Instance,
+ _ => CACHE_DIR,
+ s => s,
+ AllDefaults.GetTemplateKey,
+ new[] { "-c", CACHE_DIR },
+ () =>
+ {
+ if (Directory.Exists(CACHE_DIR))
+ {
+ Directory.Delete(CACHE_DIR, true);
+ }
+ });
+
+ public static readonly TestScenario WithBase = new(
+ "{m}_base({0})",
+ FileHashCachingStrategy.Instance,
+ AllDefaults.GetExpectedCacheDirectory,
+ AllDefaults.GetExpectedPrecompiledFilePath,
+ s => s,
+ new[] { "-b", "." },
+ () => CleanupDlls("SamplesWithBaseDir"));
+
+ public static readonly TestScenario WithCacheAndBase = new(
+ "{m}_cache_base({0})",
+ FileHashCachingStrategy.Instance,
+ WithCache.GetExpectedCacheDirectory,
+ WithCache.GetExpectedPrecompiledFilePath,
+ WithBase.GetTemplateKey,
+ new[] { "-b", ".", "-c", CACHE_DIR },
+ WithCache.Cleanup);
+
+ public static readonly TestScenario WithStrategyFileHash = new(
+ "{m}_strategy({0}, FileHash)",
+ FileHashCachingStrategy.Instance,
+ AllDefaults.GetExpectedCacheDirectory,
+ AllDefaults.GetExpectedPrecompiledFilePath,
+ AllDefaults.GetTemplateKey,
+ new[] { "-s", "FileHash" },
+ AllDefaults.Cleanup);
+
+ public static readonly TestScenario WithStrategySimple = new(
+ "{m}_strategy({0}, Simple)",
+ SimpleFileCachingStrategy.Instance,
+ AllDefaults.GetExpectedCacheDirectory,
+ AllDefaults.GetExpectedPrecompiledFilePath,
+ AllDefaults.GetTemplateKey,
+ new[] { "-s", "Simple" },
+ AllDefaults.Cleanup);
+
+ public static readonly TestScenario WithCacheAndStrategyFileHash = new(
+ "{m}_cache_strategy({0}, FileHash)",
+ FileHashCachingStrategy.Instance,
+ WithCache.GetExpectedCacheDirectory,
+ WithCache.GetExpectedPrecompiledFilePath,
+ WithCache.GetTemplateKey,
+ new[] { "-s", "FileHash", "-c", CACHE_DIR },
+ WithCache.Cleanup);
+
+ public static readonly TestScenario WithCacheAndStrategySimple = new(
+ "{m}_cache_strategy({0}, Simple)",
+ SimpleFileCachingStrategy.Instance,
+ WithCache.GetExpectedCacheDirectory,
+ WithCache.GetExpectedPrecompiledFilePath,
+ WithCache.GetTemplateKey,
+ new[] { "-s", "Simple", "-c", CACHE_DIR },
+ WithCache.Cleanup);
+
+ public static readonly TestScenario WithCacheAndBaseAndStrategySimple = new(
+ "{m}_cache_base_strategy({0}, Simple)",
+ SimpleFileCachingStrategy.Instance,
+ WithCache.GetExpectedCacheDirectory,
+ WithCache.GetExpectedPrecompiledFilePath,
+ WithBase.GetTemplateKey,
+ new[] { "-s", "Simple", "-c", CACHE_DIR, "-b", "." },
+ WithCache.Cleanup);
+
+ public static readonly TestCaseData[] TestCases = new TestCaseData[]
+ {
+ new("Samples\\WorkItemFields.json", AllDefaults) { TestName = AllDefaults.Name },
+ new("Samples\\FullMessage.cshtml", AllDefaults) { TestName = AllDefaults.Name },
+ new("Samples\\folder\\MessageItem.cshtml", AllDefaults) { TestName = AllDefaults.Name },
+ new("Samples\\WorkItemComment.cshtml", AllDefaults) { TestName = AllDefaults.Name },
+ new("Samples\\FullMessage.cshtml", WithCache) { TestName = WithCache.Name },
+ new("Samples\\folder\\MessageItem.cshtml", WithCache) { TestName = WithCache.Name },
+ new("Samples\\FullMessage.cshtml", WithStrategyFileHash) { TestName = WithStrategyFileHash.Name },
+ new("Samples\\folder\\MessageItem.cshtml", WithStrategyFileHash) { TestName = WithStrategyFileHash.Name },
+ new("Samples\\FullMessage.cshtml", WithStrategySimple) { TestName = WithStrategySimple.Name },
+ new("Samples\\folder\\MessageItem.cshtml", WithStrategySimple) { TestName = WithStrategySimple.Name },
+ new("Samples\\FullMessage.cshtml", WithCacheAndStrategyFileHash) { TestName = WithCacheAndStrategyFileHash.Name },
+ new("Samples\\folder\\MessageItem.cshtml", WithCacheAndStrategySimple) { TestName = WithCacheAndStrategySimple.Name },
+ new("SamplesWithBaseDir\\FullMessage.cshtml", WithBase) { TestName = WithBase.Name },
+ new("SamplesWithBaseDir\\MessageItem.cshtml", WithBase) { TestName = WithBase.Name },
+ new("SamplesWithBaseDir\\FullMessage.cshtml", WithCacheAndBase) { TestName = WithCacheAndBase.Name },
+ new("SamplesWithBaseDir\\MessageItem.cshtml", WithCacheAndBase) { TestName = WithCacheAndBase.Name },
+ new("SamplesWithBaseDir\\FullMessage.cshtml", WithCacheAndBaseAndStrategySimple) { TestName = WithCacheAndBaseAndStrategySimple.Name },
+ };
+ }
+}
\ No newline at end of file
diff --git a/tests/RazorLight.Precompile.Tests/PrecompileTests.cs b/tests/RazorLight.Precompile.Tests/PrecompileTests.cs
new file mode 100644
index 00000000..4579c998
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/PrecompileTests.cs
@@ -0,0 +1,67 @@
+using NUnit.Framework;
+using System.Diagnostics;
+
+namespace RazorLight.Precompile.Tests
+{
+ public class PrecompileTests
+ {
+ [TestCaseSource(typeof(PrecompileTestCases), nameof(PrecompileTestCases.TestCases))]
+ public void PrecompileFromScratch(string templateFilePath, TestScenario scenario)
+ {
+ scenario.Cleanup();
+
+ var expectedPrecompiledFilePath = GetExpectedPrecompiledFilePath(templateFilePath, scenario);
+ FileAssert.DoesNotExist(expectedPrecompiledFilePath);
+
+ Precompile(templateFilePath, scenario, expectedPrecompiledFilePath);
+
+ scenario.Cleanup();
+ FileAssert.DoesNotExist(expectedPrecompiledFilePath);
+ }
+
+ [TestCaseSource(typeof(PrecompileTestCases), nameof(PrecompileTestCases.TestCases))]
+ public void PrecompileCached(string templateFilePath, TestScenario scenario)
+ {
+ scenario.Cleanup();
+
+ var expectedPrecompiledFilePath = GetExpectedPrecompiledFilePath(templateFilePath, scenario);
+ FileAssert.DoesNotExist(expectedPrecompiledFilePath);
+
+ var sw1 = Stopwatch.StartNew();
+ Precompile(templateFilePath, scenario, expectedPrecompiledFilePath);
+ sw1.Stop();
+
+ FileAssert.Exists(expectedPrecompiledFilePath);
+
+ var sw2 = Stopwatch.StartNew();
+ Precompile(templateFilePath, scenario, expectedPrecompiledFilePath);
+ sw2.Stop();
+
+ TestContext.WriteLine($"TS1 = {sw1.Elapsed}, TS2 = {sw2.Elapsed}");
+
+ scenario.Cleanup();
+ FileAssert.DoesNotExist(expectedPrecompiledFilePath);
+ }
+
+ public static string GetExpectedPrecompiledFilePath(string templateFilePath, TestScenario scenario)
+ {
+ var cacheFileInfo = scenario.ExpectedCachingStrategy.GetCachedFileInfo(scenario.GetTemplateKey(templateFilePath), templateFilePath, scenario.GetExpectedCacheDirectory(templateFilePath));
+ return scenario.GetExpectedPrecompiledFilePath(cacheFileInfo.AssemblyFilePath);
+ }
+
+ public static void Precompile(string templateFilePath, TestScenario scenario, string? expectedPrecompiledFilePath)
+ {
+ var commandLineArgs = new List
+ {
+ "precompile",
+ "-t",
+ templateFilePath
+ };
+ commandLineArgs.AddRange(scenario.ExtraCommandLineArgs);
+
+ var precompiledFilePath = Helper.RunCommandTrimNewline(commandLineArgs.ToArray());
+ Assert.AreEqual(expectedPrecompiledFilePath, precompiledFilePath);
+ FileAssert.Exists(precompiledFilePath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/RazorLight.Precompile.Tests/RazorLight.Precompile.Tests.csproj b/tests/RazorLight.Precompile.Tests/RazorLight.Precompile.Tests.csproj
new file mode 100644
index 00000000..73be2459
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/RazorLight.Precompile.Tests.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net6.0
+ true
+ enable
+ enable
+ CS8600;CS8602;CS8603;CS8604
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
diff --git a/tests/RazorLight.Precompile.Tests/Render1Tests.cs b/tests/RazorLight.Precompile.Tests/Render1Tests.cs
new file mode 100644
index 00000000..b7627213
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/Render1Tests.cs
@@ -0,0 +1,122 @@
+using NUnit.Framework;
+
+namespace RazorLight.Precompile.Tests
+{
+ public class Render1Tests
+ {
+ private static TestCaseData T(string templateFilePath, string? jsonQuery, string expected) =>
+ new(templateFilePath, jsonQuery, expected) { TestName = "{m}({0},{1})" };
+
+ private static readonly TestCaseData[] s_testCases = new TestCaseData[]
+ {
+ T("WorkItemFields.json", "[0]", @"{
+ ""Title"": ""Veracode Issue 123"",
+ ""HyperLinks"": [
+ {
+ ""url"": ""https://www.youtube.com/watch?v=Rhc5jXWu55c&t=4815s"",
+ ""comment"": ""src/_sqlutil.async.cs:251""
+ }
+ ],
+ ""Tags"": [
+ ""SQL Injection"",
+ ""resolution:UNRESOLVED"",
+ ""status:OPEN""
+ ]
+}
+"),
+ T("folder\\MessageItem.cshtml", "[0]", @"Issue Id: 123
+
+src/_sqlutil.async.cs:251
+
+This database query contains a SQL injection flaw. The call to system_data_dll.System.Data.Common.DbCommand.ExecuteReaderAsync() constructs a dynamic SQL query using a variable derived from untrusted input. An attacker could exploit this flaw to execute arbitrary SQL queries against the database. ExecuteReaderAsync() was called on the \5__1 object, which contains tainted data. The tainted data originated from earlier calls to vacationbidding_dll.VirtualController.vc_wcfentry, and system_data_dll.System.Data.SqlClient.SqlCommand.ExecuteReader. Avoid dynamically constructing SQL queries. Instead, use parameterized prepared statements to prevent the database from interpreting the contents of bind variables as part of the query. Always validate untrusted input to ensure that it conforms to the expected format, using centralized data validation routines when possible. References: CWE OWASP
+
+Module: Architecture
+
+Type Name: SharpTop.Common.Utils._SqlUtil
+
+Member Name: GetReaderSource
+
+First Found Date: 9/9/2021 3:35:49 PM
+
+ Found no annotations.
+
+"),
+ T("folder\\MessageItem.cshtml", "[1]", @"Issue Id: 987
+
+src/sqlconnectioncontext.cs:35
+
+This call to system_data_dll.System.Data.SqlClient.SqlConnection.!newinit_0_1() allows external control of system settings. The argument to the function is constructed using untrusted input, which can disrupt service or cause an application to behave in unexpected ways. The first argument to !newinit_0_1() contains tainted data from the variable m_connectionString. The tainted data originated from an earlier call to backgroundjobs_dll.VirtualController.vc_wcfentry. Never allow untrusted or otherwise untrusted data to control system-level settings. Always validate untrusted input to ensure that it conforms to the expected format, using centralized data validation routines when possible. References: CWE
+
+Module: Architecture
+
+Type Name: xyz.UtilitySuite.DbUpgrade.SqlConnectionContext
+
+Member Name: CreateConnectionAsync
+
+First Found Date: 7/10/2020 7:00:28 PM
+
+ Found 3 annotations:
+
+ Action | Created | User Name | Comment |
+ APPROVED | 3/31/2021 6:44:12 PM | Michael Jackson | Approved per rationale provided and John Smith' review and approval on 3/30. |
+ COMMENT | 3/31/2021 6:42:26 PM | John Smith | Mitigation statements reviewed. Recommend for closure and approval |
+ APPDESIGN | 10/22/2020 6:58:56 PM | Li Jet | Some explanation |
+
+
+"),
+ T("WorkItemComment.cshtml", "[1].annotations[0]", @"Action: APPROVED
+Created: 3/31/2021 6:44:12 PM
+User name: Michael Jackson
+
+Approved per rationale provided and John Smith' review and approval on 3/30.
+
+")
+ };
+
+ [TestCaseSource(nameof(s_testCases))]
+ public void Render(string templateFilePath, string jsonQuery, string expected)
+ {
+ templateFilePath = "Samples\\" + templateFilePath;
+
+ string precompiledFilePath = Helper.RunCommandTrimNewline("precompile", "-t", templateFilePath);
+
+ var commandLineArgs = new List
+ {
+ "render",
+ "-p",
+ precompiledFilePath,
+ "-m",
+ "Samples\\FindingsWithSourceCodeInfo.json"
+ };
+ if (jsonQuery != null)
+ {
+ commandLineArgs.AddRange(new[] { "-q", jsonQuery });
+ }
+
+ var actual = Helper.RunCommand(commandLineArgs.ToArray()).ToString();
+ Assert.AreEqual(expected, actual);
+ }
+
+ [TestCaseSource(nameof(s_testCases))]
+ public void PrecompileAndRender(string templateFilePath, string jsonQuery, string expected)
+ {
+ templateFilePath = "Samples\\" + templateFilePath;
+
+ var commandLineArgs = new List
+ {
+ "precompile",
+ "-t",
+ templateFilePath,
+ "-m",
+ "Samples\\FindingsWithSourceCodeInfo.json"
+ };
+ if (jsonQuery != null)
+ {
+ commandLineArgs.AddRange(new[] { "-q", jsonQuery });
+ }
+
+ var actual = Helper.RunCommand(commandLineArgs.ToArray()).ToString();
+ Assert.AreEqual(expected, actual);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/RazorLight.Precompile.Tests/Render2Tests.cs b/tests/RazorLight.Precompile.Tests/Render2Tests.cs
new file mode 100644
index 00000000..8492ca98
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/Render2Tests.cs
@@ -0,0 +1,161 @@
+using NUnit.Framework;
+using RazorLight.Caching;
+
+namespace RazorLight.Precompile.Tests
+{
+ public class Render2Tests
+ {
+ private static TestCaseData T(string templateFilePath, string templateFilePath2, IFileSystemCachingStrategy s, string expected) =>
+ new(templateFilePath, templateFilePath2, s, expected) { TestName = "{m}({0},{1},{2})" };
+
+ private const string EXPECTED = @"Count of issues with the source information: 2
+
+
+Issue Id: 123
+
+src/_sqlutil.async.cs:251
+
+This database query contains a SQL injection flaw. The call to system_data_dll.System.Data.Common.DbCommand.ExecuteReaderAsync() constructs a dynamic SQL query using a variable derived from untrusted input. An attacker could exploit this flaw to execute arbitrary SQL queries against the database. ExecuteReaderAsync() was called on the \5__1 object, which contains tainted data. The tainted data originated from earlier calls to vacationbidding_dll.VirtualController.vc_wcfentry, and system_data_dll.System.Data.SqlClient.SqlCommand.ExecuteReader. Avoid dynamically constructing SQL queries. Instead, use parameterized prepared statements to prevent the database from interpreting the contents of bind variables as part of the query. Always validate untrusted input to ensure that it conforms to the expected format, using centralized data validation routines when possible. References: CWE OWASP
+
+Module: Architecture
+
+Type Name: SharpTop.Common.Utils._SqlUtil
+
+Member Name: GetReaderSource
+
+First Found Date: 9/9/2021 3:35:49 PM
+
+ Found no annotations.
+
+
+Issue Id: 987
+
+src/sqlconnectioncontext.cs:35
+
+This call to system_data_dll.System.Data.SqlClient.SqlConnection.!newinit_0_1() allows external control of system settings. The argument to the function is constructed using untrusted input, which can disrupt service or cause an application to behave in unexpected ways. The first argument to !newinit_0_1() contains tainted data from the variable m_connectionString. The tainted data originated from an earlier call to backgroundjobs_dll.VirtualController.vc_wcfentry. Never allow untrusted or otherwise untrusted data to control system-level settings. Always validate untrusted input to ensure that it conforms to the expected format, using centralized data validation routines when possible. References: CWE
+
+Module: Architecture
+
+Type Name: xyz.UtilitySuite.DbUpgrade.SqlConnectionContext
+
+Member Name: CreateConnectionAsync
+
+First Found Date: 7/10/2020 7:00:28 PM
+
+ Found 3 annotations:
+
+ Action | Created | User Name | Comment |
+ APPROVED | 3/31/2021 6:44:12 PM | Michael Jackson | Approved per rationale provided and John Smith' review and approval on 3/30. |
+ COMMENT | 3/31/2021 6:42:26 PM | John Smith | Mitigation statements reviewed. Recommend for closure and approval |
+ APPDESIGN | 10/22/2020 6:58:56 PM | Li Jet | Some explanation |
+
+
+
+";
+
+ private static readonly TestCaseData[] s_testCases = new TestCaseData[]
+ {
+ T("FullMessage.cshtml", "folder\\MessageItem.cshtml", FileHashCachingStrategy.Instance, EXPECTED),
+ T("FullMessage.cshtml", "folder\\MessageItem.cshtml", SimpleFileCachingStrategy.Instance, EXPECTED),
+ };
+
+ [SetUp]
+ public void Cleanup()
+ {
+ PrecompileTestCases.CleanupDlls("Samples");
+ }
+
+ [TestCaseSource(nameof(s_testCases))]
+ public void RenderOrder1(string key, string key2, IFileSystemCachingStrategy s, string expected)
+ {
+ var (a1, a2) = Precompile(key, key2, s);
+
+ Run(key, expected, a1 + ',' + a2);
+ }
+
+ [TestCaseSource(nameof(s_testCases))]
+ public void RenderOrder2(string key, string key2, IFileSystemCachingStrategy s, string expected)
+ {
+ var (a1, a2) = Precompile(key, key2, s);
+
+ Run(key, expected, a2 + ',' + a1);
+ }
+
+ [TestCaseSource(nameof(s_testCases))]
+ public void RenderGlobRecursive(string key, string key2, IFileSystemCachingStrategy s, string expected)
+ {
+ Precompile(key, key2, s);
+
+ Run(key, expected, "**\\*.dll");
+ }
+
+ [TestCaseSource(nameof(s_testCases))]
+ public void RenderFolderRecursive(string key, string key2, IFileSystemCachingStrategy s, string expected)
+ {
+ Precompile(key, key2, s);
+
+ Run(key, expected, "Samples", "-r");
+ }
+
+ [TestCaseSource(nameof(s_testCases))]
+ public void RenderFolderNonRecursive(string key, string key2, IFileSystemCachingStrategy s, string expected)
+ {
+ Precompile(key, key2, s);
+
+ var exc = Assert.Throws(() => Run(key, expected, "Samples"));
+ Assert.AreEqual("No precompiled template found for the key /folder/MessageItem.cshtml", exc.Message);
+ }
+
+ [TestCaseSource(nameof(s_testCases))]
+ public void RenderGlobNonRecursive(string key, string key2, IFileSystemCachingStrategy s, string expected)
+ {
+ Precompile(key, key2, s);
+
+ var exc = Assert.Throws(() => Run(key, expected, "Samples\\*.dll"));
+ Assert.AreEqual("No precompiled template found for the key /folder/MessageItem.cshtml", exc.Message);
+ }
+
+ private static (string, string) Precompile(string key, string key2, IFileSystemCachingStrategy s) => (
+ Helper.RunCommandTrimNewline("precompile", "-t", key, "-b", "Samples", "-s", s.Name),
+ Helper.RunCommandTrimNewline("precompile", "-t", key2, "-b", "Samples", "-s", s.Name)
+ );
+
+ private static void Run(string key, string expected, string precompiledFilePath, params string[] args)
+ {
+ var commandLineArgs = new List
+ {
+ "render",
+ "-p",
+ precompiledFilePath,
+ "-m",
+ "Samples\\FindingsWithSourceCodeInfo.json",
+ "-k",
+ key
+ };
+ commandLineArgs.AddRange(args);
+
+ var actual = Helper.RunCommand(commandLineArgs.ToArray()).ToString();
+ Assert.AreEqual(expected, actual);
+ }
+
+ [TestCaseSource(nameof(s_testCases))]
+ public void PrecompileAndRender(string templateFilePath, string _, IFileSystemCachingStrategy s, string expected)
+ {
+ var commandLineArgs = new List
+ {
+ "precompile",
+ "-t",
+ templateFilePath,
+ "-b",
+ "Samples",
+ "-s",
+ s.Name,
+ "-m",
+ "Samples\\FindingsWithSourceCodeInfo.json"
+ };
+
+ var actual = Helper.RunCommand(commandLineArgs.ToArray()).ToString();
+ Assert.AreEqual(expected, actual);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/RazorLight.Precompile.Tests/Samples/FindingsWithSourceCodeInfo.json b/tests/RazorLight.Precompile.Tests/Samples/FindingsWithSourceCodeInfo.json
new file mode 100644
index 00000000..5a04e2f1
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/Samples/FindingsWithSourceCodeInfo.json
@@ -0,0 +1,110 @@
+[
+ {
+ "issue_id": 123,
+ "scan_type": "STATIC",
+ "description": "This database query contains a SQL injection flaw. The call to system_data_dll.System.Data.Common.DbCommand.ExecuteReaderAsync() constructs a dynamic SQL query using a variable derived from untrusted input. An attacker could exploit this flaw to execute arbitrary SQL queries against the database. ExecuteReaderAsync() was called on the \\5__1 object, which contains tainted data. The tainted data originated from earlier calls to vacationbidding_dll.VirtualController.vc_wcfentry, and system_data_dll.System.Data.SqlClient.SqlCommand.ExecuteReader. Avoid dynamically constructing SQL queries. Instead, use parameterized prepared statements to prevent the database from interpreting the contents of bind variables as part of the query. Always validate untrusted input to ensure that it conforms to the expected format, using centralized data validation routines when possible. References: CWE OWASP",
+ "count": 23,
+ "context_type": "APPLICATION",
+ "context_guid": "4ba52443-07df-433e-9717-21b603b72145",
+ "violates_policy": true,
+ "finding_status": {
+ "first_found_date": "2021-09-09T15:35:49.818Z",
+ "status": "OPEN",
+ "resolution": "UNRESOLVED",
+ "mitigation_review_status": "NONE",
+ "new": false,
+ "resolution_status": "NONE",
+ "last_seen_date": "2022-07-22T23:47:15.187Z"
+ },
+ "finding_details": {
+ "severity": 4,
+ "cwe": {
+ "id": 89,
+ "name": "Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')",
+ "href": "https://api.veracode.com/appsec/v1/cwes/89"
+ },
+ "file_name": "_sqlutil.async.cs",
+ "relative_location": 60,
+ "finding_category": {
+ "id": 19,
+ "name": "SQL Injection",
+ "href": "https://api.veracode.com/appsec/v1/categories/19"
+ },
+ "exploitability": 0,
+ },
+ "build_id": 1234,
+ "SourceCodeInfo": {
+ "Module": "Architecture",
+ "Key": 123,
+ "FilePath": "src/_sqlutil.async.cs",
+ "LineNo": 251,
+ "TypeName": "SharpTop.Common.Utils._SqlUtil",
+ "MemberName": "GetReaderSource",
+ "SourceCodeUrl": "https://www.youtube.com/watch?v=Rhc5jXWu55c&t=4815s"
+ }
+ },
+ {
+ "issue_id": 987,
+ "scan_type": "STATIC",
+ "description": "This call to system_data_dll.System.Data.SqlClient.SqlConnection.!newinit_0_1() allows external control of system settings. The argument to the function is constructed using untrusted input, which can disrupt service or cause an application to behave in unexpected ways. The first argument to !newinit_0_1() contains tainted data from the variable m_connectionString. The tainted data originated from an earlier call to backgroundjobs_dll.VirtualController.vc_wcfentry. Never allow untrusted or otherwise untrusted data to control system-level settings. Always validate untrusted input to ensure that it conforms to the expected format, using centralized data validation routines when possible. References: CWE",
+ "count": 2,
+ "context_type": "APPLICATION",
+ "context_guid": "4ba52443-07df-433e-9717-21b603b72145",
+ "violates_policy": true,
+ "finding_status": {
+ "first_found_date": "2020-07-10T19:00:28.539Z",
+ "status": "CLOSED",
+ "resolution": "MITIGATED",
+ "mitigation_review_status": "NONE",
+ "new": false,
+ "resolution_status": "APPROVED",
+ "last_seen_date": "2022-07-15T01:33:23.129Z"
+ },
+ "finding_details": {
+ "severity": 4,
+ "cwe": {
+ "id": 15,
+ "name": "External Control of System or Configuration Setting",
+ "href": "https://api.veracode.com/appsec/v1/cwes/15"
+ },
+ "file_name": "sqlconnectioncontext.cs",
+ "relative_location": 16,
+ "finding_category": {
+ "id": 24,
+ "name": "Untrusted Initialization",
+ "href": "https://api.veracode.com/appsec/v1/categories/24"
+ },
+ "exploitability": 0,
+ },
+ "annotations": [
+ {
+ "comment": "Approved per rationale provided and John Smith' review and approval on 3/30.",
+ "action": "APPROVED",
+ "created": "2021-03-31T18:44:12.294Z",
+ "user_name": "Michael Jackson"
+ },
+ {
+ "comment": "Mitigation statements reviewed. Recommend for closure and approval",
+ "action": "COMMENT",
+ "created": "2021-03-31T18:42:26.222Z",
+ "user_name": "John Smith"
+ },
+ {
+ "comment": "Some explanation",
+ "action": "APPDESIGN",
+ "created": "2020-10-22T18:58:56.880Z",
+ "user_name": "Li Jet"
+ }
+ ],
+ "build_id": 9876,
+ "SourceCodeInfo": {
+ "Module": "Architecture",
+ "Key": 987,
+ "FilePath": "src/sqlconnectioncontext.cs",
+ "LineNo": 35,
+ "TypeName": "xyz.UtilitySuite.DbUpgrade.SqlConnectionContext",
+ "MemberName": "CreateConnectionAsync",
+ "SourceCodeUrl": "https://www.youtube.com/watch?v=Rhc5jXWu55c&t=4815s"
+ }
+ }
+]
\ No newline at end of file
diff --git a/tests/RazorLight.Precompile.Tests/Samples/FullMessage.cshtml b/tests/RazorLight.Precompile.Tests/Samples/FullMessage.cshtml
new file mode 100644
index 00000000..e0708208
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/Samples/FullMessage.cshtml
@@ -0,0 +1,8 @@
+Count of issues with the source information: @Model.Count
+
+@foreach (var item in Model)
+{
+
+ @{await IncludeAsync("folder\\MessageItem.cshtml", item);}
+
+}
diff --git a/tests/RazorLight.Precompile.Tests/Samples/WorkItemComment.cshtml b/tests/RazorLight.Precompile.Tests/Samples/WorkItemComment.cshtml
new file mode 100644
index 00000000..566eae82
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/Samples/WorkItemComment.cshtml
@@ -0,0 +1,5 @@
+Action: @Model.action
+Created: @Model.created
+User name: @Model.user_name
+
+@Model.comment
diff --git a/tests/RazorLight.Precompile.Tests/Samples/WorkItemFields.json b/tests/RazorLight.Precompile.Tests/Samples/WorkItemFields.json
new file mode 100644
index 00000000..a76f804a
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/Samples/WorkItemFields.json
@@ -0,0 +1,14 @@
+{
+ "Title": "Veracode Issue @Model.issue_id",
+ "HyperLinks": [
+ {
+ "url": "@Model.SourceCodeInfo.SourceCodeUrl",
+ "comment": "@Model.SourceCodeInfo.FilePath:@Model.SourceCodeInfo.LineNo"
+ }
+ ],
+ "Tags": [
+ "@Model.finding_details.finding_category.name",
+ "resolution:@Model.finding_status.resolution",
+ "status:@Model.finding_status.status"
+ ]
+}
\ No newline at end of file
diff --git a/tests/RazorLight.Precompile.Tests/Samples/folder/MessageItem.cshtml b/tests/RazorLight.Precompile.Tests/Samples/folder/MessageItem.cshtml
new file mode 100644
index 00000000..18ecad8c
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/Samples/folder/MessageItem.cshtml
@@ -0,0 +1,29 @@
+Issue Id: @Model.issue_id
+
+@Model.SourceCodeInfo.FilePath:@Model.SourceCodeInfo.LineNo
+
+@Raw(@Model.description)
+
+Module: @Model.SourceCodeInfo.Module
+
+Type Name: @Model.SourceCodeInfo.TypeName
+
+Member Name: @Model.SourceCodeInfo.MemberName
+
+First Found Date: @Model.finding_status.first_found_date
+
+@if (Model.annotations == null || Model.annotations.Count == 0)
+{
+ Found no annotations.
+}
+else
+{
+ Found @Model.annotations.Count annotations:
+
+ Action | Created | User Name | Comment |
+ @foreach (var annotation in Model.annotations)
+ {
+ @annotation.action | @annotation.created | @annotation.user_name | @annotation.comment |
+ }
+
+}
diff --git a/tests/RazorLight.Precompile.Tests/SamplesWithBaseDir/FullMessage.cshtml b/tests/RazorLight.Precompile.Tests/SamplesWithBaseDir/FullMessage.cshtml
new file mode 100644
index 00000000..60f47b85
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/SamplesWithBaseDir/FullMessage.cshtml
@@ -0,0 +1,8 @@
+Count of issues with the source information: @Model.Count
+
+@foreach (var item in Model)
+{
+
+ @{await IncludeAsync("SamplesWithBaseDir\\MessageItem.cshtml", item);}
+
+}
diff --git a/tests/RazorLight.Precompile.Tests/SamplesWithBaseDir/MessageItem.cshtml b/tests/RazorLight.Precompile.Tests/SamplesWithBaseDir/MessageItem.cshtml
new file mode 100644
index 00000000..67b2b652
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/SamplesWithBaseDir/MessageItem.cshtml
@@ -0,0 +1,29 @@
+Issue Id: @Model.issue_id
+
+@Model.SourceCodeInfo.FilePath:@Model.SourceCodeInfo.LineNo
+
+@Raw(@Model.description)
+
+Module: @Model.SourceCodeInfo.Module
+
+Type Name: @Model.SourceCodeInfo.TypeName
+
+Member Name: @Model.SourceCodeInfo.MemberName
+
+First Found Date: @Model.finding_status.first_found_date
+
+@if (Model.annotations == null || Model.annotations.Count == 0)
+{
+ Found no annotations.
+}
+else
+{
+ Found @Model.annotations.Count annotations:
+
+ Action | Created | User Name | Comment |
+ @foreach (var annotation in Model.annotations)
+ {
+ @annotation.action | @annotation.created | @annotation.user_name | @annotation.comment |
+ }
+
+}
diff --git a/tests/RazorLight.Precompile.Tests/TestScenario.cs b/tests/RazorLight.Precompile.Tests/TestScenario.cs
new file mode 100644
index 00000000..54996c69
--- /dev/null
+++ b/tests/RazorLight.Precompile.Tests/TestScenario.cs
@@ -0,0 +1,15 @@
+using RazorLight.Caching;
+
+namespace RazorLight.Precompile.Tests
+{
+ public record TestScenario
+ (
+ string Name,
+ IFileSystemCachingStrategy ExpectedCachingStrategy,
+ Func GetExpectedCacheDirectory,
+ Func GetExpectedPrecompiledFilePath,
+ Func GetTemplateKey,
+ string[] ExtraCommandLineArgs,
+ Action Cleanup
+ );
+}
\ No newline at end of file
diff --git a/tests/RazorLight.Tests/RazorLight.Tests.csproj b/tests/RazorLight.Tests/RazorLight.Tests.csproj
index 0faf23fc..524b10c5 100644
--- a/tests/RazorLight.Tests/RazorLight.Tests.csproj
+++ b/tests/RazorLight.Tests/RazorLight.Tests.csproj
@@ -5,7 +5,8 @@
false
true
$(DefineConstants);SOME_TEST_DEFINE
- true
+ true
+ NU1701;CS0618