diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationFailedException.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationFailedException.cs index 5a58d7fc50..871e958d62 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationFailedException.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationFailedException.cs @@ -4,38 +4,70 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNet.FileSystems; namespace Microsoft.AspNet.Mvc.Razor { + /// + /// An exception thrown when accessing the result of a failed compilation. + /// public class CompilationFailedException : Exception { - public CompilationFailedException(IEnumerable messages, string generatedCode) + /// + /// Instantiates a new instance of . + /// + /// The path of the Razor source file that was compiled. + /// The contents of the Razor source file. + /// The generated C# content that was compiled. + /// A sequence of encountered + /// during compilation. + public CompilationFailedException( + [NotNull] string filePath, + [NotNull] string fileContent, + [NotNull] string compiledContent, + [NotNull] IEnumerable messages) : base(FormatMessage(messages)) { + FilePath = filePath; + FileContent = fileContent; + CompiledContent = compiledContent; Messages = messages.ToList(); - GeneratedCode = generatedCode; } - public string GeneratedCode { get; private set; } + /// + /// Gets the path of the Razor source file that produced the compilation failure. + /// + public string FilePath { get; private set; } + /// + /// Gets a sequence of instances encountered during compilation. + /// public IEnumerable Messages { get; private set; } - public string CompilationSource - { - get { return GeneratedCode; } - } + /// + /// Gets the content of the Razor source file. + /// + public string FileContent { get; private set; } + /// + /// Gets the generated C# content that was compiled. + /// + public string CompiledContent { get; private set; } + + /// public override string Message { get { - return "Compilation Failed:" + FormatMessage(Messages); + return Resources.FormatCompilationFailed(FilePath) + + Environment.NewLine + + FormatMessage(Messages); } } private static string FormatMessage(IEnumerable messages) { - return String.Join(Environment.NewLine, messages); + return string.Join(Environment.NewLine, messages); } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationMessage.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationMessage.cs index 352a80bfd4..a99e80d4c9 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationMessage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationMessage.cs @@ -3,15 +3,30 @@ namespace Microsoft.AspNet.Mvc.Razor { + /// + /// Represents a message encountered during compilation. + /// public class CompilationMessage { + /// + /// Initializes a with the specified message. + /// + /// A message produced from compilation. public CompilationMessage(string message) { Message = message; } + /// + /// Gets a message produced from compilation. + /// public string Message { get; private set; } + /// + /// Returns a representation of this instance of . + /// + /// A representing this instance. + /// Returns same value as . public override string ToString() { return Message; diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs index 400cf64e18..7fbcac81b2 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs @@ -3,46 +3,145 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.IO; +using Microsoft.AspNet.FileSystems; namespace Microsoft.AspNet.Mvc.Razor { + /// + /// Represents the result of compilation. + /// public class CompilationResult { - private readonly Type _type; + private Type _type; - private CompilationResult(string generatedCode, Type type, IEnumerable messages) + private CompilationResult() { - _type = type; - GeneratedCode = generatedCode; - Messages = messages.ToList(); } - public IEnumerable Messages { get; private set; } - - public string GeneratedCode { get; private set; } + /// + /// Gets the path of the Razor file that was compiled. + /// + public string FilePath + { + get + { + if (File != null) + { + return File.PhysicalPath; + } + return null; + } + } + /// + /// Gets a sequence of instances encountered during compilation. + /// + public IEnumerable Messages { get; private set; } + + /// + /// Gets additional information from compilation. + /// + /// + /// In the event of a compilation failure, values from this dictionary are copied to the + /// property of the thrown. + /// + public IDictionary AdditionalInfo + { + get; private set; + } + + /// + /// Gets the generated C# content that was compiled. + /// + public string CompiledContent { get; private set; } + + /// + /// Gets the type produced as a result of compilation. + /// + /// An error occured during compilation. public Type CompiledType { get { if (_type == null) { - throw new CompilationFailedException(Messages, GeneratedCode); + throw CreateCompilationFailedException(); } return _type; } } - public static CompilationResult Failed(string generatedCode, IEnumerable messages) + private IFileInfo File { get; set; } + + /// + /// Creates a that represents a failure in compilation. + /// + /// The for the Razor file that was compiled. + /// The generated C# content to be compiled. + /// The sequence of failure messages encountered during compilation. + /// Additional info about the compilation. + /// A CompilationResult instance representing a failure. + public static CompilationResult Failed([NotNull] IFileInfo file, + [NotNull] string compilationContent, + [NotNull] IEnumerable messages, + IDictionary additionalInfo) { - return new CompilationResult(generatedCode, type: null, messages: messages); + return new CompilationResult + { + File = file, + CompiledContent = compilationContent, + Messages = messages, + AdditionalInfo = additionalInfo + }; } - public static CompilationResult Successful(string generatedCode, Type type) + /// + /// Creates a that represents a success in compilation. + /// + /// The compiled type. + /// A CompilationResult instance representing a success. + public static CompilationResult Successful([NotNull] Type type) { - return new CompilationResult(generatedCode, type, Enumerable.Empty()); + return new CompilationResult + { + _type = type + }; + } + + private CompilationFailedException CreateCompilationFailedException() + { + var fileContent = ReadContent(File); + var exception = new CompilationFailedException(FilePath, fileContent, CompiledContent, Messages); + if (AdditionalInfo != null) + { + foreach (var item in AdditionalInfo) + { + exception.Data.Add(item.Key, item.Value); + } + } + + return exception; + } + + private static string ReadContent(IFileInfo file) + { + try + { + using (var stream = file.CreateReadStream()) + { + using (var reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } + catch (IOException) + { + // Don't throw if reading the file fails. + return string.Empty; + } } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs index bdc1884c0e..619b815235 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.Razor return result; } - return CompilationResult.Successful(generatedCode: null, type: compiledType); + return CompilationResult.Successful(compiledType); } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilationService.cs index 90618f40ee..730ca831a5 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/ICompilationService.cs @@ -1,12 +1,23 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Threading.Tasks; +using Microsoft.AspNet.FileSystems; namespace Microsoft.AspNet.Mvc.Razor { + /// + /// Provides methods for compilation of a Razor page. + /// public interface ICompilationService { - CompilationResult Compile(string content); + /// + /// Compiles content and returns the result of compilation. + /// + /// The for the Razor file that was compiled. + /// The generated C# content to be compiled. + /// + /// A representing the result of compilation. + /// + CompilationResult Compile(IFileInfo fileInfo, string compilationContent); } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs index dc8edffc5d..e176f9afd3 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs @@ -7,15 +7,22 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Text; +using Microsoft.AspNet.FileSystems; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; using Microsoft.Framework.Runtime; namespace Microsoft.AspNet.Mvc.Razor.Compilation { + /// + /// A type that uses Roslyn to compile C# content. + /// public class RoslynCompilationService : ICompilationService { + public static readonly string CompilationResultDiagnosticsKey = "Diagnostics"; private static readonly ConcurrentDictionary _metadataFileCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); @@ -23,6 +30,12 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation private readonly IApplicationEnvironment _environment; private readonly IAssemblyLoaderEngine _loader; + /// + /// Initalizes a new instance of the class. + /// + /// The environment for the executing application. + /// The loader used to load compiled assemblies. + /// The library manager that provides export and reference information. public RoslynCompilationService(IApplicationEnvironment environment, IAssemblyLoaderEngine loaderEngine, ILibraryManager libraryManager) @@ -32,9 +45,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation _libraryManager = libraryManager; } - public CompilationResult Compile(string content) + /// + public CompilationResult Compile(IFileInfo fileInfo, string compilationContent) { - var syntaxTrees = new[] { CSharpSyntaxTree.ParseText(content) }; + var sourceText = SourceText.From(compilationContent, Encoding.UTF8); + var syntaxTrees = new[] { CSharpSyntaxTree.ParseText(sourceText, path: fileInfo.PhysicalPath) }; var targetFramework = _environment.TargetFramework; var references = GetApplicationReferences(); @@ -70,7 +85,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation .Select(d => GetCompilationMessage(formatter, d)) .ToList(); - return CompilationResult.Failed(content, messages); + var additionalInfo = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { CompilationResultDiagnosticsKey, result.Diagnostics } + }; + return CompilationResult.Failed(fileInfo, compilationContent, messages, additionalInfo); } Assembly assembly; @@ -89,7 +108,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation var type = assembly.GetExportedTypes() .First(); - return CompilationResult.Successful(string.Empty, type); + return CompilationResult.Successful(type); } } } @@ -135,7 +154,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation }); } - private CompilationMessage GetCompilationMessage(DiagnosticFormatter formatter, Diagnostic diagnostic) + private static CompilationMessage GetCompilationMessage(DiagnosticFormatter formatter, Diagnostic diagnostic) { return new CompilationMessage(formatter.Format(diagnostic)); } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index 21ff13315a..6380f8cc82 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -26,6 +26,22 @@ namespace Microsoft.AspNet.Mvc.Razor return GetString("ArgumentCannotBeNullOrEmpty"); } + /// + /// Compilation for '{0}' failed: + /// + internal static string CompilationFailed + { + get { return GetString("CompilationFailed"); } + } + + /// + /// Compilation for '{0}' failed: + /// + internal static string FormatCompilationFailed(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CompilationFailed"), p0); + } + /// /// The layout view '{0}' could not be located. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs index 85231041c1..20945a83e8 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.Razor private readonly string _appRoot; public RazorCompilationService(IApplicationEnvironment environment, - ICompilationService compilationService, + ICompilationService compilationService, IMvcRazorHost razorHost) { _environment = environment; @@ -49,10 +49,10 @@ namespace Microsoft.AspNet.Mvc.Razor if (!results.Success) { var messages = results.ParserErrors.Select(e => new CompilationMessage(e.Message)); - throw new CompilationFailedException(messages, results.GeneratedCode); + return CompilationResult.Failed(file, results.GeneratedCode, messages, additionalInfo: null); } - return _baseCompilationService.Compile(results.GeneratedCode); + return _baseCompilationService.Compile(file, results.GeneratedCode); } private static string EnsureTrailingSlash([NotNull]string path) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index 767a9ac8df..bae45587d0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -120,6 +120,9 @@ The value cannot be null or empty. + + Compilation for '{0}' failed: + The layout view '{0}' could not be located. diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilationResultTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilationResultTest.cs new file mode 100644 index 0000000000..94a75ca5be --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilationResultTest.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.AspNet.FileSystems; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor.Test +{ + public class CompilationResultTest + { + [Fact] + public void FailedResult_ThrowsWhenAccessingCompiledType() + { + // Arrange + var expected = +@"Compilation for 'myfile' failed: +hello +world"; + var originalContent = "Original file content"; + var fileInfo = new Mock(); + fileInfo.SetupGet(f => f.PhysicalPath) + .Returns("myfile"); + var contentBytes = Encoding.UTF8.GetBytes(originalContent); + fileInfo.Setup(f => f.CreateReadStream()) + .Returns(new MemoryStream(contentBytes)); + var messages = new[] + { + new CompilationMessage("hello"), + new CompilationMessage("world") + }; + var additionalInfo = new Dictionary + { + { "key", "value" } + }; + var result = CompilationResult.Failed(fileInfo.Object, + "

hello world

", + messages, + additionalInfo); + + // Act and Assert + var ex = Assert.Throws(() => result.CompiledType); + Assert.Equal(expected, ex.Message); + Assert.Equal("value", ex.Data["key"]); + Assert.Equal(originalContent, ex.FileContent); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj b/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj index f6d31ead62..9d7e6cc0c2 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Microsoft.AspNet.Mvc.Razor.Test.kproj @@ -21,6 +21,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs index c64bb3e077..fc43e1e07f 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs @@ -27,14 +27,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test host.Setup(h => h.GenerateCode(@"views\index\home.cshtml", It.IsAny())) .Returns(new GeneratorResults(new Block(new BlockBuilder { Type = BlockType.Comment }), new RazorError[0], new CodeBuilderResult("", new LineMapping[0]))) .Verifiable(); - var compiler = new Mock(); - compiler.Setup(c => c.Compile(It.IsAny())) - .Returns(CompilationResult.Successful("", typeof(RazorCompilationServiceTest))); - var razorService = new RazorCompilationService(env.Object, compiler.Object, host.Object); var fileInfo = new Mock(); fileInfo.Setup(f => f.PhysicalPath).Returns(viewPath); fileInfo.Setup(f => f.CreateReadStream()).Returns(Stream.Null); + var compiler = new Mock(); + compiler.Setup(c => c.Compile(fileInfo.Object, It.IsAny())) + .Returns(CompilationResult.Successful(typeof(RazorCompilationServiceTest))); + var razorService = new RazorCompilationService(env.Object, compiler.Object, host.Object); // Act razorService.CompileCore(fileInfo.Object);