├── .gitignore ├── BuildAndPack.ps1 ├── Directory.Build.props ├── README.md ├── Release.ps1 ├── StackExchange.Precompilation.Build ├── App.config ├── Attributes.cs ├── Compilation.cs ├── CompilationAssemblyResolver.cs ├── CompilationProxy.cs ├── ICompilationProxy.cs ├── PrecompilationCommandLineArgs.cs ├── PrecompilationCommandLineParser.cs ├── Program.cs ├── StackExchange.Precompilation.Build.csproj ├── StackExchange.Precompilation.Build.packages.config └── StackExchange.Precompilation.Build.targets ├── StackExchange.Precompilation.Tests ├── CommandLineTests.cs └── StackExchange.Precompilation.Tests.csproj ├── StackExchange.Precompilation.sln ├── StackExchange.Precompilation ├── AfterCompileContext.cs ├── AppDomainHelper.cs ├── Attributes.cs ├── BeforeCompileContext.cs ├── CompileContext.cs ├── CompileModuleElement.cs ├── CompileModulesCollection.cs ├── CompiledFromDirectoryAttribute.cs ├── CompiledFromFileAttribute.cs ├── ICompileContext.cs ├── ICompileModule.cs ├── IMetadataReference.cs ├── PrecompilationModuleLoader.cs ├── PrecompilerSection.cs ├── RazorCacheElement.cs └── StackExchange.Precompilation.csproj ├── StackExhcange.Precompilation.MVC5 ├── Hacks.cs ├── PrecompilationView.cs ├── PrecompilationVirtualPathFactory.cs ├── PrecompiledViewEngine.cs ├── ProfiledVirtualPathProviderViewEngine.cs ├── RazorParser.cs ├── RoslynRazorViewEngine.cs └── StackExchange.Precompilation.MVC5.csproj ├── Test.ConsoleApp ├── AliasTest.cs ├── App.config ├── Program.cs └── Test.ConsoleApp.csproj ├── Test.Module ├── Test.Module.csproj └── TestCompileModule.cs ├── Test.WebApp.ExternalViews ├── App_Code │ └── Helpers.cshtml ├── ExternalViews.cs ├── Test.WebApp.ExternalViews.csproj └── Views │ ├── Shared │ ├── ExternalPartial.cshtml │ └── ExternalView.cshtml │ └── Web.config ├── Test.WebApp ├── Content │ └── PartialExternalContent.cshtml ├── Controllers │ └── HomeController.cs ├── Models │ └── SampleModel.cs ├── MvcApplication.cs ├── Properties │ └── AssemblyInfo.cs ├── Test.WebApp.csproj ├── Views │ ├── Home │ │ ├── ExcludedLayout.cshtml │ │ ├── Index.Mobile.cshtml │ │ └── Index.cshtml │ ├── Other │ │ └── RelativePartial.cshtml │ ├── Shared │ │ ├── EditorTemplates │ │ │ ├── SampleModel.Mobile.cshtml │ │ │ ├── SampleModel.cshtml │ │ │ └── String.cshtml │ │ ├── _Footer.Mobile.cshtml │ │ ├── _Footer.cshtml │ │ ├── _Layout.Excluded.cshtml │ │ ├── _Layout.Mobile.cshtml │ │ ├── _Layout.Overridden.cshtml │ │ └── _Layout.cshtml │ ├── Web.config │ └── _ViewStart.cshtml └── Web.config ├── appveyor.yml ├── license.txt └── semver.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | */bin/ 3 | */obj/ 4 | *.orig 5 | *.user 6 | *.nupkg 7 | *.GhostDoc.xml 8 | MoonSpeak.sln.ide/ 9 | tools 10 | packages 11 | .vs 12 | _ReSharper* 13 | -------------------------------------------------------------------------------- /BuildAndPack.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [parameter(Position=0)] 3 | [string] $VersionSuffix, 4 | [parameter(Position=1)] 5 | [string] $GitCommitId, 6 | [parameter(Position=2)] 7 | [string[]] $MsBuildArgs, 8 | [switch] $CIBuild 9 | ) 10 | 11 | if (-not $semver) 12 | { 13 | set-variable -name semver -scope global -value (get-content .\semver.txt) 14 | } 15 | 16 | if ($VersionSuffix -or $CIBuild) 17 | { 18 | $version = "$semver$VersionSuffix" 19 | } 20 | else 21 | { 22 | $epoch = [math]::truncate((new-timespan -start (get-date -date "01/01/1970") -end (get-date)).TotalSeconds) 23 | $version = "$semver-local$epoch" 24 | } 25 | 26 | if(-not $GitCommitId) 27 | { 28 | $GitCommitId = $(git rev-parse HEAD) 29 | } 30 | 31 | $solutionDir = "$((Resolve-Path .).Path)\" 32 | $defaultArgs = "/v:n", "/nologo", 33 | "/p:SolutionDir=$solutionDir", 34 | "/p:RepositoryCommit=$GitCommitId", 35 | "/p:Version=$version", 36 | "/p:Configuration=Release", 37 | "/p:SEPrecompilerPath=$solutionDir\StackExchange.Precompilation.Build\bin\Release\net462" 38 | if ($MsBuildArgs) 39 | { 40 | $defaultArgs += $MsBuildArgs 41 | } 42 | & msbuild ($defaultArgs + "/t:Restore") 43 | & msbuild ($defaultArgs + "/t:Build,Pack") 44 | 45 | if ($LastExitCode -ne 0) 46 | { 47 | throw "MSBuild failed" 48 | } 49 | 50 | .\Test.ConsoleApp\bin\Release\net462\Test.ConsoleApp.exe 51 | 52 | if ($LastExitCode -ne 0) 53 | { 54 | throw "Test.ConsoleApp failed to run" 55 | } 56 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | full 5 | false 6 | 7 | 8 | 9 | embedded 10 | $(SolutionDir)packages\obj\ 11 | m0sa 12 | Stack Exchange 2017 13 | Razor AspNet MsBuild Roslyn Metaprogramming 14 | https://github.com/StackExchange/StackExchange.Precompilation.git 15 | git 16 | https://github.com/StackExchange/StackExchange.Precompilation 17 | .cs) generation step 52 | * added optional razorCache element to the precompilation configuration section 53 | * fixing non-default langversion bug 54 | * better handling of failing precompilation modules 55 | * don't ouput hidden diagnostics to console 56 | * updated roslyn packages to 2.4.0 57 | 58 | Version 4.1.1 59 | * updated roslyn packages to 2.3.2 60 | 61 | Version 4.1.0 62 | * updated roslyn packages to 2.3.1 63 | * don't emit pdb files when debugtype embedded 64 | * pathmap support 65 | ]]> 66 | 67 | 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | StackExchange.Precompilation 2 | ============================ 3 | 4 | [![Build status](https://ci.appveyor.com/api/projects/status/lvt06wa9io6k64c3/branch/master?svg=true)](https://ci.appveyor.com/project/StackExchange/stackexchange-precompilation/branch/master) 5 | 6 | Replacing csc.exe 7 | ----------------- 8 | 9 | - `Install-Package StackExchange.Precompilation.Build -Pre` 10 | 11 | Replacing aspnet_compiler.exe for .cshtml precompilation 12 | -------------------------------------------------------- 13 | 14 | - `Install-Package StackExchange.Precompilation.Build -Pre` 15 | - Add `true` to your .csproj file (usually replacing the `MvcBuildViews` property) 16 | 17 | #### Using precompiled views 18 | 19 | - [Add the PrecompiledViewEngine to ViewEngines](https://github.com/StackExchange/StackExchange.Precompilation/blob/fd536b764983e2674a4549b7be6f26e971190c1e/Test.WebApp/Global.asax.cs#L29) 20 | 21 | #### Using C# 7 in ASP.NET MVC 5 22 | 23 | - [Add the RoslynRazorViewEngine to ViewEngines](https://github.com/StackExchange/StackExchange.Precompilation/blob/fd536b764983e2674a4549b7be6f26e971190c1e/Test.WebApp/Global.asax.cs#L32) 24 | 25 | Meta-programming 26 | ---------------- 27 | 28 | - Create a new project 29 | - `Install-Package StackExchange.Precompilation -Pre` 30 | - Implement the ICompileModule interface 31 | - `Install-Package StackExchange.Precompilation.Build -Pre` in the target project 32 | - [Configure your new module](https://github.com/StackExchange/StackExchange.Precompilation/blob/fd536b764983e2674a4549b7be6f26e971190c1e/Test.ConsoleApp/App.config#L8) in the target project's web.config or app.config 33 | 34 | 35 | Development 36 | ----------- 37 | 38 | if you have an existing project with StackExchange.Precompilation packages and encounter a bug you can simply: 39 | 40 | - pull this repo 41 | - increment semver.txt 42 | - make the fix in the source code 43 | - run BuildAndPack.ps1 (requires a console with VS env vars in your PATH, I recommend powershell with Posh-VsVars) 44 | - setup a nuget source pointing at .\packages\obj 45 | - after that you can update the packages StackExchange.Precompilation in your target project from the packages\obj source 46 | - this gives you local *-local{timestamp} suffixed packages instead of the *-alpha{build} ones produced by the CI build 47 | - PROTIP: if you want to attach an debugger to the compilation of your project or any of the Test.* projects, add a `System.Diagnostics.Debugger.Launch()` statement somewhere in the code ;) 48 | - CI *-alpha{build} packages are available on the stackoverflow myget feed https://www.myget.org/F/stackoverflow/api/v2 49 | -------------------------------------------------------------------------------- /Release.ps1: -------------------------------------------------------------------------------- 1 | set-variable -name semver -scope global -value (get-content .\semver.txt) 2 | 3 | git tag -a "releases/$semver" -m "creating $semver release" 4 | 5 | git push --tags -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/Attributes.cs: -------------------------------------------------------------------------------- 1 | [assembly:System.Runtime.CompilerServices.InternalsVisibleToAttribute("StackExchange.Precompilation.MVC5")] -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/Compilation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Runtime.InteropServices; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Microsoft.CodeAnalysis; 10 | using Microsoft.CodeAnalysis.CSharp; 11 | using Microsoft.CodeAnalysis.Text; 12 | using Microsoft.CodeAnalysis.Emit; 13 | using System.Collections.Immutable; 14 | using Microsoft.CodeAnalysis.Diagnostics; 15 | using Microsoft.CodeAnalysis.Host; 16 | using System.Composition.Hosting; 17 | using Microsoft.CodeAnalysis.Host.Mef; 18 | using System.Composition; 19 | using System.Threading; 20 | 21 | namespace StackExchange.Precompilation 22 | { 23 | internal class Compilation 24 | { 25 | private readonly PrecompilationCommandLineArgs _precompilationCommandLineArgs; 26 | 27 | internal CSharpCommandLineArguments CscArgs { get; private set; } 28 | internal DirectoryInfo CurrentDirectory { get; private set; } 29 | internal List Diagnostics { get; private set; } 30 | internal Encoding Encoding { get; private set; } 31 | 32 | private const string DiagnosticCategory = "StackExchange.Precompilation"; 33 | private static DiagnosticDescriptor FailedToCreateModule = 34 | new DiagnosticDescriptor("SE001", "Failed to instantiate ICompileModule", "Failed to instantiate ICompileModule '{0}': {1}", DiagnosticCategory, DiagnosticSeverity.Error, true); 35 | private static DiagnosticDescriptor FailedToCreateCompilation = 36 | new DiagnosticDescriptor("SE002", "Failed to create compilation", "{0}", DiagnosticCategory, DiagnosticSeverity.Error, true); 37 | internal static DiagnosticDescriptor ViewGenerationFailed = 38 | new DiagnosticDescriptor("SE003", "View generation failed", "View generation failed: {0}", DiagnosticCategory, DiagnosticSeverity.Error, true); 39 | internal static DiagnosticDescriptor FailedParsingSourceTree = 40 | new DiagnosticDescriptor("SE004", "Failed parsing source tree", "Failed parasing source tree: {0}", DiagnosticCategory, DiagnosticSeverity.Error, true); 41 | internal static DiagnosticDescriptor PrecompilationModuleFailed = 42 | new DiagnosticDescriptor("SE005", "Precompilation module failed", "{0}: {1}", DiagnosticCategory, DiagnosticSeverity.Error, true); 43 | private static DiagnosticDescriptor AnalysisFailed = 44 | new DiagnosticDescriptor("SE006", "Analysis failed", "{0}", DiagnosticCategory, DiagnosticSeverity.Error, true); 45 | private static DiagnosticDescriptor UnhandledException = 46 | new DiagnosticDescriptor("SE007", "Unhandled exception", "Unhandled exception: {0}", DiagnosticCategory, DiagnosticSeverity.Error, true); 47 | internal static DiagnosticDescriptor ERR_FileNotFound = 48 | new DiagnosticDescriptor("CS2001", "FileNotFound", "Source file '{0}' could not be found", DiagnosticCategory, DiagnosticSeverity.Error, true); 49 | internal static DiagnosticDescriptor ERR_BinaryFile = 50 | new DiagnosticDescriptor("CS2015", "BinaryFile", "'{0}' is a binary file instead of a text file", DiagnosticCategory, DiagnosticSeverity.Error, true); 51 | internal static DiagnosticDescriptor ERR_NoSourceFile = 52 | new DiagnosticDescriptor("CS1504", "NoSourceFile", "Source file '{0}' could not be opened ('{1}')", DiagnosticCategory, DiagnosticSeverity.Error, true); 53 | internal static DiagnosticDescriptor CachingFailed = 54 | new DiagnosticDescriptor("SE008", "Razor caching failed", "Caching generated cshtml for '{0}' failed, deleting file '{1}' - '{2}'", DiagnosticCategory, DiagnosticSeverity.Warning, true); 55 | internal static DiagnosticDescriptor CachingFailedHard = 56 | new DiagnosticDescriptor("SE009", "Razor caching failed hard", "Caching generated cshtml for '{0}' to '{1}' failed, unabled to delete cache file", DiagnosticCategory, DiagnosticSeverity.Error, true); 57 | internal static DiagnosticDescriptor RazorParserError = 58 | new DiagnosticDescriptor("SE010", "Razor parser error", "Razor parser error: {0}", DiagnosticCategory, DiagnosticSeverity.Error, true); 59 | 60 | public Compilation(PrecompilationCommandLineArgs precompilationCommandLineArgs) 61 | { 62 | _precompilationCommandLineArgs = precompilationCommandLineArgs; 63 | 64 | CurrentDirectory = new DirectoryInfo(_precompilationCommandLineArgs.BaseDirectory); 65 | 66 | AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(CurrentDirectory.FullName, "App_Data")); // HACK mocking ASP.NET's ~/App_Data aka. |DataDirectory| 67 | 68 | // HACK moar HttpRuntime stuff 69 | AppDomain.CurrentDomain.SetData(".appDomain", AppDomain.CurrentDomain.FriendlyName); 70 | AppDomain.CurrentDomain.SetData(".appPath", CurrentDirectory.FullName); 71 | AppDomain.CurrentDomain.SetData(".appVPath", "/"); 72 | } 73 | 74 | public async Task RunAsync(CancellationToken cancellationToken = default(CancellationToken)) 75 | { 76 | try 77 | { 78 | // this parameter was introduced in rc3, all call to it seem to be using RuntimeEnvironment.GetRuntimeDirectory() 79 | // https://github.com/dotnet/roslyn/blob/0382e3e3fc543fc483090bff3ab1eaae39dfb4d9/src/Compilers/CSharp/csc/Program.cs#L18 80 | var sdkDirectory = RuntimeEnvironment.GetRuntimeDirectory(); 81 | 82 | CscArgs = CSharpCommandLineParser.Default.Parse(_precompilationCommandLineArgs.Arguments, _precompilationCommandLineArgs.BaseDirectory, sdkDirectory); 83 | Diagnostics = new List(CscArgs.Errors); 84 | 85 | // load those before anything else hooks into our AssemlbyResolve. 86 | var loader = new PrecompilationModuleLoader(PrecompilerSection.Current); 87 | loader.ModuleInitializationFailed += (module, ex) => 88 | { 89 | Diagnostics.Add(Diagnostic.Create( 90 | FailedToCreateModule, 91 | Location.Create(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile, new TextSpan(), new LinePositionSpan()), 92 | module.Type, 93 | ex.Message)); 94 | }; 95 | var compilationModules = loader.LoadedModules; 96 | 97 | if (Diagnostics.Any()) 98 | { 99 | return false; 100 | } 101 | Encoding = CscArgs.Encoding ?? new UTF8Encoding(false); // utf8 without bom 102 | 103 | var outputPath = Path.Combine(CscArgs.OutputDirectory, CscArgs.OutputFileName); 104 | var pdbPath = CscArgs.PdbPath ?? Path.ChangeExtension(outputPath, ".pdb"); 105 | 106 | using (var container = CreateCompositionHost()) 107 | using (var workspace = CreateWokspace(container)) 108 | using (var peStream = new MemoryStream()) 109 | using (var pdbStream = CscArgs.EmitPdb && CscArgs.EmitOptions.DebugInformationFormat != DebugInformationFormat.Embedded ? new MemoryStream() : null) 110 | using (var xmlDocumentationStream = !string.IsNullOrWhiteSpace(CscArgs.DocumentationPath) ? new MemoryStream() : null) 111 | { 112 | EmitResult emitResult = null; 113 | 114 | var documentExtenders = workspace.Services.FindLanguageServices(_ => true).ToList(); 115 | var project = CreateProject(workspace, documentExtenders); 116 | CSharpCompilation compilation = null; 117 | CompilationWithAnalyzers compilationWithAnalyzers = null; 118 | try 119 | { 120 | Diagnostics.AddRange((await Task.WhenAll(documentExtenders.Select(x => x.Complete()))).SelectMany(x => x)); 121 | compilation = await project.GetCompilationAsync(cancellationToken) as CSharpCompilation; 122 | } 123 | catch (Exception ex) 124 | { 125 | Diagnostics.Add(Diagnostic.Create(FailedToCreateCompilation, Location.None, ex)); 126 | return false; 127 | } 128 | 129 | var analyzers = project.AnalyzerReferences.SelectMany(x => x.GetAnalyzers(project.Language)).ToImmutableArray(); 130 | if (!analyzers.IsEmpty) 131 | { 132 | compilationWithAnalyzers = compilation.WithAnalyzers(analyzers, project.AnalyzerOptions, cancellationToken); 133 | compilation = compilationWithAnalyzers.Compilation as CSharpCompilation; 134 | } 135 | 136 | var context = new CompileContext(compilationModules); 137 | context.Before(new BeforeCompileContext 138 | { 139 | Arguments = CscArgs, 140 | Compilation = compilation.AddSyntaxTrees(GeneratedSyntaxTrees()), 141 | Diagnostics = Diagnostics, 142 | }); 143 | 144 | CscArgs = context.BeforeCompileContext.Arguments; 145 | compilation = context.BeforeCompileContext.Compilation; 146 | 147 | var analysisTask = compilationWithAnalyzers?.GetAnalysisResultAsync(cancellationToken); 148 | 149 | using (var win32Resources = CreateWin32Resource(compilation)) 150 | { 151 | // PathMapping is also required here, to actually get the symbols to line up: 152 | // https://github.com/dotnet/roslyn/blob/9d081e899b35294b8f1793d31abe5e2c43698844/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs#L616 153 | // PathUtilities.NormalizePathPrefix is internal, but callable via the SourceFileResolver, that we set in CreateProject 154 | var emitOptions = CscArgs.EmitOptions 155 | .WithPdbFilePath(compilation.Options.SourceReferenceResolver.NormalizePath(pdbPath, CscArgs.BaseDirectory)); 156 | 157 | // https://github.com/dotnet/roslyn/blob/41950e21da3ac2c307fb46c2ca8c8509b5059909/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs#L437 158 | emitResult = compilation.Emit( 159 | peStream: peStream, 160 | pdbStream: pdbStream, 161 | xmlDocumentationStream: xmlDocumentationStream, 162 | win32Resources: win32Resources, 163 | manifestResources: CscArgs.ManifestResources, 164 | options: emitOptions, 165 | sourceLinkStream: TryOpenFile(CscArgs.SourceLink, out var sourceLinkStream) ? sourceLinkStream : null, 166 | embeddedTexts: CscArgs.EmbeddedFiles.AsEnumerable() 167 | .Select(x => TryOpenFile(x.Path, out var embeddedText) ? EmbeddedText.FromStream(x.Path, embeddedText) : null) 168 | .Where(x => x != null), 169 | debugEntryPoint: null, 170 | cancellationToken: cancellationToken); 171 | } 172 | 173 | Diagnostics.AddRange(emitResult.Diagnostics); 174 | 175 | try 176 | { 177 | var analysisResult = analysisTask == null ? null : await analysisTask; 178 | if (analysisResult != null) 179 | { 180 | Diagnostics.AddRange(analysisResult.GetAllDiagnostics()); 181 | 182 | foreach (var info in analysisResult.AnalyzerTelemetryInfo) 183 | { 184 | Console.WriteLine($"hidden: {info.Key} {info.Value.ExecutionTime.TotalMilliseconds:#}ms"); 185 | } 186 | } 187 | } 188 | catch (OperationCanceledException) 189 | { 190 | Console.WriteLine("warning: analysis canceled"); 191 | } 192 | catch (Exception ex) 193 | { 194 | Diagnostics.Add(Diagnostic.Create(AnalysisFailed, Location.None, ex)); 195 | return false; 196 | } 197 | 198 | if (!emitResult.Success || HasErrors) 199 | { 200 | return false; 201 | } 202 | 203 | context.After(new AfterCompileContext 204 | { 205 | Arguments = CscArgs, 206 | AssemblyStream = peStream, 207 | Compilation = compilation, 208 | Diagnostics = Diagnostics, 209 | SymbolStream = pdbStream, 210 | XmlDocStream = xmlDocumentationStream, 211 | }); 212 | 213 | if (!HasErrors) 214 | { 215 | // do not create the output files if emit fails 216 | // if the output files are there, msbuild incremental build thinks the previous build succeeded 217 | await Task.WhenAll( 218 | DumpToFileAsync(outputPath, peStream, cancellationToken), 219 | DumpToFileAsync(pdbPath, pdbStream, cancellationToken), 220 | DumpToFileAsync(CscArgs.DocumentationPath, xmlDocumentationStream, cancellationToken)); 221 | return true; 222 | } 223 | 224 | return false; 225 | } 226 | } 227 | catch (PrecompilationModuleException pmex) 228 | { 229 | Diagnostics.Add(Diagnostic.Create(PrecompilationModuleFailed, Location.None, pmex.Message, pmex.InnerException)); 230 | return false; 231 | } 232 | catch (Exception ex) 233 | { 234 | Diagnostics.Add(Diagnostic.Create(UnhandledException, Location.None, ex)); 235 | return false; 236 | } 237 | finally 238 | { 239 | // strings only, since the Console.Out textwriter is another app domain... 240 | // https://stackoverflow.com/questions/2459994/is-there-a-way-to-print-a-new-line-when-using-message 241 | for (var i = 0; i < Diagnostics.Count; i++) 242 | { 243 | var d = Diagnostics[i]; 244 | if (!d.IsSuppressed && d.Severity != DiagnosticSeverity.Hidden) 245 | { 246 | Console.WriteLine(d.ToString().Replace("\r", "").Replace("\n", "\\n")); 247 | } 248 | } 249 | } 250 | } 251 | 252 | private bool HasErrors => Diagnostics.Any(x => !x.IsSuppressed && x.Severity == DiagnosticSeverity.Error); 253 | 254 | private const string WorkspaceKind = nameof(StackExchange) + "." + nameof(StackExchange.Precompilation); 255 | 256 | // all of this is because DesktopAnalyzerAssemblyLoader needs full paths 257 | [ExportWorkspaceService(typeof(IAnalyzerService), WorkspaceKind), Shared] 258 | private class CompilationAnalyzerService : IAnalyzerService, IWorkspaceService 259 | { 260 | private readonly IAnalyzerAssemblyLoader _loader = new CompilationAnalyzerAssemblyLoader(); 261 | 262 | public IAnalyzerAssemblyLoader GetLoader() => _loader; 263 | } 264 | 265 | private class CompilationAnalyzerAssemblyLoader : IAnalyzerAssemblyLoader 266 | { 267 | private static Type DesktopAssemblyLoader = Type.GetType("Microsoft.CodeAnalysis.DesktopAnalyzerAssemblyLoader, Microsoft.CodeAnalysis.Workspaces.Desktop"); 268 | private static IAnalyzerAssemblyLoader _desktopLoader = (IAnalyzerAssemblyLoader)Activator.CreateInstance(DesktopAssemblyLoader); 269 | 270 | private string ResolvePath(string path) => Path.IsPathRooted(path) ? path : Path.GetFullPath(path); 271 | 272 | public void AddDependencyLocation(string fullPath) => _desktopLoader.AddDependencyLocation(ResolvePath(fullPath)); 273 | 274 | public Assembly LoadFromPath(string fullPath) => _desktopLoader.LoadFromPath(ResolvePath(fullPath)); 275 | } 276 | 277 | private CompositionHost CreateCompositionHost() 278 | { 279 | var assemblies = new[] 280 | { 281 | "Microsoft.CodeAnalysis.Workspaces", 282 | "Microsoft.CodeAnalysis.CSharp.Workspaces", 283 | "Microsoft.CodeAnalysis.Workspaces.Desktop", 284 | "StackExchange.Precompilation.MVC5", 285 | }; 286 | 287 | var parts = new List(); 288 | foreach (var a in assemblies) 289 | { 290 | try 291 | { 292 | parts.AddRange(Assembly.Load(a)?.GetTypes() ?? Enumerable.Empty()); 293 | } 294 | catch (ReflectionTypeLoadException thatsWhyWeCantHaveNiceThings) 295 | { 296 | // https://msdn.microsoft.com/en-us/library/system.reflection.assembly.gettypes(v=vs.110).aspx#Anchor_2 297 | parts.AddRange(thatsWhyWeCantHaveNiceThings.Types.Where(x => x != null)); 298 | } 299 | catch (FileNotFoundException nfe) when (nfe.FileName == "StackExchange.Precompilation.MVC5") 300 | { 301 | // enable this to be loaded dynamically 302 | } 303 | } 304 | 305 | return new ContainerConfiguration() 306 | .WithParts(parts) 307 | .WithPart() 308 | .WithPart() 309 | .CreateContainer(); 310 | } 311 | 312 | private static AdhocWorkspace CreateWokspace(CompositionHost container) 313 | { 314 | var host = MefHostServices.Create(container); 315 | // belive me, I did try DesktopMefHostServices.DefaultServices 316 | var workspace = new AdhocWorkspace(host, WorkspaceKind); 317 | return workspace; 318 | } 319 | 320 | private Project CreateProject(AdhocWorkspace workspace, List documentExtenders) 321 | { 322 | var projectInfo = CommandLineProject.CreateProjectInfo(CscArgs.OutputFileName, "C#", Environment.CommandLine, _precompilationCommandLineArgs.BaseDirectory, workspace); 323 | 324 | projectInfo = projectInfo 325 | .WithCompilationOptions(CscArgs.CompilationOptions 326 | .WithSourceReferenceResolver(new SourceFileResolver(CscArgs.SourcePaths, CscArgs.BaseDirectory, CscArgs.PathMap))) // required for path mapping support 327 | .WithDocuments( 328 | projectInfo 329 | .Documents 330 | .Select(d => documentExtenders.Aggregate(d, (doc, ex) => ex.Extend(doc)))); 331 | 332 | return workspace.AddProject(projectInfo); 333 | } 334 | 335 | private bool TryOpenFile(string path, out Stream stream) 336 | { 337 | stream = null; 338 | if (string.IsNullOrEmpty(path)) 339 | { 340 | return false; 341 | } 342 | if (!File.Exists(path)) 343 | { 344 | Diagnostics.Add(Diagnostic.Create(ERR_FileNotFound, null, path)); 345 | return false; 346 | } 347 | try 348 | { 349 | stream = File.OpenRead(path); 350 | return true; 351 | } 352 | catch (Exception ex) 353 | { 354 | Diagnostics.Add(Diagnostic.Create(ERR_NoSourceFile, null, path, ex.Message)); 355 | return false; 356 | } 357 | } 358 | 359 | private Stream CreateWin32Resource(CSharpCompilation compilation) 360 | { 361 | if (TryOpenFile(CscArgs.Win32ResourceFile, out var stream)) return stream; 362 | 363 | using (var manifestStream = compilation.Options.OutputKind != OutputKind.NetModule && TryOpenFile(CscArgs.Win32Manifest, out var manifest) ? manifest : null) 364 | using (var iconStream = TryOpenFile(CscArgs.Win32Icon, out var icon) ? icon : null) 365 | return compilation.CreateDefaultWin32Resources(true, CscArgs.NoWin32Manifest, manifestStream, iconStream); 366 | } 367 | 368 | private static async Task DumpToFileAsync(string path, MemoryStream stream, CancellationToken cancellationToken) 369 | { 370 | if (stream?.Length > 0) 371 | { 372 | stream.Position = 0; 373 | using (var file = File.Create(path)) 374 | using (cancellationToken.Register(() => { try { File.Delete(path); } catch { } })) 375 | { 376 | await stream.CopyToAsync(file, 4096, cancellationToken); 377 | } 378 | } 379 | } 380 | 381 | private sealed class NaiveReferenceResolver : MetadataReferenceResolver 382 | { 383 | private NaiveReferenceResolver() { } 384 | public static NaiveReferenceResolver Instance { get; } = new NaiveReferenceResolver(); 385 | public override bool Equals(object other) => other is NaiveReferenceResolver; 386 | 387 | public override int GetHashCode() => 42; 388 | 389 | public override ImmutableArray ResolveReference(string reference, string baseFilePath, MetadataReferenceProperties properties) 390 | => ImmutableArray.Create(MetadataReference.CreateFromFile(reference, properties)); 391 | } 392 | 393 | private IEnumerable GeneratedSyntaxTrees() 394 | { 395 | yield return SyntaxFactory.ParseSyntaxTree($"[assembly: {typeof(CompiledFromDirectoryAttribute).FullName}(@\"{CurrentDirectory.FullName}\")]", CscArgs.ParseOptions); 396 | } 397 | 398 | public Location AsLocation(string path) 399 | { 400 | return Location.Create(path, new TextSpan(), new LinePositionSpan()); 401 | } 402 | } 403 | 404 | public interface IDocumentExtender : ILanguageService 405 | { 406 | DocumentInfo Extend(DocumentInfo document); 407 | Task> Complete(); 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/CompilationAssemblyResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading; 7 | 8 | namespace StackExchange.Precompilation 9 | { 10 | 11 | class CompilationAssemblyResolver : MarshalByRefObject 12 | { 13 | internal static void Register(AppDomain domain, string[] references) 14 | { 15 | CompilationAssemblyResolver resolver = 16 | domain.CreateInstanceFromAndUnwrap( 17 | Assembly.GetExecutingAssembly().Location, 18 | typeof(CompilationAssemblyResolver).FullName) as CompilationAssemblyResolver; 19 | resolver.RegisterDomain(domain); 20 | resolver.Setup(references); 21 | } 22 | 23 | private AppDomain domain; 24 | private readonly ConcurrentDictionary> resolvedAssemblies = new ConcurrentDictionary>(); 25 | 26 | private void Setup(string[] references) 27 | { 28 | void Resolve(AssemblyName name, Func loader) 29 | { 30 | var resolved = new Lazy(loader, LazyThreadSafetyMode.ExecutionAndPublication); 31 | var keyName = new AssemblyName(ApplyPolicy(name.FullName)); 32 | resolvedAssemblies.AddOrUpdate(keyName.FullName, resolved, (key, existing) => existing); // TODO log conflicting binds? 33 | resolvedAssemblies.AddOrUpdate(keyName.Name, resolved, (key, existing) => existing); // TODO log conflicting partial binds? 34 | } 35 | 36 | // load runtime references from tools/*.dll 37 | 38 | var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 39 | Directory.EnumerateFiles(location, "*.dll") 40 | .AsParallel() 41 | .ForAll(dll => 42 | { 43 | try 44 | { 45 | var assemblyName = AssemblyName.GetAssemblyName(dll); 46 | Resolve(assemblyName, () => Assembly.LoadFile(dll)); 47 | } 48 | catch (Exception ex) 49 | { 50 | Console.WriteLine("hidden: failed to resolve assembly {0}: {1}", dll, ex.Message); 51 | } 52 | }); 53 | 54 | // load all the other references 55 | references 56 | .AsParallel() 57 | .Select(x => 58 | { 59 | try 60 | { 61 | return AssemblyName.GetAssemblyName(x); 62 | } 63 | catch (Exception ex) 64 | { 65 | Console.WriteLine($"warning: Couldn't load reference from '{x}' - '{ex.Message}'"); 66 | return null; 67 | } 68 | }) 69 | .Where(x => x != null) 70 | .ForAll(name => Resolve(name, () => 71 | { 72 | var path = new Uri(name.CodeBase).LocalPath; 73 | try 74 | { 75 | return Assembly.LoadFile(path); 76 | } 77 | catch (Exception ex) 78 | { 79 | Console.WriteLine($"warning: Couldn't load reference '{name.FullName}' from '{path}' - '{ex.Message}'"); 80 | return null; 81 | } 82 | })); 83 | } 84 | 85 | private void RegisterDomain(AppDomain domain) 86 | { 87 | this.domain = domain; 88 | this.domain.AssemblyResolve += ResolveAssembly; 89 | } 90 | 91 | private string ApplyPolicy(string name) 92 | { 93 | while (true) { 94 | var newName = domain.ApplyPolicy(name); 95 | if (newName == name) return name; 96 | name = newName; 97 | } 98 | } 99 | 100 | private Assembly ResolveAssembly(object sender, ResolveEventArgs e) 101 | { 102 | var name = ApplyPolicy(e.Name); 103 | var assemblyName = new AssemblyName(name); 104 | 105 | return resolvedAssemblies.GetOrAdd(assemblyName.FullName, NullAssembly).Value ?? 106 | resolvedAssemblies.GetOrAdd(assemblyName.Name, NullAssembly).Value; 107 | } 108 | 109 | private static Lazy NullAssembly(string key) => new Lazy(() => null, LazyThreadSafetyMode.ExecutionAndPublication); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/CompilationProxy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading; 7 | 8 | namespace StackExchange.Precompilation 9 | { 10 | class CompilationProxy : MarshalByRefObject 11 | { 12 | public static bool RunCs(string[] args) 13 | { 14 | var precompilationArgs = PrecompilationCommandLineParser.Parse(args, Directory.GetCurrentDirectory()); 15 | 16 | CompilationProxy proxy = null; 17 | AppDomain compilationDomain = null; 18 | try 19 | { 20 | var currentSetup = AppDomain.CurrentDomain.SetupInformation; 21 | var setup = new AppDomainSetup() 22 | { 23 | ApplicationName = currentSetup.ApplicationName, 24 | ApplicationBase = currentSetup.ApplicationBase, 25 | ConfigurationFile = precompilationArgs.AppConfig, 26 | DisallowBindingRedirects = true, 27 | }; 28 | 29 | if (setup.ConfigurationFile == null) 30 | { 31 | setup.ConfigurationFile = new[] { "app.config", "web.config" }.Select(x => Path.Combine(precompilationArgs.BaseDirectory, x)).FirstOrDefault(File.Exists); 32 | if (!string.IsNullOrWhiteSpace(setup.ConfigurationFile)) 33 | { 34 | Console.WriteLine("WARNING: '" + setup.ConfigurationFile + "' used as fallback config file"); 35 | } 36 | } 37 | 38 | compilationDomain = AppDomain.CreateDomain( 39 | AppDomainHelper.CsCompilationAppDomainName, 40 | AppDomain.CurrentDomain.Evidence, 41 | setup); 42 | compilationDomain.UnhandledException += (s, e) => 43 | { 44 | Console.WriteLine("error: " + e); 45 | }; 46 | 47 | var references = precompilationArgs.References; //.Concat(Directory.EnumerateFiles(AppDomain.CurrentDomain.BaseDirectory).Where(r => assemblyExt.Contains(Path.GetExtension(r)))).ToArray() 48 | CompilationAssemblyResolver.Register(compilationDomain, references); 49 | 50 | 51 | proxy = (CompilationProxy)compilationDomain.CreateInstanceAndUnwrap( 52 | typeof(CompilationProxy).Assembly.FullName, 53 | typeof(CompilationProxy).FullName); 54 | 55 | Console.CancelKeyPress += (s, e) => proxy?.ForceStop(); 56 | return proxy.RunCs(precompilationArgs); 57 | } 58 | finally 59 | { 60 | proxy?.ForceStop(); 61 | // runtime has exited, finish off by unloading the runtime appdomain 62 | if (compilationDomain != null) AppDomain.Unload(compilationDomain); 63 | } 64 | } 65 | 66 | public override object InitializeLifetimeService() => null; 67 | 68 | private CancellationTokenSource _cts; 69 | 70 | // ReSharper disable MemberCanBeMadeStatic.Local 71 | // making the methods below static would not be a good idea, they need to run in the _compilation app domain 72 | private bool RunCs(PrecompilationCommandLineArgs precompilationArgs) 73 | { 74 | _cts = new CancellationTokenSource(); 75 | return new Compilation(precompilationArgs).RunAsync(_cts.Token).Result; 76 | } 77 | 78 | private void ForceStop() 79 | { 80 | var cts = _cts; 81 | _cts = null; 82 | cts?.Cancel(); 83 | cts?.Dispose(); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/ICompilationProxy.cs: -------------------------------------------------------------------------------- 1 | namespace StackExchange.Precompilation 2 | { 3 | interface ICompilationProxy 4 | { 5 | object InitializeLifetimeService(); 6 | } 7 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/PrecompilationCommandLineArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StackExchange.Precompilation 4 | { 5 | [Serializable] 6 | public class PrecompilationCommandLineArgs : MarshalByRefObject 7 | { 8 | /// 9 | /// Unprocessed arguments. 10 | /// 11 | public string[] Arguments { get; set; } 12 | 13 | /// 14 | /// The current directory in which the compilation was started. 15 | /// 16 | public string BaseDirectory { get; set; } 17 | 18 | /// 19 | /// The value of the /appconfig switch if present. 20 | /// 21 | public string AppConfig { get; set; } 22 | 23 | /// 24 | /// The values of the /reference switches. 25 | /// 26 | public string[] References { get; set; } 27 | } 28 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/PrecompilationCommandLineParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace StackExchange.Precompilation 8 | { 9 | // we need the values of the /reference and the /appconfig switches before we spin up the app domain 10 | // to get those we need to either spin up a new AppDomain and load Microsoft.CodeAnalysis.CSharp to use CSharpCommandLineParser or parse the args ourselves 11 | // https://msdn.microsoft.com/en-us/library/78f4aasd.aspx 12 | public class PrecompilationCommandLineParser 13 | { 14 | private static readonly Regex UnespacedBackslashes = new Regex(@"(?!\\+"")\\+", RegexOptions.Compiled); 15 | private static readonly Regex QuotesAfterSingleBackSlash = new Regex(@"(?<=(^|[^\\])((\\\\)+)?)""", RegexOptions.ExplicitCapture | RegexOptions.Compiled); 16 | private static readonly Regex Escaped = new Regex(@"\\(\\|"")", RegexOptions.Compiled); 17 | 18 | public static string[] SplitCommandLine(string commandLine) 19 | { 20 | return Split(commandLine) 21 | .TakeWhile(arg => !arg.StartsWith("#", StringComparison.Ordinal)) 22 | .Select(dirty => UnespacedBackslashes.Replace(dirty, "$0$0")) 23 | .Select(normalized => QuotesAfterSingleBackSlash.Replace(normalized, "")) 24 | .Select(unquoted => Escaped.Replace(unquoted, "$1")) 25 | .Where(arg => !string.IsNullOrEmpty(arg)) 26 | .Select(str => str.Trim()) 27 | .ToArray(); 28 | } 29 | 30 | private static IEnumerable Split(string commandLine) 31 | { 32 | var isQuoted = false; 33 | var backslashCount = 0; 34 | var splitIndex = 0; 35 | var length = commandLine.Length; 36 | 37 | for (var i = 0; i < length; i++) 38 | { 39 | var c = commandLine[i]; 40 | switch (c) 41 | { 42 | case '\\': 43 | backslashCount += 1; 44 | break; 45 | case '\"': 46 | if (backslashCount % 2 == 0) isQuoted = !isQuoted; 47 | goto default; 48 | case ' ': 49 | case '\t': 50 | case '\n': 51 | case '\r': 52 | if (!isQuoted) 53 | { 54 | var take = i - splitIndex; 55 | if (take > 0) 56 | { 57 | yield return commandLine.Substring(splitIndex, take); 58 | } 59 | splitIndex = i + 1; 60 | } 61 | goto default; 62 | default: 63 | backslashCount = 0; 64 | break; 65 | } 66 | } 67 | 68 | if (splitIndex < length) 69 | { 70 | yield return commandLine.Substring(splitIndex); 71 | } 72 | } 73 | 74 | private static readonly Regex Reference = new Regex(@"/r(eference)?:(?[\w_]+=)?", RegexOptions.IgnoreCase | RegexOptions.Compiled); 75 | 76 | public static PrecompilationCommandLineArgs Parse(string[] arguments, string baseDirectory) 77 | { 78 | var result = new PrecompilationCommandLineArgs { Arguments = arguments, BaseDirectory = baseDirectory }; 79 | if (arguments == null) return result; 80 | 81 | var loadedRsp = new HashSet(); 82 | var references = new HashSet(); 83 | for(var i = 0; i < arguments.Length; i++) 84 | { 85 | var arg = arguments[i]; 86 | var reference = Reference.Match(arg); 87 | if(arg.StartsWith("@")) 88 | { 89 | if (!loadedRsp.Add(arg = ParseFileFromArg(arg, '@'))) continue; 90 | arguments = arguments.Concat(File.ReadAllLines(arg).SelectMany(SplitCommandLine)).ToArray(); 91 | } 92 | else if(reference.Success) 93 | { 94 | // don't care about reference aliases in the compilation appdomain 95 | // https://msdn.microsoft.com/en-us/library/ms173212.aspx 96 | references.Add(ParseFileFromArg(arg, reference.Groups["alias"].Success ? '=' : ':')); 97 | } 98 | else if(arg.StartsWith("/appconfig:")) 99 | { 100 | result.AppConfig = ParseFileFromArg(arg); 101 | } 102 | } 103 | result.References = references.ToArray(); 104 | return result; 105 | } 106 | 107 | private static string ParseFileFromArg(string arg, char delimiter = ':') 108 | { 109 | return Path.GetFullPath(arg.Substring(arg.IndexOf(delimiter) + 1)); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | 7 | namespace StackExchange.Precompilation 8 | { 9 | static class Program 10 | { 11 | static void Main(string[] args) 12 | { 13 | try 14 | { 15 | if (!CompilationProxy.RunCs(args)) 16 | { 17 | Environment.ExitCode = 1; 18 | } 19 | } 20 | catch (Exception ex) 21 | { 22 | var agg = ex as AggregateException; 23 | Console.WriteLine("ERROR: An unhandled exception occured"); 24 | if (agg != null) 25 | { 26 | agg = agg.Flatten(); 27 | foreach (var inner in agg.InnerExceptions) 28 | { 29 | Console.Error.WriteLine("error: " + inner); 30 | } 31 | } 32 | else 33 | { 34 | Console.Error.WriteLine("error: " + ex); 35 | } 36 | Environment.ExitCode = 2; 37 | } 38 | } 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/StackExchange.Precompilation.Build.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net462 6 | StackExchange.Precompiler 7 | Replaces CSC and aspnet_compiler.exe with StackExchange.Precompiler for compiling C# (.cs) and Razor View (.cshtml) files in asp.net mvc 5 projects. 8 | true 9 | true 10 | StackExchange.Precompilation.Build 11 | true 12 | _ToolsSetup;$(BeforePack) 13 | 14 | 15 | 16 | {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 17 | StackExchange.Precompilation 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <_BinOutputs Include="bin\$(Configuration)\$(TargetFramework)\*.*" /> 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/StackExchange.Precompilation.Build.packages.config: -------------------------------------------------------------------------------- 1 |  8 | -------------------------------------------------------------------------------- /StackExchange.Precompilation.Build/StackExchange.Precompilation.Build.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 7 | false 8 | false 9 | 10 | $(CompileDependsOn); 11 | SEPrecompilerCore; 12 | 13 | $(MSBuildThisFileDirectory)..\tools 14 | true 15 | 16 | 17 | 18 | $(SEPrecompilerTools) 19 | $(SEPrecompilerPath) 20 | StackExchange.Precompiler.exe 21 | 22 | 23 | 28 | 29 | $(SEPrecompilerCscToolPath) 30 | $(SEPrecompilerCscToolExe) 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /StackExchange.Precompilation.Tests/CommandLineTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using NUnit.Framework; 5 | 6 | namespace StackExchange.Precompilation.Tests 7 | { 8 | [TestFixture] 9 | public class CommandLineTests 10 | { 11 | // test cases from https://github.com/dotnet/roslyn/blob/9c66e81c1424d8f4999f70eb8b85f0e76f253c30/src/Compilers/Core/CodeAnalysisTest/CommonCommandLineParserTests.cs#L83 12 | [Test] 13 | [TestCase("", new string[0])] 14 | [TestCase(" \t ", new string[0])] 15 | [TestCase(" abc\tdef baz quuz ", new[] { "abc", "def", "baz", "quuz" })] 16 | [TestCase(@" ""abc def"" fi""ddle dee de""e ""hi there ""dude he""llo there"" ", new [] { @"abc def", @"fiddle dee dee", @"hi there dude", @"hello there" })] 17 | [TestCase(@" ""abc def \"" baz quuz"" ""\""straw berry"" fi\""zz \""buzz fizzbuzz", new [] { @"abc def "" baz quuz", @"""straw berry", @"fi""zz", @"""buzz", @"fizzbuzz" })] 18 | [TestCase(@" \\""abc def"" \\\""abc def"" ", new [] { @"\abc def", @"\""abc", @"def" })] 19 | [TestCase(@" \\\\""abc def"" \\\\\""abc def"" ", new [] { @"\\abc def", @"\\""abc", @"def" })] 20 | [TestCase(@" \\\\""abc def"" \\\\\""abc def"" q a r ", new [] { @"\\abc def", @"\\""abc", @"def q a r" })] 21 | [TestCase(@"abc #Comment ignored", new [] { @"abc" })] 22 | public static void SplitArguments(string input, string[] expected) 23 | { 24 | var actual = PrecompilationCommandLineParser.SplitCommandLine(input); 25 | 26 | CollectionAssert.AreEqual(expected, actual); 27 | } 28 | 29 | [Test] 30 | public void ParseArguments() 31 | { 32 | Assert.DoesNotThrow(() => PrecompilationCommandLineParser.Parse(null, null)); 33 | Assert.DoesNotThrow(() => PrecompilationCommandLineParser.Parse(new string[0], null)); 34 | Assert.DoesNotThrow(() => PrecompilationCommandLineParser.Parse(null, "")); 35 | Assert.DoesNotThrow(() => PrecompilationCommandLineParser.Parse(new string[0], "")); 36 | } 37 | 38 | [Test] 39 | [TestCase("a b c", new string[0], (string)null)] 40 | [TestCase("a b /r:c.dll", new[] { "c.dll" }, (string)null)] 41 | [TestCase("a b /r:a=c.dll", new[] { "c.dll" }, (string)null)] 42 | [TestCase("a b /reference:a=c.dll", new[] { "c.dll" }, (string)null)] 43 | [TestCase("a b /r:c.dll /r:d.dll", new[] { "c.dll", "d.dll" }, (string)null)] 44 | [TestCase("a b /r:c.dll /appconfig:moar.config /r:d.dll", new[] { "c.dll", "d.dll" }, "moar.config")] 45 | [TestCase("a b /r:alias=c.dll /appconfig:moar.config /reference:d.dll", new[] { "c.dll", "d.dll" }, "moar.config")] 46 | public void ParseArgumentCases(string cmdline, string[] references, string appconfig) 47 | { 48 | var dir = Guid.NewGuid().ToString(); 49 | var args = PrecompilationCommandLineParser.SplitCommandLine(cmdline); 50 | var parsed = PrecompilationCommandLineParser.Parse(args, dir); 51 | Func resolvePath = Path.GetFullPath; 52 | 53 | Assert.AreEqual(dir, parsed.BaseDirectory); 54 | Assert.AreEqual(appconfig == null ? null : resolvePath(appconfig), parsed.AppConfig); 55 | CollectionAssert.AreEqual(references.Select(resolvePath), parsed.References); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /StackExchange.Precompilation.Tests/StackExchange.Precompilation.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net462 5 | x86 6 | x86 7 | 8 | 9 | 10 | 11 | 12 | 13 | {31DFCCCC-2F44-405E-A2D7-BB1AC718E7B9} 14 | StackExchange.Precompilation.Build 15 | 16 | 17 | -------------------------------------------------------------------------------- /StackExchange.Precompilation.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.2015 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackExchange.Precompilation.Build", "StackExchange.Precompilation.Build\StackExchange.Precompilation.Build.csproj", "{31DFCCCC-2F44-405E-A2D7-BB1AC718E7B9}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackExchange.Precompilation", "StackExchange.Precompilation\StackExchange.Precompilation.csproj", "{3C0A90F1-B19E-4305-AB71-3CD31C7D0F4D}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{5958EFE5-C8D6-4759-9A8E-8C64558314FD}" 11 | ProjectSection(SolutionItems) = preProject 12 | .nuget\NuGet.Config = .nuget\NuGet.Config 13 | .nuget\NuGet.exe = .nuget\NuGet.exe 14 | .nuget\NuGet.targets = .nuget\NuGet.targets 15 | EndProjectSection 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.Module", "Test.Module\Test.Module.csproj", "{5FCAECC3-787B-473F-A372-783D0C235190}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.ConsoleApp", "Test.ConsoleApp\Test.ConsoleApp.csproj", "{BA716DA2-4E3C-4D9F-B9C2-78C0EAEF66D7}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.WebApp", "Test.WebApp\Test.WebApp.csproj", "{5B0105A4-256B-4A88-852C-6F5E9D185515}" 22 | ProjectSection(ProjectDependencies) = postProject 23 | {31DFCCCC-2F44-405E-A2D7-BB1AC718E7B9} = {31DFCCCC-2F44-405E-A2D7-BB1AC718E7B9} 24 | EndProjectSection 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A6462B41-5067-4F2B-B5B8-B7BD1B2D75CB}" 27 | ProjectSection(SolutionItems) = preProject 28 | BuildAndPack.ps1 = BuildAndPack.ps1 29 | PrepareForPack.ps1 = PrepareForPack.ps1 30 | semver.txt = semver.txt 31 | EndProjectSection 32 | EndProject 33 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackExchange.Precompilation.Tests", "StackExchange.Precompilation.Tests\StackExchange.Precompilation.Tests.csproj", "{B6306D9B-0770-44DF-AADE-5703B1DCFD67}" 34 | ProjectSection(ProjectDependencies) = postProject 35 | {C8F659BD-D0D1-4404-9CC5-3F14ED4B28F3} = {C8F659BD-D0D1-4404-9CC5-3F14ED4B28F3} 36 | {3C0A90F1-B19E-4305-AB71-3CD31C7D0F4D} = {3C0A90F1-B19E-4305-AB71-3CD31C7D0F4D} 37 | EndProjectSection 38 | EndProject 39 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.WebApp.ExternalViews", "Test.WebApp.ExternalViews\Test.WebApp.ExternalViews.csproj", "{2BA24772-F7B0-4652-A430-2F4C2262E882}" 40 | ProjectSection(ProjectDependencies) = postProject 41 | {5FCAECC3-787B-473F-A372-783D0C235190} = {5FCAECC3-787B-473F-A372-783D0C235190} 42 | {31DFCCCC-2F44-405E-A2D7-BB1AC718E7B9} = {31DFCCCC-2F44-405E-A2D7-BB1AC718E7B9} 43 | EndProjectSection 44 | EndProject 45 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackExchange.Precompilation.MVC5", "StackExhcange.Precompilation.MVC5\StackExchange.Precompilation.MVC5.csproj", "{C8F659BD-D0D1-4404-9CC5-3F14ED4B28F3}" 46 | EndProject 47 | Global 48 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 49 | Debug|Any CPU = Debug|Any CPU 50 | Release|Any CPU = Release|Any CPU 51 | EndGlobalSection 52 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 53 | {31DFCCCC-2F44-405E-A2D7-BB1AC718E7B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {31DFCCCC-2F44-405E-A2D7-BB1AC718E7B9}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {31DFCCCC-2F44-405E-A2D7-BB1AC718E7B9}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {31DFCCCC-2F44-405E-A2D7-BB1AC718E7B9}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {3C0A90F1-B19E-4305-AB71-3CD31C7D0F4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {3C0A90F1-B19E-4305-AB71-3CD31C7D0F4D}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {3C0A90F1-B19E-4305-AB71-3CD31C7D0F4D}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {3C0A90F1-B19E-4305-AB71-3CD31C7D0F4D}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {5FCAECC3-787B-473F-A372-783D0C235190}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {5FCAECC3-787B-473F-A372-783D0C235190}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {5FCAECC3-787B-473F-A372-783D0C235190}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {5FCAECC3-787B-473F-A372-783D0C235190}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {BA716DA2-4E3C-4D9F-B9C2-78C0EAEF66D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {BA716DA2-4E3C-4D9F-B9C2-78C0EAEF66D7}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {BA716DA2-4E3C-4D9F-B9C2-78C0EAEF66D7}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {BA716DA2-4E3C-4D9F-B9C2-78C0EAEF66D7}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {5B0105A4-256B-4A88-852C-6F5E9D185515}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {5B0105A4-256B-4A88-852C-6F5E9D185515}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {5B0105A4-256B-4A88-852C-6F5E9D185515}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {5B0105A4-256B-4A88-852C-6F5E9D185515}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {B6306D9B-0770-44DF-AADE-5703B1DCFD67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {B6306D9B-0770-44DF-AADE-5703B1DCFD67}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {B6306D9B-0770-44DF-AADE-5703B1DCFD67}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {B6306D9B-0770-44DF-AADE-5703B1DCFD67}.Release|Any CPU.Build.0 = Release|Any CPU 77 | {2BA24772-F7B0-4652-A430-2F4C2262E882}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 78 | {2BA24772-F7B0-4652-A430-2F4C2262E882}.Debug|Any CPU.Build.0 = Debug|Any CPU 79 | {2BA24772-F7B0-4652-A430-2F4C2262E882}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {2BA24772-F7B0-4652-A430-2F4C2262E882}.Release|Any CPU.Build.0 = Release|Any CPU 81 | {C8F659BD-D0D1-4404-9CC5-3F14ED4B28F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 82 | {C8F659BD-D0D1-4404-9CC5-3F14ED4B28F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 83 | {C8F659BD-D0D1-4404-9CC5-3F14ED4B28F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 84 | {C8F659BD-D0D1-4404-9CC5-3F14ED4B28F3}.Release|Any CPU.Build.0 = Release|Any CPU 85 | EndGlobalSection 86 | GlobalSection(SolutionProperties) = preSolution 87 | HideSolutionNode = FALSE 88 | EndGlobalSection 89 | GlobalSection(ExtensibilityGlobals) = postSolution 90 | SolutionGuid = {153D37EE-CF3F-42D8-9703-EE953797B3A9} 91 | EndGlobalSection 92 | EndGlobal 93 | -------------------------------------------------------------------------------- /StackExchange.Precompilation/AfterCompileContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | 6 | namespace StackExchange.Precompilation 7 | { 8 | public class AfterCompileContext : ICompileContext 9 | { 10 | public CSharpCommandLineArguments Arguments { get; internal set; } 11 | 12 | public CSharpCompilation Compilation { get; internal set; } 13 | 14 | public Stream AssemblyStream { get; internal set; } 15 | 16 | public Stream SymbolStream { get; internal set; } 17 | 18 | public Stream XmlDocStream { get; internal set; } 19 | 20 | public IList Diagnostics { get; internal set; } 21 | } 22 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation/AppDomainHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StackExchange.Precompilation 4 | { 5 | /// 6 | /// Precompilation helper methods. 7 | /// 8 | public static class AppDomainHelper 9 | { 10 | /// 11 | /// The friednly name of the hosting the compilation. 12 | /// 13 | public const string CsCompilationAppDomainName = "csMoonSpeak"; 14 | 15 | /// 16 | /// 17 | /// 18 | /// Returns true if the is a Precompilation domain. 19 | public static bool IsPrecompilation(this AppDomain appDomain) 20 | { 21 | return (appDomain ?? AppDomain.CurrentDomain).FriendlyName == CsCompilationAppDomainName; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation/Attributes.cs: -------------------------------------------------------------------------------- 1 | [assembly:System.Runtime.CompilerServices.InternalsVisibleToAttribute("StackExchange.Precompiler")] 2 | [assembly:System.Runtime.CompilerServices.InternalsVisibleToAttribute("StackExchange.Precompilation.Build")] 3 | [assembly:System.Runtime.CompilerServices.InternalsVisibleToAttribute("StackExchange.Precompilation.MVC5")] -------------------------------------------------------------------------------- /StackExchange.Precompilation/BeforeCompileContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | 5 | namespace StackExchange.Precompilation 6 | { 7 | public class BeforeCompileContext : ICompileContext 8 | { 9 | public CSharpCommandLineArguments Arguments { get; set; } 10 | 11 | public CSharpCompilation Compilation { get; set; } 12 | 13 | public IList Diagnostics { get; internal set; } 14 | } 15 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation/CompileContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace StackExchange.Precompilation 6 | { 7 | 8 | internal class CompileContext 9 | { 10 | private readonly ICollection _modules; 11 | public BeforeCompileContext BeforeCompileContext { get; private set; } 12 | public AfterCompileContext AfterCompileContext { get; private set; } 13 | public CompileContext(ICollection modules) 14 | { 15 | _modules = modules; 16 | } 17 | public void Before(BeforeCompileContext context) 18 | { 19 | Apply(context, x => BeforeCompileContext = x, m => m.BeforeCompile); 20 | } 21 | public void After(AfterCompileContext context) 22 | { 23 | Apply(context, x => AfterCompileContext = x, m => m.AfterCompile); 24 | } 25 | private void Apply(TContext ctx, Action setter, Func> actionGetter) 26 | where TContext : ICompileContext 27 | { 28 | setter(ctx); 29 | foreach(var module in _modules) 30 | { 31 | try 32 | { 33 | var action = actionGetter(module); 34 | action(ctx); 35 | } 36 | catch (Exception ex) 37 | { 38 | var methodName = ctx is BeforeCompileContext ? nameof(ICompileModule.BeforeCompile) : nameof(ICompileModule.AfterCompile); 39 | throw new PrecompilationModuleException($"Precompilation module '{module.GetType().FullName}.{methodName}({typeof(TContext)})' failed", ex); 40 | } 41 | } 42 | } 43 | } 44 | 45 | internal class PrecompilationModuleException : Exception 46 | { 47 | public PrecompilationModuleException(string message, Exception inner) : base(message, inner) 48 | { 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation/CompileModuleElement.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | 3 | namespace StackExchange.Precompilation 4 | { 5 | /// 6 | /// A compile module configuration element. 7 | /// 8 | /// 9 | public class CompileModuleElement : ConfigurationElement 10 | { 11 | /// 12 | /// The type of the to be loaded at compile time. 13 | /// 14 | [ConfigurationProperty("type", IsRequired = true, DefaultValue = null)] 15 | public string Type { get { return (string)base["type"]; } } 16 | } 17 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation/CompileModulesCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | 3 | namespace StackExchange.Precompilation 4 | { 5 | /// 6 | /// A collection of instances. 7 | /// 8 | public class CompileModulesCollection : ConfigurationElementCollection 9 | { 10 | protected override ConfigurationElement CreateNewElement() 11 | { 12 | return new CompileModuleElement(); 13 | } 14 | 15 | protected override object GetElementKey(ConfigurationElement element) 16 | { 17 | return ((CompileModuleElement)element).Type; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation/CompiledFromDirectoryAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StackExchange.Precompilation 4 | { 5 | /// 6 | /// Decorates a precompiled MVC assembly. Used to calculate relative view paths. 7 | /// 8 | [AttributeUsage(AttributeTargets.Assembly)] 9 | public class CompiledFromDirectoryAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the source directory path. 13 | /// 14 | public string SourceDirectory { get; private set; } 15 | 16 | /// 17 | /// 18 | public CompiledFromDirectoryAttribute(string sourceDirectory) 19 | { 20 | SourceDirectory = sourceDirectory; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation/CompiledFromFileAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StackExchange.Precompilation 4 | { 5 | /// 6 | /// Decorates a precompiled MVC page. 7 | /// 8 | [AttributeUsage(AttributeTargets.Class)] 9 | public class CompiledFromFileAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the source file path. 13 | /// 14 | public string SourceFile { get; private set; } 15 | 16 | /// 17 | /// 18 | public CompiledFromFileAttribute(string sourceFile) 19 | { 20 | SourceFile = sourceFile; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /StackExchange.Precompilation/ICompileContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | 5 | namespace StackExchange.Precompilation 6 | { 7 | public interface ICompileContext 8 | { 9 | CSharpCommandLineArguments Arguments { get; } 10 | 11 | CSharpCompilation Compilation { get; } 12 | 13 | IList Diagnostics { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /StackExchange.Precompilation/ICompileModule.cs: -------------------------------------------------------------------------------- 1 | namespace StackExchange.Precompilation 2 | { 3 | /// 4 | /// Allows plugging into the compilation pipeline. Has to be registered in app/web.config 5 | /// 6 | /// 7 | /// 8 | /// 10 | /// 11 | ///
12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// ]]> 19 | /// 20 | /// 21 | /// 22 | public interface ICompileModule 23 | { 24 | /// 25 | /// Called before anything is emitted 26 | /// 27 | /// 28 | void BeforeCompile(BeforeCompileContext context); 29 | 30 | /// 31 | /// Called after the compilation is emitted. Changing the compilation will not have any effect at this point 32 | /// but the assembly can be changed before it is saved on disk or loaded into memory. 33 | /// 34 | /// 35 | void AfterCompile(AfterCompileContext context); 36 | } 37 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation/IMetadataReference.cs: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /StackExchange.Precompilation/PrecompilationModuleLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace StackExchange.Precompilation 6 | { 7 | internal class PrecompilationModuleLoader 8 | { 9 | /// Fires when a cannot be resolved to an actual . 10 | /// Register the handlers before touching . 11 | public event Action ModuleInitializationFailed; 12 | 13 | /// Gets a cached collection of loaded modules. 14 | public ICollection LoadedModules => _loadedModules.Value; 15 | 16 | private readonly Lazy> _loadedModules; 17 | private readonly PrecompilerSection _configuration; 18 | 19 | public PrecompilationModuleLoader(PrecompilerSection configuration) 20 | { 21 | _configuration = configuration; 22 | _loadedModules = new Lazy>(() => 23 | { 24 | var result = new List(); 25 | if (_configuration == null || _configuration.CompileModules == null) 26 | { 27 | return result; 28 | } 29 | 30 | foreach(var module in _configuration.CompileModules.Cast()) 31 | { 32 | try 33 | { 34 | var type = Type.GetType(module.Type, true); 35 | if (Activator.CreateInstance(type, true) is ICompileModule cm) 36 | { 37 | result.Add(cm); 38 | } 39 | else 40 | { 41 | throw new TypeLoadException($"{module.Type} is not an {nameof(ICompileModule)}."); 42 | } 43 | } 44 | catch(Exception ex) 45 | { 46 | ModuleInitializationFailed?.Invoke(module, ex); 47 | } 48 | } 49 | return result; 50 | }); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation/PrecompilerSection.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | 3 | namespace StackExchange.Precompilation 4 | { 5 | /// 6 | /// Defines the stackExchange.precompiler . 7 | /// 8 | /// 9 | public class PrecompilerSection : ConfigurationSection 10 | { 11 | /// 12 | /// Gets the . 13 | /// 14 | [ConfigurationProperty("modules", IsRequired = false)] 15 | [ConfigurationCollection(typeof(CompileModulesCollection))] 16 | public CompileModulesCollection CompileModules => (CompileModulesCollection)base["modules"]; 17 | 18 | /// 19 | /// Gets the stackExchange.precompiler section from the . 20 | /// 21 | public static PrecompilerSection Current => (PrecompilerSection)ConfigurationManager.GetSection("stackExchange.precompiler"); 22 | 23 | [ConfigurationProperty("razorCache", IsRequired = false)] 24 | public RazorCacheElement RazorCache => (RazorCacheElement)base["razorCache"]; 25 | 26 | } 27 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation/RazorCacheElement.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | 3 | namespace StackExchange.Precompilation 4 | { 5 | public class RazorCacheElement : ConfigurationElement 6 | { 7 | /// 8 | /// The type of the to be loaded at compile time. 9 | /// 10 | [ConfigurationProperty("directory", IsRequired = true, DefaultValue = null)] 11 | public string Directory => (string)base["directory"]; 12 | } 13 | } -------------------------------------------------------------------------------- /StackExchange.Precompilation/StackExchange.Precompilation.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net462;netstandard20 5 | Hooks into the ASP.NET MVC pipeline to enable usage of C# 7.2, and views precompiled with StackExchange.Precompilation.Build 6 | true 7 | true 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /StackExhcange.Precompilation.MVC5/Hacks.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using System; 4 | using System.Reflection; 5 | using System.Web.Mvc; 6 | 7 | namespace StackExchange.Precompilation 8 | { 9 | static class Hacks 10 | { 11 | private static readonly Action WebViewPage_OverridenLayoutPathSetter = 12 | (Action)typeof(WebViewPage) 13 | .GetProperty("OverridenLayoutPath", BindingFlags.Instance | BindingFlags.NonPublic) 14 | .SetMethod 15 | .CreateDelegate(typeof(Action)); 16 | 17 | /// 18 | /// Sets the WebViewPage.OverridenLayoutPath internal property, the only way to handle 19 | /// values. 20 | /// 21 | /// 22 | /// Using reflection to get a mis-spelled internal property setter and calling it via reflection. 23 | /// What could possibly go wrong? 24 | /// 25 | public static void SetOverriddenLayoutPath(WebViewPage webViewPage, string overridenLayoutPath) => 26 | WebViewPage_OverridenLayoutPathSetter.Invoke(webViewPage, overridenLayoutPath); 27 | 28 | /// 29 | /// Bear with me, so, in case a the view engine is being executed in a page targeting net45+ (including net46*) 30 | /// on a system that has net47+ installed, mscorlib contained referenced by BuildManager already 31 | /// contains ValueTuple, but due to this package referencing CodeAnalysis.Common, which also pulls in 32 | /// the System.ValueTuple package, that gets copied to /bin, and is therefore also picked up as a 33 | /// reference in build manager. 34 | /// This causes compilation.GetTypeByMetadataName("System.ValueTuple`2") to return null due to an 35 | /// ambigous match, resulting in the lovely CS8137 and CS8179 warnings, at runtime. 36 | /// The contents of the /bin directory get included due to the default ]]> entry. 37 | /// ]]> doesn't work since it fails to load the assembly generated for global.asax.cs 38 | /// ]]> doesn't work, since the wildcard takes preference 39 | /// https://referencesource.microsoft.com/#System.Web/Configuration/CompilationSection.cs,119d7e4aae57b4b6 40 | /// 41 | /// So when this is the case, we need to remove the reference to the System.ValueTuple.dll 42 | /// 43 | /// 44 | /// 45 | public static CSharpCompilation MakeValueTuplesWorkWhenRunningOn47RuntimeAndTargetingNet45Plus(CSharpCompilation compilation) 46 | { 47 | var mscorlibAssembly = typeof(object).Assembly; 48 | var valueTupleAssembly = typeof(ValueTuple).Assembly; 49 | if (mscorlibAssembly != valueTupleAssembly && 50 | compilation.GetAssemblyOrModuleSymbol(RoslynRazorViewEngine.ResolveReference(mscorlibAssembly)) is IAssemblySymbol mscorlib) 51 | { 52 | compilation = compilation.RemoveReferences(RoslynRazorViewEngine.ResolveReference(valueTupleAssembly)); 53 | } 54 | 55 | return compilation; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /StackExhcange.Precompilation.MVC5/PrecompilationView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Web.Mvc; 3 | using System.Web.WebPages; 4 | 5 | namespace StackExchange.Precompilation 6 | { 7 | internal class PrecompilationView : IView 8 | { 9 | private readonly string _virtualPath; 10 | private readonly string _masterPath; 11 | private readonly Type _viewType; 12 | private readonly ProfiledVirtualPathProviderViewEngine _viewEngine; 13 | private readonly bool _runViewStart; 14 | 15 | public PrecompilationView(string virtualPath, string masterPath, Type viewType, bool runViewStart, ProfiledVirtualPathProviderViewEngine viewEngine) 16 | { 17 | _virtualPath = virtualPath; 18 | _masterPath = masterPath; 19 | _viewType = viewType; 20 | _runViewStart = runViewStart; 21 | _viewEngine = viewEngine; 22 | } 23 | 24 | private WebPageBase CreatePage(ViewContext viewContext, System.IO.TextWriter writer, out WebPageContext pageContext, out WebPageRenderingBase startPage) 25 | { 26 | var basePage = (WebPageBase)Activator.CreateInstance(_viewType); 27 | basePage.VirtualPath = _virtualPath; 28 | basePage.VirtualPathFactory = _viewEngine.VirtualPathFactory; 29 | 30 | pageContext = new WebPageContext(viewContext.HttpContext, basePage, viewContext.ViewData?.Model); 31 | 32 | startPage = _runViewStart 33 | ? StartPage.GetStartPage(basePage, "_ViewStart", _viewEngine.FileExtensions) 34 | : null; 35 | 36 | var viewPage = basePage as WebViewPage; 37 | if (viewPage != null) 38 | { 39 | if (!string.IsNullOrEmpty(_masterPath)) 40 | { 41 | Hacks.SetOverriddenLayoutPath(viewPage, _masterPath); 42 | } 43 | 44 | viewPage.ViewContext = viewContext; 45 | viewPage.ViewData = viewContext.ViewData; 46 | viewPage.InitHelpers(); 47 | } 48 | 49 | return basePage; 50 | } 51 | 52 | public void Render(ViewContext viewContext, System.IO.TextWriter writer) 53 | { 54 | using (_viewEngine.DoProfileStep("Render")) 55 | { 56 | var webViewPage = CreatePage(viewContext, writer, out var pageContext, out var startPage); 57 | 58 | using (_viewEngine.DoProfileStep("ExecutePageHierarchy")) 59 | { 60 | webViewPage.ExecutePageHierarchy(pageContext, writer, startPage); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /StackExhcange.Precompilation.MVC5/PrecompilationVirtualPathFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Web.WebPages; 3 | 4 | namespace StackExchange.Precompilation 5 | { 6 | /// 7 | /// is used for resolving virtual paths once a view has 8 | /// been resolved. Setting it to an assumes that all underlying virtual paths 9 | /// are resolvable (layouts partials etc) are resolvable using the given instance. This can lead to some side 10 | /// effect when different view engines are combined, and the matching views are intermixed. 11 | /// 12 | /// 13 | /// This is our version of the , which is meant for extending the pipeline, 14 | /// mentioned above, but is not extendable in the sense that it always falls back to , 15 | /// which calls the old csc.exe in the framework dir, not the shiny one from our nuget package, 16 | /// and can thus cause unexpected behavior at runtime. 17 | /// 18 | internal class PrecompilationVirtualPathFactory : IVirtualPathFactory 19 | { 20 | private readonly PrecompiledViewEngine _precompiled; 21 | private readonly RoslynRazorViewEngine _runtime; 22 | 23 | public PrecompilationVirtualPathFactory(PrecompiledViewEngine precompiled = null, RoslynRazorViewEngine runtime = null) 24 | { 25 | _precompiled = precompiled; 26 | _runtime = runtime; 27 | } 28 | 29 | public object CreateInstance(string virtualPath) 30 | { 31 | if (_precompiled?.TryLookupCompiledType(virtualPath) is Type precompiledType) 32 | { 33 | return Activator.CreateInstance(precompiledType); 34 | } 35 | else if (_runtime?.GetTypeFromVirtualPath(virtualPath) is Type runtimeType) 36 | { 37 | return Activator.CreateInstance(runtimeType); 38 | } 39 | else 40 | { 41 | return null; 42 | } 43 | } 44 | 45 | public bool Exists(string virtualPath) 46 | { 47 | if (_precompiled?.TryLookupCompiledType(virtualPath) != null) 48 | { 49 | return true; 50 | } 51 | else if (_runtime?.FileExists(virtualPath) == true) 52 | { 53 | return true; 54 | } 55 | else 56 | { 57 | return false; 58 | } 59 | } 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /StackExhcange.Precompilation.MVC5/PrecompiledViewEngine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Web; 9 | using System.Web.Mvc; 10 | using System.Web.Routing; 11 | using System.Web.WebPages; 12 | 13 | namespace StackExchange.Precompilation 14 | { 15 | 16 | /// 17 | /// Supports loading of precompiled views. 18 | /// 19 | public class PrecompiledViewEngine : ProfiledVirtualPathProviderViewEngine 20 | { 21 | /// 22 | /// Gets the view paths 23 | /// 24 | public IEnumerable ViewPaths { get; private set; } 25 | 26 | private readonly Dictionary _views; 27 | 28 | /// 29 | /// Creates a new PrecompiledViewEngine instance, scanning all assemblies in for precompiled views. 30 | /// Precompiled views are types deriving from decorated with a 31 | /// 32 | /// The path to scan for assemblies with precompiled views. 33 | /// 34 | /// Use this constructor if you use aspnet_compiler.exe with it's targetDir parameter instead of StackExchange.Precompilation.Build. 35 | /// 36 | public PrecompiledViewEngine(string findAssembliesInPath) 37 | : this(FindViewAssemblies(findAssembliesInPath).ToArray()) 38 | { 39 | } 40 | 41 | /// 42 | /// Creates a new PrecompiledViewEngine instance, scanning the provided for precompiled views. 43 | /// Precompiled views are types deriving from decorated with a 44 | /// 45 | /// The assemblies to scan for precompiled views. 46 | public PrecompiledViewEngine(params Assembly[] assemblies) 47 | { 48 | AreaViewLocationFormats = new[] 49 | { 50 | "~/Areas/{2}/Views/{1}/{0}.cshtml", 51 | "~/Areas/{2}/Views/Shared/{0}.cshtml", 52 | }; 53 | AreaMasterLocationFormats = new[] 54 | { 55 | "~/Areas/{2}/Views/{1}/{0}.cshtml", 56 | "~/Areas/{2}/Views/Shared/{0}.cshtml", 57 | }; 58 | AreaPartialViewLocationFormats = new[] 59 | { 60 | "~/Areas/{2}/Views/{1}/{0}.cshtml", 61 | "~/Areas/{2}/Views/Shared/{0}.cshtml", 62 | }; 63 | FileExtensions = new[] 64 | { 65 | "cshtml", 66 | }; 67 | MasterLocationFormats = new[] 68 | { 69 | "~/Views/{1}/{0}.cshtml", 70 | "~/Views/Shared/{0}.cshtml", 71 | }; 72 | PartialViewLocationFormats = new[] 73 | { 74 | "~/Views/{1}/{0}.cshtml", 75 | "~/Views/Shared/{0}.cshtml", 76 | }; 77 | ViewLocationFormats = new[] 78 | { 79 | "~/Views/{1}/{0}.cshtml", 80 | "~/Views/Shared/{0}.cshtml", 81 | }; 82 | 83 | _views = new Dictionary(StringComparer.InvariantCultureIgnoreCase); 84 | 85 | foreach (var asm in assemblies) 86 | { 87 | // https://msdn.microsoft.com/en-us/library/system.reflection.assembly.gettypes(v=vs.110).aspx#Anchor_2 88 | Type[] asmTypes; 89 | try 90 | { 91 | asmTypes = asm.GetTypes(); 92 | } 93 | catch (ReflectionTypeLoadException thatsWhyWeCantHaveNiceThings) 94 | { 95 | asmTypes = thatsWhyWeCantHaveNiceThings.Types; 96 | } 97 | var viewTypes = asmTypes.Where(t => typeof(WebPageRenderingBase).IsAssignableFrom(t)).ToList(); 98 | 99 | var sourceDirectory = asm.GetCustomAttribute()?.SourceDirectory; 100 | 101 | foreach (var view in viewTypes) 102 | { 103 | var attr = view.GetCustomAttribute(); 104 | if (attr != null) 105 | { 106 | _views[MakeVirtualPath(attr.SourceFile, sourceDirectory)] = view; 107 | } 108 | } 109 | } 110 | 111 | ViewPaths = _views.Keys.OrderBy(_ => _).ToList(); 112 | } 113 | 114 | /// 115 | protected override IVirtualPathFactory CreateVirtualPathFactory() => new PrecompilationVirtualPathFactory( 116 | precompiled: this, 117 | runtime: ViewEngines.Engines.OfType().FirstOrDefault()); 118 | 119 | private static string MakeVirtualPath(string absoluteViewPath, string absoluteDirectoryPath = null) 120 | { 121 | if (absoluteDirectoryPath != null && absoluteViewPath.StartsWith(absoluteDirectoryPath)) 122 | { 123 | return MakeVirtualPath(absoluteViewPath, absoluteDirectoryPath.Length - (absoluteDirectoryPath.EndsWith("\\") ? 1 : 0)); 124 | } 125 | 126 | // we could just bail here, but let's make a best effort... 127 | var firstArea = absoluteViewPath.IndexOf(@"\Areas\"); 128 | if (firstArea != -1) 129 | { 130 | return MakeVirtualPath(absoluteViewPath, firstArea); 131 | } 132 | else 133 | { 134 | var firstView = absoluteViewPath.IndexOf(@"\Views\"); 135 | if (firstView == -1) throw new Exception("Couldn't make virtual for: " + absoluteViewPath); 136 | 137 | return MakeVirtualPath(absoluteViewPath, firstView); 138 | } 139 | } 140 | 141 | private static string MakeVirtualPath(string absoluteViewPath, int startIndex) 142 | { 143 | var tail = absoluteViewPath.Substring(startIndex); 144 | var vp = "~" + tail.Replace(@"\", "/"); 145 | 146 | return vp; 147 | } 148 | 149 | private static List FindViewAssemblies(string dirPath) 150 | { 151 | var pendingDirs = new List(); 152 | pendingDirs.Add(dirPath); 153 | 154 | var ret = new List(); 155 | 156 | while (pendingDirs.Count > 0) 157 | { 158 | var dir = pendingDirs[0]; 159 | pendingDirs.RemoveAt(0); 160 | 161 | pendingDirs.AddRange(Directory.EnumerateDirectories(dir)); 162 | 163 | var dlls = Directory.EnumerateFiles(dir, "*.dll").Where(w => Path.GetFileNameWithoutExtension(w).Contains("_Web_")); 164 | 165 | foreach (var dll in dlls) 166 | { 167 | try 168 | { 169 | var pdb = Path.Combine(Path.GetDirectoryName(dll), Path.GetFileNameWithoutExtension(dll) + ".pdb"); 170 | 171 | var asmBytes = File.ReadAllBytes(dll); 172 | var pdbBytes = File.Exists(pdb) ? File.ReadAllBytes(pdb) : null; 173 | 174 | Assembly asm; 175 | 176 | if (pdbBytes == null) 177 | { 178 | asm = Assembly.Load(asmBytes); 179 | } 180 | else 181 | { 182 | asm = Assembly.Load(asmBytes, pdbBytes); 183 | } 184 | 185 | ret.Add(asm); 186 | 187 | Debug.WriteLine("Loading view assembly: " + dll); 188 | } 189 | catch (Exception) { } 190 | } 191 | } 192 | 193 | return ret; 194 | } 195 | 196 | /// 197 | protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) => 198 | CreateViewImpl(partialPath, masterPath: null, runViewStart: false); 199 | 200 | /// 201 | protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) => 202 | CreateViewImpl(viewPath, masterPath, runViewStart: true); 203 | 204 | 205 | internal Type TryLookupCompiledType(string viewPath) 206 | { 207 | Type compiledView; 208 | if (!_views.TryGetValue(viewPath, out compiledView)) 209 | { 210 | return null; 211 | } 212 | 213 | return compiledView; 214 | } 215 | private IView CreateViewImpl(string viewPath, string masterPath, bool runViewStart) 216 | { 217 | var compiledType = TryLookupCompiledType(viewPath); 218 | if (compiledType == null) 219 | { 220 | return null; 221 | } 222 | 223 | return new PrecompilationView(viewPath, masterPath, compiledType, runViewStart, this); 224 | } 225 | 226 | /// 227 | public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache) 228 | { 229 | if (controllerContext == null) throw new ArgumentNullException(nameof(controllerContext)); 230 | if (string.IsNullOrEmpty(partialViewName)) throw new ArgumentException($"\"{nameof(partialViewName)}\" argument cannot be null or empty.", nameof(partialViewName)); 231 | 232 | var areaName = AreaHelpers.GetAreaName(controllerContext.RouteData); 233 | 234 | var locationsSearched = new List( 235 | DisplayModeProvider.Modes.Count * ( 236 | ((PartialViewLocationFormats?.Length ?? 0) + 237 | (areaName != null ? AreaPartialViewLocationFormats?.Length ?? 0 : 0))) 238 | ); 239 | 240 | var viewPath = ResolveViewPath(partialViewName, areaName, PartialViewLocationFormats, AreaPartialViewLocationFormats, locationsSearched, controllerContext); 241 | 242 | return string.IsNullOrEmpty(viewPath) 243 | ? new ViewEngineResult(locationsSearched) 244 | : new ViewEngineResult(CreatePartialView(controllerContext, viewPath), this); 245 | } 246 | 247 | /// 248 | public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) 249 | { 250 | // All this madness is essentially re-written from the VirtualPathProviderViewEngine class, but that class 251 | // checks on things like cache and whether the file exists and a whole bunch of stuff that's unnecessary. 252 | // Plus: unecessary allocations :( 253 | 254 | if (controllerContext == null) throw new ArgumentNullException(nameof(controllerContext)); 255 | if (string.IsNullOrEmpty(viewName)) throw new ArgumentException($"\"{nameof(viewName)}\" argument cannot be null or empty.", nameof(viewName)); 256 | 257 | var areaName = AreaHelpers.GetAreaName(controllerContext.RouteData); 258 | 259 | // minimize re-allocations of List 260 | var locationsSearched = new List( 261 | DisplayModeProvider.Modes.Count * ( 262 | ((ViewLocationFormats?.Length ?? 0) + 263 | (areaName != null ? AreaViewLocationFormats?.Length ?? 0 : 0)) + 264 | (MasterLocationFormats?.Length ?? 0) + 265 | (areaName != null ? AreaMasterLocationFormats?.Length ?? 0 : 0)) 266 | ); 267 | 268 | var viewPath = ResolveViewPath(viewName, areaName, ViewLocationFormats, AreaViewLocationFormats, locationsSearched, controllerContext); 269 | var masterPath = ResolveViewPath(masterName, areaName, MasterLocationFormats, AreaMasterLocationFormats, locationsSearched, controllerContext); 270 | 271 | if (string.IsNullOrEmpty(viewPath) || 272 | (string.IsNullOrEmpty(masterPath) && !string.IsNullOrEmpty(masterName))) 273 | { 274 | return new ViewEngineResult(locationsSearched); 275 | } 276 | 277 | return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this); 278 | } 279 | 280 | private string ResolveViewPath(string viewName, string areaName, string[] viewLocationFormats, string[] areaViewLocationFormats, List viewLocationsSearched, ControllerContext controllerContext) 281 | { 282 | if (string.IsNullOrEmpty(viewName)) 283 | { 284 | return null; 285 | } 286 | 287 | var controllerName = controllerContext.RouteData.GetRequiredString("controller"); 288 | if (IsSpecificPath(viewName)) 289 | { 290 | var normalized = NormalizeViewName(viewName); 291 | 292 | viewLocationsSearched.Add(viewName); 293 | return _views.ContainsKey(normalized) ? normalized : null; 294 | } 295 | 296 | areaViewLocationFormats = (areaName == null ? null : areaViewLocationFormats) ?? new string[0]; 297 | viewLocationFormats = viewLocationFormats ?? new string[0]; 298 | 299 | var httpContext = controllerContext.HttpContext; 300 | var availableDisplayModes = DisplayModeProvider.GetAvailableDisplayModesForContext(httpContext, controllerContext.DisplayMode); 301 | foreach (var displayMode in availableDisplayModes) 302 | { 303 | for (var i = 0; i < areaViewLocationFormats.Length; i++) 304 | { 305 | var path = string.Format(areaViewLocationFormats[i], viewName, controllerName, areaName); 306 | if (TryResolveView(httpContext, displayMode, ref path, viewLocationsSearched)) return path; 307 | } 308 | 309 | for (var i = 0; i < viewLocationFormats.Length; i++) 310 | { 311 | var path = string.Format(viewLocationFormats[i], viewName, controllerName); 312 | if (TryResolveView(httpContext, displayMode, ref path, viewLocationsSearched)) return path; 313 | } 314 | } 315 | 316 | return null; 317 | } 318 | 319 | private bool TryResolveView(HttpContextBase httpContext, IDisplayMode displayMode, ref string path, ICollection viewLocationsSearched) 320 | { 321 | path = NormalizeViewName(VirtualPathUtility.ToAppRelative(path)); // resolve relative path portions 322 | var displayInfo = displayMode.GetDisplayInfo(httpContext, path, _views.ContainsKey); 323 | 324 | if (displayInfo == null || displayInfo.FilePath == null) 325 | { 326 | viewLocationsSearched.Add(path); 327 | return false; 328 | } 329 | 330 | path = displayInfo.FilePath; 331 | return true; 332 | } 333 | 334 | private static string NormalizeViewName(string viewName) 335 | { 336 | return viewName[0] == '/' ? ("~" + viewName) : viewName; 337 | } 338 | 339 | private static bool IsSpecificPath(string path) => path.Length > 0 && (path[0] == '~' || path[0] == '/'); 340 | } 341 | 342 | // Hooray, another MVC5 class that should be public but ISN'T 343 | internal static class AreaHelpers 344 | { 345 | public static string GetAreaName(RouteBase route) 346 | { 347 | var routeWithArea = route as IRouteWithArea; 348 | if (routeWithArea != null) 349 | return routeWithArea.Area; 350 | 351 | var castRoute = route as Route; 352 | return castRoute?.DataTokens?["area"] as string; 353 | } 354 | 355 | public static string GetAreaName(RouteData routeData) 356 | { 357 | object area; 358 | if (routeData.DataTokens.TryGetValue("area", out area)) 359 | { 360 | return area as string; 361 | } 362 | 363 | return GetAreaName(routeData.Route); 364 | } 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /StackExhcange.Precompilation.MVC5/ProfiledVirtualPathProviderViewEngine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Web.Mvc; 3 | using System.Web.WebPages; 4 | 5 | namespace StackExchange.Precompilation 6 | { 7 | /// 8 | /// Base class for implementing derived types that provide custom profiling steps. 9 | /// 10 | public abstract class ProfiledVirtualPathProviderViewEngine : VirtualPathProviderViewEngine 11 | { 12 | /// 13 | /// Triggers when the engine performs a step that can be profiled. 14 | /// 15 | public Func ProfileStep { get; set; } 16 | 17 | internal IVirtualPathFactory VirtualPathFactory => _virtualPathFactoryFactory.Value; 18 | 19 | protected abstract IVirtualPathFactory CreateVirtualPathFactory(); 20 | 21 | private readonly Lazy _virtualPathFactoryFactory; // sorry, I had to... 22 | 23 | /// 24 | protected ProfiledVirtualPathProviderViewEngine() 25 | { 26 | _virtualPathFactoryFactory = new Lazy(CreateVirtualPathFactory); 27 | } 28 | } 29 | 30 | internal static class ProfileVirtualPathProviderViewEngineExtensions 31 | { 32 | /// 33 | /// Invokes the if it's set. 34 | /// 35 | public static IDisposable DoProfileStep(this ProfiledVirtualPathProviderViewEngine instance, string name) => 36 | instance?.ProfileStep?.Invoke(name); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /StackExhcange.Precompilation.MVC5/RazorParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.CodeDom; 4 | using System.CodeDom.Compiler; 5 | using System.Collections.Concurrent; 6 | using System.IO; 7 | using System.Security.Cryptography; 8 | using System.Web.Configuration; 9 | using System.Web.Razor; 10 | using System.Web.WebPages.Razor; 11 | using System.Web.WebPages.Razor.Configuration; 12 | using Microsoft.CodeAnalysis; 13 | using System.Threading.Tasks; 14 | using System.Threading; 15 | using Microsoft.CodeAnalysis.Text; 16 | using RazorWorker = System.Func>; 17 | using System.Composition; 18 | using Microsoft.CodeAnalysis.Host; 19 | using Microsoft.CodeAnalysis.Host.Mef; 20 | 21 | namespace StackExchange.Precompilation 22 | { 23 | [ExportLanguageServiceFactoryAttribute(typeof(IDocumentExtender), LanguageNames.CSharp)] 24 | class RazorParserFactory : ILanguageServiceFactory 25 | { 26 | public ILanguageService CreateLanguageService(HostLanguageServices languageServices) => 27 | new RazorParser(languageServices.WorkspaceServices.Workspace); 28 | } 29 | 30 | class RazorParser : IDocumentExtender, IDisposable 31 | { 32 | private readonly Workspace _workspace; 33 | private readonly WebConfigurationFileMap _configMap; 34 | private readonly DirectoryInfo _cacheDirectory; 35 | private readonly BlockingCollection _workItems; 36 | private readonly Lazy _backgroundWorkers; 37 | private readonly CancellationToken _cancellationToken; 38 | 39 | public RazorParser(Workspace workspace) 40 | { 41 | var dir = PrecompilerSection.Current?.RazorCache?.Directory; 42 | // dir = string.IsNullOrWhiteSpace(dir) 43 | // ? Environment.GetEnvironmentVariable(nameof(Precompilation) + "_" + nameof(PrecompilerSection.RazorCache) + nameof(RazorCacheElement.Directory)) 44 | // : dir; 45 | // if (!string.IsNullOrWhiteSpace(dir) && ! dir exists) 46 | // Directory.CreateDirectory(Path.Combine(CscArgs.OutputDirectory, dir)) 47 | 48 | // if (cacheDirectory.Exists != true) 49 | // { 50 | // throw new ArgumentException($"Specified directory '{cacheDirectory.FullName}' doesn't exist.", nameof(cacheDirectory)); 51 | // } 52 | 53 | _workItems = new BlockingCollection(); 54 | _workspace = workspace; 55 | _configMap = new WebConfigurationFileMap { VirtualDirectories = { { "/", new VirtualDirectoryMapping(Environment.CurrentDirectory, true) } } }; 56 | _backgroundWorkers = new Lazy( 57 | () => _cancellationToken.IsCancellationRequested 58 | ? Task.CompletedTask 59 | : Task.WhenAll(Enumerable.Range(0, Environment.ProcessorCount).Select(_ => Task.Run(BackgroundWorker, _cancellationToken))), 60 | LazyThreadSafetyMode.ExecutionAndPublication); 61 | } 62 | 63 | private async Task BackgroundWorker() 64 | { 65 | foreach(var loader in _workItems.GetConsumingEnumerable(_cancellationToken)) 66 | { 67 | if (_cancellationToken.IsCancellationRequested) 68 | { 69 | loader.Result.SetCanceled(); 70 | continue; 71 | } 72 | try 73 | { 74 | var originalText = await loader.OriginalLoader.LoadTextAndVersionAsync(_workspace, null, default(CancellationToken)); 75 | var viewFullPath = originalText.FilePath; 76 | var viewVirtualPath = GetRelativeUri(originalText.FilePath, Environment.CurrentDirectory); 77 | var viewConfig = WebConfigurationManager.OpenMappedWebConfiguration(_configMap, viewVirtualPath); 78 | var host = viewConfig.GetSectionGroup("system.web.webPages.razor") is RazorWebSectionGroup razorConfig 79 | ? WebRazorHostFactory.CreateHostFromConfig(razorConfig, viewVirtualPath, viewFullPath) 80 | : WebRazorHostFactory.CreateDefaultHost(viewVirtualPath, viewFullPath); 81 | 82 | // having this as a field would require the ASP.NET MVC dependency even for console apps... 83 | RazorWorker worker = RazorWorker; 84 | if (_cacheDirectory != null) 85 | { 86 | worker = CachedRazorWorker; 87 | } 88 | 89 | using (var stream = await worker(host, originalText)) 90 | { 91 | var generatedText = TextAndVersion.Create( 92 | SourceText.From(stream, originalText.Text.Encoding, originalText.Text.ChecksumAlgorithm, canBeEmbedded: originalText.Text.CanBeEmbedded, throwIfBinaryDetected: true), 93 | originalText.Version, 94 | originalText.FilePath); 95 | 96 | loader.Result.TrySetResult(generatedText); 97 | } 98 | } 99 | catch (Exception ex) 100 | { 101 | loader.Result.TrySetException(ex); 102 | } 103 | } 104 | } 105 | 106 | void IDisposable.Dispose() 107 | { 108 | _workItems?.Dispose(); 109 | } 110 | 111 | private async Task CachedRazorWorker(RazorEngineHost host, TextAndVersion originalText) 112 | { 113 | var cacheFile = GetCachedFileInfo(); 114 | if (cacheFile.Exists) 115 | { 116 | return cacheFile.OpenRead(); 117 | } 118 | else 119 | { 120 | (var success, var source) = await RazorWorkerImpl(host, originalText); 121 | 122 | FileStream fs = null; 123 | try 124 | { 125 | if (success) 126 | { 127 | fs = cacheFile.Create(); 128 | await source.CopyToAsync(fs, 4096, _cancellationToken); 129 | await fs.FlushAsync(_cancellationToken); 130 | } 131 | } 132 | catch (Exception ex) 133 | { 134 | ReportDiagnostic(Diagnostic.Create(Compilation.CachingFailed, Location.None, originalText.FilePath, cacheFile.FullName, ex)); 135 | for (var i = 0; i < 10 && cacheFile.Exists; i++) 136 | { 137 | await Task.Delay(100 * i); 138 | try { cacheFile.Delete(); } catch { } 139 | } 140 | if (cacheFile.Exists) 141 | { 142 | ReportDiagnostic(Diagnostic.Create(Compilation.CachingFailedHard, Location.None, originalText.FilePath, cacheFile.FullName)); 143 | } 144 | } 145 | finally 146 | { 147 | fs?.Dispose(); 148 | source.Position = 0; 149 | } 150 | 151 | return source; // return the in-memory stream, since it's faster 152 | } 153 | 154 | 155 | FileInfo GetCachedFileInfo() 156 | { 157 | using (var md5 = MD5.Create()) 158 | using (var str = new MemoryStream()) 159 | using (var sw = new StreamWriter(str)) 160 | { 161 | 162 | // all those things can affect the generated c# 163 | // so we need to include them in the hash... 164 | sw.WriteLine(host.CodeLanguage.LanguageName); 165 | sw.WriteLine(host.CodeLanguage.CodeDomProviderType.FullName); 166 | sw.WriteLine(host.DefaultBaseClass); 167 | sw.WriteLine(host.DefaultClassName); 168 | sw.WriteLine(host.DefaultNamespace); 169 | sw.WriteLine(string.Join(";",host.NamespaceImports)); 170 | sw.WriteLine(host.StaticHelpers); 171 | sw.WriteLine(host.TabSize); 172 | sw.WriteLine(originalText.FilePath); 173 | originalText.Text.Write(sw, _cancellationToken); // .cshtml content 174 | 175 | sw.Flush(); 176 | str.Position = 0; 177 | var hashBytes = md5.ComputeHash(str); 178 | var fileName = BitConverter.ToString(hashBytes).Replace("-","") + ".cs"; 179 | var filePath = Path.Combine(_cacheDirectory.FullName, fileName); 180 | return new FileInfo(filePath); 181 | } 182 | } 183 | } 184 | private readonly ConcurrentBag _diagnostics = new ConcurrentBag(); 185 | private void ReportDiagnostic(Diagnostic d) => _diagnostics.Add(d); 186 | 187 | private Task RazorWorker(RazorEngineHost host, TextAndVersion originalText) => 188 | RazorWorkerImpl(host, originalText).ContinueWith(x => x.Result.result, TaskContinuationOptions.OnlyOnRanToCompletion); 189 | 190 | private Task<(bool success, Stream result)> RazorWorkerImpl(RazorEngineHost host, TextAndVersion originalText) 191 | { 192 | var success = true; 193 | var generatedStream = new MemoryStream(capacity: originalText.Text.Length * 8); // generated .cs files contain a lot of additional crap vs actualy cshtml 194 | var viewFullPath = originalText.FilePath; 195 | using (var sourceReader = new StreamReader(generatedStream, originalText.Text.Encoding, false, 4096, leaveOpen: true)) 196 | using (var provider = CodeDomProvider.CreateProvider("csharp")) 197 | using (var generatedWriter = new StreamWriter(generatedStream, originalText.Text.Encoding, 4096, leaveOpen: true)) 198 | { 199 | // write cshtml into generated stream and rewind 200 | originalText.Text.Write(generatedWriter); 201 | generatedWriter.Flush(); 202 | generatedStream.Position = 0; 203 | 204 | // generated code and clear memory stream 205 | var engine = new RazorTemplateEngine(host); 206 | var razorOut = engine.GenerateCode(sourceReader, null, null, viewFullPath); 207 | 208 | if (!razorOut.Success) 209 | { 210 | success = false; 211 | foreach(var error in razorOut.ParserErrors) 212 | { 213 | var position = new LinePosition(error.Location.LineIndex, error.Location.CharacterIndex - 1); 214 | ReportDiagnostic(Diagnostic.Create( 215 | Compilation.RazorParserError, 216 | Location.Create( 217 | originalText.FilePath, 218 | new TextSpan(error.Location.AbsoluteIndex, error.Length), 219 | new LinePositionSpan(position, position)), 220 | error.Message)); 221 | } 222 | } 223 | 224 | // add the CompiledFromFileAttribute to the generated class 225 | razorOut.GeneratedCode 226 | .Namespaces.OfType().FirstOrDefault()? 227 | .Types.OfType().FirstOrDefault()? 228 | .CustomAttributes.Add( 229 | new CodeAttributeDeclaration( 230 | new CodeTypeReference(typeof(CompiledFromFileAttribute)), 231 | new CodeAttributeArgument(new CodePrimitiveExpression(viewFullPath)) 232 | )); 233 | 234 | // reuse the memory stream for code generation 235 | generatedStream.Position = 0; 236 | generatedStream.SetLength(0); 237 | var codeGenOptions = new CodeGeneratorOptions { VerbatimOrder = true, ElseOnClosing = false, BlankLinesBetweenMembers = false }; 238 | provider.GenerateCodeFromCompileUnit(razorOut.GeneratedCode, generatedWriter, codeGenOptions); 239 | 240 | // rewind 241 | generatedWriter.Flush(); 242 | generatedStream.Position = 0; 243 | } 244 | 245 | return Task.FromResult((success: success, stream: (Stream)generatedStream)); 246 | } 247 | 248 | private string GetRelativeUri(string filespec, string folder) 249 | { 250 | Uri pathUri = new Uri(filespec); 251 | if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString())) 252 | { 253 | folder += Path.DirectorySeparatorChar; 254 | } 255 | Uri folderUri = new Uri(folder); 256 | return "/" + folderUri.MakeRelativeUri(pathUri).ToString().TrimStart('/'); 257 | } 258 | 259 | public DocumentInfo Extend(DocumentInfo document) 260 | { 261 | if (Path.GetExtension(document.Name) != ".cshtml") return document; 262 | 263 | var razorLoader = new RazorTextLoader(this, document.TextLoader); 264 | _workItems.Add(razorLoader); 265 | return document.WithTextLoader(razorLoader); 266 | } 267 | 268 | public async Task> Complete() 269 | { 270 | _workItems.CompleteAdding(); 271 | if (_backgroundWorkers.IsValueCreated || !_workItems.IsCompleted) 272 | { 273 | await _backgroundWorkers.Value; 274 | } 275 | return _diagnostics.ToList(); 276 | } 277 | 278 | private Task EnsureWorkers() => _backgroundWorkers.Value; 279 | 280 | private sealed class RazorTextLoader : TextLoader 281 | { 282 | public TextLoader OriginalLoader { get; } 283 | public TaskCompletionSource Result { get; } 284 | 285 | private readonly RazorParser _parser; 286 | 287 | public RazorTextLoader(RazorParser parser, TextLoader originalLoader) 288 | { 289 | _parser = parser; 290 | OriginalLoader = originalLoader; 291 | Result = new TaskCompletionSource(); 292 | } 293 | 294 | private Task _worker; 295 | public override Task LoadTextAndVersionAsync(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken) 296 | { 297 | _worker = _worker ?? _parser.EnsureWorkers(); // ensuring that lazy workers are running 298 | return Result.Task; 299 | } 300 | } 301 | } 302 | } -------------------------------------------------------------------------------- /StackExhcange.Precompilation.MVC5/RoslynRazorViewEngine.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.Emit; 4 | using Microsoft.CodeAnalysis.Text; 5 | using System; 6 | using System.CodeDom; 7 | using System.CodeDom.Compiler; 8 | using System.Collections.Concurrent; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Reflection; 13 | using System.Threading; 14 | using System.Web; 15 | using System.Web.Compilation; 16 | using System.Web.Hosting; 17 | using System.Web.Mvc; 18 | using System.Web.Razor; 19 | using System.Web.Razor.Generator; 20 | using System.Web.Razor.Parser.SyntaxTree; 21 | using System.Web.WebPages; 22 | using System.Web.WebPages.Razor; 23 | 24 | namespace StackExchange.Precompilation 25 | { 26 | /// 27 | /// A replacement for the that uses roslyn () instead of to compile views. 28 | /// 29 | public class RoslynRazorViewEngine : ProfiledVirtualPathProviderViewEngine 30 | { 31 | /// 32 | /// Creates a new instance. 33 | /// 34 | public RoslynRazorViewEngine() 35 | { 36 | AreaViewLocationFormats = new[] { 37 | "~/Areas/{2}/Views/{1}/{0}.cshtml", 38 | "~/Areas/{2}/Views/Shared/{0}.cshtml" 39 | }; 40 | 41 | AreaMasterLocationFormats = new[] { 42 | "~/Areas/{2}/Views/{1}/{0}.cshtml", 43 | "~/Areas/{2}/Views/Shared/{0}.cshtml" 44 | }; 45 | 46 | AreaPartialViewLocationFormats = new[] { 47 | "~/Areas/{2}/Views/{1}/{0}.cshtml", 48 | "~/Areas/{2}/Views/Shared/{0}.cshtml" 49 | }; 50 | ViewLocationFormats = new[] { 51 | "~/Views/{1}/{0}.cshtml", 52 | "~/Views/Shared/{0}.cshtml" 53 | }; 54 | MasterLocationFormats = new[] { 55 | "~/Views/{1}/{0}.cshtml", 56 | "~/Views/Shared/{0}.cshtml" 57 | }; 58 | PartialViewLocationFormats = new[] { 59 | "~/Views/{1}/{0}.cshtml", 60 | "~/Views/Shared/{0}.cshtml" 61 | }; 62 | FileExtensions = new[] { 63 | "cshtml" 64 | }; 65 | } 66 | 67 | /// When set to true, configured s are used when the views are compiled 68 | public bool UseCompilationModules { get; set; } 69 | 70 | private readonly ICompileModule[] _noModule = new ICompileModule[0]; 71 | private readonly PrecompilationModuleLoader _moduleLoader = new PrecompilationModuleLoader(PrecompilerSection.Current); 72 | 73 | /// 74 | protected override IVirtualPathFactory CreateVirtualPathFactory() => new PrecompilationVirtualPathFactory( 75 | runtime: this, 76 | precompiled: ViewEngines.Engines.OfType().FirstOrDefault()); 77 | 78 | /// 79 | protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) => 80 | new PrecompilationView(partialPath, null, GetTypeFromVirtualPath(partialPath), false, this); 81 | 82 | /// 83 | protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) => 84 | new PrecompilationView(viewPath, masterPath, GetTypeFromVirtualPath(viewPath), true, this); 85 | 86 | internal bool FileExists(string virtualPath) => 87 | HostingEnvironment.VirtualPathProvider.FileExists(virtualPath); 88 | 89 | internal Type GetTypeFromVirtualPath(string virtualPath) 90 | { 91 | virtualPath = VirtualPathUtility.ToAbsolute(virtualPath); 92 | 93 | var cacheKey = "RoslynRazor_" + virtualPath; 94 | var type = HttpRuntime.Cache[cacheKey] as Type; 95 | if (type == null) 96 | { 97 | type = GetTypeFromVirtualPathNoCache(virtualPath); 98 | 99 | // Cache it, and make it dependent on the razor file 100 | var cacheDependency = HostingEnvironment.VirtualPathProvider.GetCacheDependency(virtualPath, new string[] { virtualPath }, DateTime.UtcNow); 101 | HttpRuntime.Cache.Insert(cacheKey, type, cacheDependency); 102 | } 103 | 104 | return type; 105 | } 106 | 107 | private Type GetTypeFromVirtualPathNoCache(string virtualPath) 108 | { 109 | using (this.DoProfileStep($"{nameof(RoslynRazorViewEngine)}: Compiling {virtualPath}")) 110 | { 111 | OnCodeGenerationStarted(); 112 | var args = new CompilingPathEventArgs(virtualPath, WebRazorHostFactory.CreateHostFromConfig(virtualPath)); 113 | OnBeforeCompilePath(args); 114 | var host = args.Host; 115 | var razorResult = RunRazorGenerator(virtualPath, host); 116 | var syntaxTree = GetSyntaxTree(host, razorResult); 117 | var assembly = CompileToAssembly(host, syntaxTree, UseCompilationModules ? _moduleLoader.LoadedModules : _noModule); 118 | return assembly.GetType($"{host.DefaultNamespace}.{host.DefaultClassName}"); 119 | } 120 | } 121 | 122 | private GeneratorResults RunRazorGenerator(string virtualPath, WebPageRazorHost host) 123 | { 124 | var file = HostingEnvironment.VirtualPathProvider.GetFile(virtualPath); 125 | var engine = new RazorTemplateEngine(host); 126 | using (var viewStream = file.Open()) 127 | using (var viewReader = new StreamReader(viewStream)) 128 | { 129 | var razorResult = engine.GenerateCode(viewReader, className: null, rootNamespace: null, sourceFileName: host.PhysicalPath); 130 | if (!razorResult.Success) 131 | { 132 | var sourceCode = (string)null; 133 | if (viewStream.CanSeek) 134 | { 135 | viewStream.Seek(0, SeekOrigin.Begin); 136 | sourceCode = viewReader.ReadToEnd(); 137 | } 138 | throw CreateExceptionFromParserError(razorResult.ParserErrors.Last(), virtualPath, sourceCode); 139 | } 140 | OnCodeGenerationCompleted(razorResult.GeneratedCode, host); 141 | return razorResult; 142 | } 143 | } 144 | 145 | private static SyntaxTree GetSyntaxTree(WebPageRazorHost host, GeneratorResults razorResult) 146 | { 147 | // Use CodeDom to generate source code from the CodeCompileUnit 148 | // Use roslyn to parse it back 149 | using (var codeDomProvider = CodeDomProvider.CreateProvider(host.CodeLanguage.LanguageName)) 150 | using (var viewCodeStream = new MemoryStream()) 151 | using (var viewCodeWriter = new StreamWriter(viewCodeStream)) 152 | { 153 | codeDomProvider.GenerateCodeFromCompileUnit(razorResult.GeneratedCode, viewCodeWriter, new CodeGeneratorOptions()); 154 | viewCodeWriter.Flush(); 155 | viewCodeStream.Position = 0; 156 | var sourceText = SourceText.From(viewCodeStream); 157 | 158 | // We need a different file path for the generated file, otherwise breakpoints won't get 159 | // hit due to #line directives pointing at the original .cshtml. If we'd want breakpoint 160 | // in the generated .cs code, we'd have to dump them somewhere on disk, and specify the path here. 161 | var sourcePath = string.IsNullOrEmpty(host.PhysicalPath) 162 | ? host.VirtualPath // yay virtual paths, won't point at the original file 163 | : Path.ChangeExtension(host.PhysicalPath, ".roslynviewengine"); 164 | return SyntaxFactory.ParseSyntaxTree(sourceText, path: sourcePath); 165 | } 166 | } 167 | 168 | // we were getting OutOfMemory exceptions caused by MetadataReference.CreateFrom* creating 169 | // System.Reflection.PortableExecutable.PEReader instances for the same assembly for each view being compiled 170 | private static readonly ConcurrentDictionary> ReferenceCache = new ConcurrentDictionary>(); 171 | internal static MetadataReference ResolveReference(Assembly assembly) 172 | { 173 | var key = assembly.Location; 174 | Uri uri; 175 | if (Uri.TryCreate(assembly.CodeBase, UriKind.Absolute, out uri) && uri.IsFile) 176 | { 177 | key = uri.LocalPath; 178 | } 179 | return ReferenceCache.GetOrAdd( 180 | key, 181 | loc => new Lazy( 182 | () => MetadataReference.CreateFromFile(loc), 183 | LazyThreadSafetyMode.ExecutionAndPublication)).Value; 184 | } 185 | 186 | private static Assembly CompileToAssembly(WebPageRazorHost host, SyntaxTree syntaxTree, ICollection compilationModules) 187 | { 188 | var strArgs = new List(); 189 | strArgs.Add("/target:library"); 190 | strArgs.Add(host.DefaultDebugCompilation ? "/o-" : "/o+"); 191 | strArgs.Add(host.DefaultDebugCompilation ? "/debug+" : "/debug-"); 192 | 193 | var cscArgs = CSharpCommandLineParser.Default.Parse(strArgs, null, null); 194 | 195 | var compilation = CSharpCompilation.Create( 196 | "RoslynRazor", // Note: using a fixed assembly name, which doesn't matter as long as we don't expect cross references of generated assemblies 197 | new[] { syntaxTree }, 198 | BuildManager.GetReferencedAssemblies().OfType().Select(ResolveReference), 199 | cscArgs.CompilationOptions.WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default)); 200 | 201 | compilation = Hacks.MakeValueTuplesWorkWhenRunningOn47RuntimeAndTargetingNet45Plus(compilation); 202 | var diagnostics = new List(); 203 | var context = new CompileContext(compilationModules); 204 | context.Before(new BeforeCompileContext 205 | { 206 | Arguments = cscArgs, 207 | Compilation = compilation, 208 | Diagnostics = diagnostics, 209 | }); 210 | compilation = context.BeforeCompileContext.Compilation; 211 | 212 | using (var dllStream = new MemoryStream()) 213 | using (var pdbStream = new MemoryStream()) 214 | { 215 | EmitResult emitResult = compilation.Emit(dllStream, pdbStream); 216 | diagnostics.AddRange(emitResult.Diagnostics); 217 | 218 | if (!emitResult.Success) 219 | { 220 | Diagnostic diagnostic = diagnostics.First(x => x.Severity == DiagnosticSeverity.Error); 221 | string message = diagnostic.ToString(); 222 | LinePosition linePosition = diagnostic.Location.GetMappedLineSpan().StartLinePosition; 223 | 224 | throw new HttpParseException(message, null, host.VirtualPath, null, linePosition.Line + 1); 225 | } 226 | 227 | context.After(new AfterCompileContext 228 | { 229 | Arguments = context.BeforeCompileContext.Arguments, 230 | AssemblyStream = dllStream, 231 | Compilation = compilation, 232 | Diagnostics = diagnostics, 233 | SymbolStream = pdbStream, 234 | XmlDocStream = null, 235 | }); 236 | 237 | return Assembly.Load(dllStream.GetBuffer(), pdbStream.GetBuffer()); 238 | } 239 | } 240 | 241 | private static HttpParseException CreateExceptionFromParserError(RazorError error, string virtualPath, string sourceCode) => 242 | new HttpParseException(error.Message + Environment.NewLine, null, virtualPath, sourceCode, error.Location.LineIndex + 1); 243 | 244 | /// 245 | /// This is the equivalent of the event, since bypasses completely. 246 | /// 247 | public static event EventHandler CompilingPath; 248 | 249 | /// 250 | /// Raises the event. 251 | /// 252 | /// 253 | protected virtual void OnBeforeCompilePath(CompilingPathEventArgs args) => 254 | CompilingPath?.Invoke(this, args); 255 | 256 | /// 257 | /// This is the equivalent of the event, since bypasses completely. 258 | /// 259 | public static event EventHandler CodeGenerationStarted; 260 | 261 | private void OnCodeGenerationStarted() => 262 | CodeGenerationStarted?.Invoke(this, EventArgs.Empty); 263 | 264 | /// 265 | /// This is the equivalent of the event, since bypasses completely. 266 | /// 267 | public static event EventHandler CodeGenerationCompleted; 268 | 269 | private void OnCodeGenerationCompleted(CodeCompileUnit codeCompileUnit, WebPageRazorHost host) => 270 | CodeGenerationCompleted?.Invoke(this, new CodeGenerationCompleteEventArgs(host.VirtualPath, host.PhysicalPath, codeCompileUnit)); 271 | } 272 | } 273 | 274 | -------------------------------------------------------------------------------- /StackExhcange.Precompilation.MVC5/StackExchange.Precompilation.MVC5.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net462 5 | Hooks into the ASP.NET MVC and StackExchange.Precompilation.Build pipeline to enable usage of C# 7.2, and views precompiled with StackExchange.Precompilation.Build 6 | true 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Test.ConsoleApp/AliasTest.cs: -------------------------------------------------------------------------------- 1 | #if NET462 2 | extern alias aliastest; 3 | 4 | namespace Test.ConsoleApp 5 | { 6 | class AliasTest 7 | { 8 | public const string DataSet = nameof(aliastest::System.Data.DataSet); 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /Test.ConsoleApp/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /Test.ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using Test.Module; 4 | 5 | namespace Test.ConsoleApp 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | Console.WriteLine(PathMapTest().Dump()); 12 | #if NET462 13 | Console.WriteLine(typeof(AliasTest).FullName); 14 | #endif 15 | } 16 | 17 | // path mapping test, configured via property in the .csproj 18 | static string PathMapTest([CallerFilePath] string path = null) => 19 | path.StartsWith("X:\\Test\\") 20 | ? path 21 | : throw new InvalidOperationException($"CallerFilePath was expected to start with X:\\Test\\ but was {path}."); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Test.ConsoleApp/Test.ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | PackageReference 5 | Exe 6 | net462;netcoreapp20 7 | win10-x64 8 | $(SolutionDir)StackExchange.Precompilation.Build\bin\$(Configuration)\net462\ 9 | $(SolutionDir)=X:\Test\ 10 | portable 11 | 12 | 13 | 14 | {5fcaecc3-787b-473f-a372-783d0c235190} 15 | Test.Module 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Test.Module/Test.Module.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net462;netstandard20 5 | 6 | 7 | 8 | {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 9 | StackExchange.Precompilation 10 | 11 | 12 | -------------------------------------------------------------------------------- /Test.Module/TestCompileModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using StackExchange.Precompilation; 7 | 8 | namespace Test.Module 9 | { 10 | public class TestCompileModule : ICompileModule 11 | { 12 | public void BeforeCompile(BeforeCompileContext context) 13 | { 14 | // this can potentially run multiple times (for every view compiled at runtime) in RoslynRazorViewEngine; 15 | if(context.Compilation.GetTypeByMetadataName("Test.Module.Extensions") != null) return; 16 | 17 | context.Diagnostics.Add( 18 | Diagnostic.Create( 19 | new DiagnosticDescriptor("TEST", "TEST", "Hello meta programming world!", "TEST", DiagnosticSeverity.Info, true), 20 | Location.None)); 21 | 22 | context.Compilation = context.Compilation.AddSyntaxTrees( 23 | SyntaxFactory.ParseSyntaxTree(@" 24 | namespace Test.Module 25 | { 26 | public static class Extensions 27 | { 28 | public static T Dump(this T i) 29 | { 30 | if (i != null) 31 | { 32 | System.Console.WriteLine(i); 33 | } 34 | return i; 35 | } 36 | } 37 | } 38 | ", context.Arguments.ParseOptions)); 39 | } 40 | 41 | public void AfterCompile(AfterCompileContext context) 42 | { 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Test.WebApp.ExternalViews/App_Code/Helpers.cshtml: -------------------------------------------------------------------------------- 1 | @helper Test() 2 | { 3 |
HELPERS
4 | } 5 | -------------------------------------------------------------------------------- /Test.WebApp.ExternalViews/ExternalViews.cs: -------------------------------------------------------------------------------- 1 | namespace Test.WebApp 2 | { 3 | public class ExternalViews 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Test.WebApp.ExternalViews/Test.WebApp.ExternalViews.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | Properties 6 | Test.WebApp 7 | net462 8 | portable 9 | 10 | 11 | 12 | {31dfcccc-2f44-405e-a2d7-bb1ac718e7b9} 13 | StackExchange.Precompilation.Build 14 | 15 | 16 | {3c0a90f1-b19e-4305-ab71-3cd31c7d0f4d} 17 | StackExchange.Precompilation 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | $(SolutionDir)StackExchange.Precompilation.Build\bin\$(Configuration)\$(TargetFramework)\ 28 | 29 | true 30 | 31 | 32 | -------------------------------------------------------------------------------- /Test.WebApp.ExternalViews/Views/Shared/ExternalPartial.cshtml: -------------------------------------------------------------------------------- 1 | ExternalPartial.cshtml 2 | -------------------------------------------------------------------------------- /Test.WebApp.ExternalViews/Views/Shared/ExternalView.cshtml: -------------------------------------------------------------------------------- 1 | ExternalView.cshtml 2 | @{ Html.RenderPartial("ExternalPartial"); } -------------------------------------------------------------------------------- /Test.WebApp.ExternalViews/Views/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Test.WebApp/Content/PartialExternalContent.cshtml: -------------------------------------------------------------------------------- 1 | Partial External Content -------------------------------------------------------------------------------- /Test.WebApp/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web.Mvc; 5 | using StackExchange.Precompilation; 6 | 7 | namespace Test.WebApp.Controllers 8 | { 9 | public class HomeController : Controller 10 | { 11 | public ActionResult Index() 12 | { 13 | IEnumerable viewPaths; 14 | #if DEBUG 15 | viewPaths = new string[] { "We don't keep track of the views in the RoslynRazorViewEngine." }; 16 | #else 17 | var viewEngine = ViewEngines.Engines.OfType().Single(); 18 | viewPaths = viewEngine.ViewPaths; 19 | #endif 20 | 21 | return View(new Models.SampleModel { ViewPaths = viewPaths }); 22 | } 23 | 24 | public ActionResult IndexOverridden() 25 | { 26 | return new ViewResult 27 | { 28 | ViewName = "~/Views/Home/Index.cshtml", 29 | MasterName = "~/Views/Shared/_Layout.Overridden.cshtml", 30 | ViewData = new ViewDataDictionary(new Models.SampleModel { ViewPaths = new [] { "OVERRIDDDDDEN" } }), 31 | }; 32 | } 33 | 34 | public ActionResult ExcludedLayout() => View(); 35 | } 36 | } -------------------------------------------------------------------------------- /Test.WebApp/Models/SampleModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Test.WebApp.Models 4 | { 5 | public class SampleModel 6 | { 7 | public IEnumerable ViewPaths { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Test.WebApp/MvcApplication.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using System.Web.Mvc; 3 | using System.Web.Routing; 4 | using StackExchange.Precompilation; 5 | using Test.WebApp.Controllers; 6 | 7 | namespace Test.WebApp 8 | { 9 | public static class MvcApplicationInitializer 10 | { 11 | public static void PreApplicationStart() => 12 | System.Web.UI.PageParser.DefaultApplicationBaseType = typeof(MvcApplication); 13 | } 14 | 15 | public class MvcApplication : HttpApplication 16 | { 17 | public static bool IsDebug => 18 | #if DEBUG 19 | true; 20 | #else 21 | false; 22 | #endif 23 | 24 | protected void Application_Start() 25 | { 26 | AreaRegistration.RegisterAllAreas(); 27 | GlobalFilters.Filters.Add(new HandleErrorAttribute()); 28 | 29 | RouteTable.Routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 30 | RouteTable.Routes.MapRoute( 31 | name: "Default", 32 | url: "{controller}/{action}/{id}", 33 | defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 34 | ); 35 | 36 | ViewEngines.Engines.Clear(); 37 | #if !DEBUG 38 | // use precompiled engine first (supports some C# 6), 39 | ViewEngines.Engines.Add(new PrecompiledViewEngine(typeof(HomeController).Assembly, typeof(ExternalViews).Assembly)); 40 | #endif 41 | // fallback to RoslynRazorViewEngine (RazorViewEngine doesn't support C# 6). 42 | ViewEngines.Engines.Add(new RoslynRazorViewEngine() { UseCompilationModules = true }); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Test.WebApp/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using System.Web; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("Test.WebApp")] 10 | [assembly: AssemblyDescription("")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("")] 13 | [assembly: AssemblyProduct("Test.WebApp")] 14 | [assembly: AssemblyCopyright("Copyright © 2015")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | 18 | // Setting ComVisible to false makes the types in this assembly not visible 19 | // to COM components. If you need to access a type in this assembly from 20 | // COM, set the ComVisible attribute to true on that type. 21 | [assembly: ComVisible(false)] 22 | 23 | // The following GUID is for the ID of the typelib if this project is exposed to COM 24 | [assembly: Guid("09bb37e2-0d7e-472d-b74c-aa02018c89c6")] 25 | 26 | // Version information for an assembly consists of the following four values: 27 | // 28 | // Major Version 29 | // Minor Version 30 | // Build Number 31 | // Revision 32 | // 33 | // You can specify all the values or you can default the Revision and Build Numbers 34 | // by using the '*' as shown below: 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | [assembly: PreApplicationStartMethod(typeof(Test.WebApp.MvcApplicationInitializer), nameof(Test.WebApp.MvcApplicationInitializer.PreApplicationStart))] 38 | 39 | -------------------------------------------------------------------------------- /Test.WebApp/Test.WebApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | PackageReference 6 | Debug 7 | AnyCPU 8 | 9 | 10 | 2.0 11 | {5B0105A4-256B-4A88-852C-6F5E9D185515} 12 | {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} 13 | Library 14 | Properties 15 | Test.WebApp 16 | Test.WebApp 17 | net462 18 | v4.6.2 19 | false 20 | true 21 | 22 | 23 | 24 | 25 | ..\ 26 | 27 | 28 | win 29 | false 30 | 31 | 32 | true 33 | full 34 | false 35 | bin\ 36 | DEBUG;TRACE 37 | prompt 38 | 4 39 | 40 | 41 | pdbonly 42 | true 43 | bin\ 44 | TRACE 45 | prompt 46 | 4 47 | 48 | 49 | 50 | {3C0A90F1-B19E-4305-AB71-3CD31C7D0F4D} 51 | StackExchange.Precompilation 52 | 53 | 54 | 55 | {2ba24772-f7b0-4652-a430-2f4c2262e882} 56 | Test.WebApp.ExternalViews 57 | 58 | 59 | {5fcaecc3-787b-473f-a372-783d0c235190} 60 | Test.Module 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | True 87 | 88 | 89 | 90 | 91 | 92 | 93 | $(SolutionDir)StackExchange.Precompilation.Build\bin\$(Configuration)\$(TargetFramework)\ 94 | 95 | true 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Test.WebApp/Views/Home/ExcludedLayout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | @{ 3 | Layout = "~/Views/Shared/_Layout.Excluded.cshtml"; 4 | } 5 | 6 |

ExcludedLayout

7 | 8 | -------------------------------------------------------------------------------- /Test.WebApp/Views/Home/Index.Mobile.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleModel 2 | @{ 3 | ViewBag.Title = "Test Page"; 4 | } 5 | Mobile Precompiled View Paths: 6 |
    7 | @foreach (var path in Model.ViewPaths) 8 | { 9 |
  • @path
  • 10 | } 11 |
12 | 13 | @Html.EditorForModel() 14 | 15 | @Helpers.Test() 16 | -------------------------------------------------------------------------------- /Test.WebApp/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleModel 2 | @{ 3 | ViewBag.Title = "Test Page"; 4 | } 5 | Precompiled View Paths: 6 |
    7 | @foreach (var path in Model.ViewPaths) 8 | { 9 |
  • @path
  • 10 | } 11 |
12 | 13 | @Html.EditorForModel() 14 | @Html.Partial("../Other/../Other/RelativePartial") 15 | @Helpers.Test() 16 | 17 | @if (!MvcApplication.IsDebug) 18 | { 19 | @Html.Partial("ExternalPartial"); 20 | } 21 | 22 | @{ 23 | // c# 7 features 24 | 25 | 26 | (string title, string url) GetTitle() => ("ValueTuple", "`2"); 27 | @GetTitle().Item1 28 | } -------------------------------------------------------------------------------- /Test.WebApp/Views/Other/RelativePartial.cshtml: -------------------------------------------------------------------------------- 1 | RelativePartial.cshtml 2 | @{ 3 | @* 4 | code bellow is a repro case for a Razer parser errors, that were only caught 5 | in RoslynRazorViewEngine but not during precompilation... 6 | 7 | to repro use `@foreach` instead of `foreach` 8 | *@ 9 | 10 | foreach(var x in new []{1,2,3}) 11 | { 12 | @:@x, 13 | } 14 | } -------------------------------------------------------------------------------- /Test.WebApp/Views/Shared/EditorTemplates/SampleModel.Mobile.cshtml: -------------------------------------------------------------------------------- 1 | @model Test.WebApp.Models.SampleModel 2 |
3 | EditorTemplates.Mobile 4 | @using (Html.BeginForm()) 5 | { 6 | @Html.AntiForgeryToken() 7 |
8 | @Html.ValidationSummary(true, "", new { @class = "text-danger" }) 9 | @Html.EditorFor(m => m.ViewPaths) 10 |
11 |
12 | 13 |
14 |
15 |
16 | } 17 |
18 | -------------------------------------------------------------------------------- /Test.WebApp/Views/Shared/EditorTemplates/SampleModel.cshtml: -------------------------------------------------------------------------------- 1 | @model Test.WebApp.Models.SampleModel 2 |
3 | EditorTemplates 4 | @using (Html.BeginForm()) 5 | { 6 | @Html.AntiForgeryToken() 7 |
8 | @Html.ValidationSummary(true, "", new { @class = "text-danger" }) 9 | @Html.EditorFor(m => m.ViewPaths) 10 |
11 |
12 | 13 |
14 |
15 |
16 | } 17 |
18 | -------------------------------------------------------------------------------- /Test.WebApp/Views/Shared/EditorTemplates/String.cshtml: -------------------------------------------------------------------------------- 1 | @model System.String 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /Test.WebApp/Views/Shared/_Footer.Mobile.cshtml: -------------------------------------------------------------------------------- 1 | @model DateTime 2 |
3 |

© @Model.Year Mobile Partial Footer

4 |
5 | -------------------------------------------------------------------------------- /Test.WebApp/Views/Shared/_Footer.cshtml: -------------------------------------------------------------------------------- 1 | @model DateTime 2 |
3 |

© @Model.Year Partial Footer

4 |
5 | -------------------------------------------------------------------------------- /Test.WebApp/Views/Shared/_Layout.Excluded.cshtml: -------------------------------------------------------------------------------- 1 | Layout 2 | 3 | @RenderBody() 4 | 5 | /Layout -------------------------------------------------------------------------------- /Test.WebApp/Views/Shared/_Layout.Mobile.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | @ViewBag.Title - Test.WebApp - Mobile 6 | 7 | 8 |

Mobile

9 |
10 |
11 | @RenderBody() 12 |
13 |
14 | @Html.Partial("_Footer", DateTime.Now) 15 | @Html.Partial("~/Content/PartialExternalContent.cshtml") 16 | 17 | 18 | -------------------------------------------------------------------------------- /Test.WebApp/Views/Shared/_Layout.Overridden.cshtml: -------------------------------------------------------------------------------- 1 | 

OVERRIDDDDDEN

2 |

@ViewBag.Title - Test.WebApp

3 | @RenderBody() 4 |

/OVERRIDDDDDEN

5 | -------------------------------------------------------------------------------- /Test.WebApp/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | @ViewBag.Title - Test.WebApp 6 | 7 | 8 |
9 | @RenderBody() 10 |
11 |
12 | @Html.ActionLink("Overridden layout", "IndexOverridden", new { controller = "Home".Dump() }) 13 | @Html.ActionLink("Excluded layout", "ExcludedLayout", new { controller = "Home" }) 14 | @Html.Partial("_Footer", DateTime.Now) 15 | @Html.Partial("~/Content/PartialExternalContent.cshtml") 16 | 17 | 18 | -------------------------------------------------------------------------------- /Test.WebApp/Views/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Test.WebApp/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "~/Views/Shared/_Layout.cshtml"; 3 | } 4 | _ViewStart 5 |
6 | -------------------------------------------------------------------------------- /Test.WebApp/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | image: Visual Studio 2017 3 | assembly_info: 4 | patch: true 5 | file: '**\AssemblyInfo.*' 6 | assembly_version: $(semver) 7 | assembly_file_version: $(semver).{build} 8 | assembly_informational_version: '{version}' 9 | init: 10 | - git config --global core.autocrlf input 11 | install: 12 | - ps: >- 13 | set -name semver -scope global -value (get-content .\semver.txt) 14 | 15 | $env:semver = $semver 16 | 17 | if ("$env:appveyor_repo_tag_name" -ne "releases/$semver") { 18 | $env:package_suffix = "-alpha$env:appveyor_build_number" } 19 | 20 | update-appveyorbuild -Version "$env:semver$env:package_suffix" 21 | nuget: 22 | disable_publish_on_pr: true 23 | build_script: 24 | - ps: .\BuildAndPack.ps1 -VersionSuffix "$env:package_suffix" -GitCommitId "$env:appveyor_repo_commit" -MsBuildArgs @(, "/logger:C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll") -CIBuild 25 | skip_branch_with_pr: true 26 | skip_tags: false 27 | skip_commits: 28 | files: 29 | - '**/*.md' 30 | artifacts: 31 | - path: packages/obj/*.nupkg 32 | deploy: 33 | - provider: NuGet 34 | name: alpha 35 | on: 36 | branch: master 37 | server: https://www.myget.org/F/stackoverflow/api/v2 38 | api_key: 39 | secure: P/UHxq2DEs0GI1SoDXDesHjRVsSVgdywz5vmsnhFQQY5aJgO3kP+QfhwfhXz19Rw 40 | symbol_server: https://www.myget.org/F/stackoverflow/symbols/api/v2/package 41 | - provider: NuGet 42 | name: release 43 | on: 44 | appveyor_repo_tag: true 45 | server: https://www.myget.org/F/stackoverflow/api/v2 46 | api_key: 47 | secure: P/UHxq2DEs0GI1SoDXDesHjRVsSVgdywz5vmsnhFQQY5aJgO3kP+QfhwfhXz19Rw 48 | symbol_server: https://www.myget.org/F/stackoverflow/symbols/api/v2/package 49 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stack Exchange 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /semver.txt: -------------------------------------------------------------------------------- 1 | 5.1.0 2 | --------------------------------------------------------------------------------