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

ExcludedLayout

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

© @Model.Year Mobile Partial Footer

================================================ FILE: Test.WebApp/Views/Shared/_Footer.cshtml ================================================ @model DateTime

© @Model.Year Partial Footer

================================================ FILE: Test.WebApp/Views/Shared/_Layout.Excluded.cshtml ================================================ Layout @RenderBody() /Layout ================================================ FILE: Test.WebApp/Views/Shared/_Layout.Mobile.cshtml ================================================  @ViewBag.Title - Test.WebApp - Mobile

Mobile


@RenderBody()
@Html.Partial("_Footer", DateTime.Now) @Html.Partial("~/Content/PartialExternalContent.cshtml") ================================================ FILE: Test.WebApp/Views/Shared/_Layout.Overridden.cshtml ================================================ 

OVERRIDDDDDEN

@ViewBag.Title - Test.WebApp

@RenderBody()

/OVERRIDDDDDEN

================================================ FILE: Test.WebApp/Views/Shared/_Layout.cshtml ================================================  @ViewBag.Title - Test.WebApp
@RenderBody()
@Html.ActionLink("Overridden layout", "IndexOverridden", new { controller = "Home".Dump() }) @Html.ActionLink("Excluded layout", "ExcludedLayout", new { controller = "Home" }) @Html.Partial("_Footer", DateTime.Now) @Html.Partial("~/Content/PartialExternalContent.cshtml") ================================================ FILE: Test.WebApp/Views/Web.config ================================================ 
================================================ FILE: Test.WebApp/Views/_ViewStart.cshtml ================================================ @{ Layout = "~/Views/Shared/_Layout.cshtml"; } _ViewStart
================================================ FILE: Test.WebApp/Web.config ================================================ 
================================================ FILE: Test.WebApp.ExternalViews/App_Code/Helpers.cshtml ================================================ @helper Test() {
HELPERS
} ================================================ FILE: Test.WebApp.ExternalViews/ExternalViews.cs ================================================ namespace Test.WebApp { public class ExternalViews { } } ================================================ FILE: Test.WebApp.ExternalViews/Test.WebApp.ExternalViews.csproj ================================================  Library Properties Test.WebApp net462 portable {31dfcccc-2f44-405e-a2d7-bb1ac718e7b9} StackExchange.Precompilation.Build {3c0a90f1-b19e-4305-ab71-3cd31c7d0f4d} StackExchange.Precompilation $(SolutionDir)StackExchange.Precompilation.Build\bin\$(Configuration)\$(TargetFramework)\ true ================================================ FILE: Test.WebApp.ExternalViews/Views/Shared/ExternalPartial.cshtml ================================================ ExternalPartial.cshtml ================================================ FILE: Test.WebApp.ExternalViews/Views/Shared/ExternalView.cshtml ================================================ ExternalView.cshtml @{ Html.RenderPartial("ExternalPartial"); } ================================================ FILE: Test.WebApp.ExternalViews/Views/Web.config ================================================ 
================================================ FILE: appveyor.yml ================================================ version: '{build}' image: Visual Studio 2017 assembly_info: patch: true file: '**\AssemblyInfo.*' assembly_version: $(semver) assembly_file_version: $(semver).{build} assembly_informational_version: '{version}' init: - git config --global core.autocrlf input install: - ps: >- set -name semver -scope global -value (get-content .\semver.txt) $env:semver = $semver if ("$env:appveyor_repo_tag_name" -ne "releases/$semver") { $env:package_suffix = "-alpha$env:appveyor_build_number" } update-appveyorbuild -Version "$env:semver$env:package_suffix" nuget: disable_publish_on_pr: true build_script: - ps: .\BuildAndPack.ps1 -VersionSuffix "$env:package_suffix" -GitCommitId "$env:appveyor_repo_commit" -MsBuildArgs @(, "/logger:C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll") -CIBuild skip_branch_with_pr: true skip_tags: false skip_commits: files: - '**/*.md' artifacts: - path: packages/obj/*.nupkg deploy: - provider: NuGet name: alpha on: branch: master server: https://www.myget.org/F/stackoverflow/api/v2 api_key: secure: P/UHxq2DEs0GI1SoDXDesHjRVsSVgdywz5vmsnhFQQY5aJgO3kP+QfhwfhXz19Rw symbol_server: https://www.myget.org/F/stackoverflow/symbols/api/v2/package - provider: NuGet name: release on: appveyor_repo_tag: true server: https://www.myget.org/F/stackoverflow/api/v2 api_key: secure: P/UHxq2DEs0GI1SoDXDesHjRVsSVgdywz5vmsnhFQQY5aJgO3kP+QfhwfhXz19Rw symbol_server: https://www.myget.org/F/stackoverflow/symbols/api/v2/package ================================================ FILE: license.txt ================================================ The MIT License (MIT) Copyright (c) 2015 Stack Exchange Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: semver.txt ================================================ 5.1.0