diff --git a/src/Microsoft.AspNet.Razor/Generator/CodeGeneratorContext.cs b/src/Microsoft.AspNet.Razor/Generator/CodeGeneratorContext.cs
index 773caa828b..5babf29f67 100644
--- a/src/Microsoft.AspNet.Razor/Generator/CodeGeneratorContext.cs
+++ b/src/Microsoft.AspNet.Razor/Generator/CodeGeneratorContext.cs
@@ -23,6 +23,11 @@ namespace Microsoft.AspNet.Razor.Generator
public CodeTreeBuilder CodeTreeBuilder { get; set; }
+ ///
+ /// Gets or sets the SHA1 based checksum for the file whose location is defined by .
+ ///
+ public string Checksum { get; set; }
+
public static CodeGeneratorContext Create(RazorEngineHost host, string className, string rootNamespace, string sourceFile, bool shouldGenerateLinePragmas)
{
return new CodeGeneratorContext()
diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeBuilder.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeBuilder.cs
index 4e307f0f9a..2857915e87 100644
--- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeBuilder.cs
+++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpCodeBuilder.cs
@@ -10,6 +10,8 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
{
public class CSharpCodeBuilder : CodeBuilder
{
+ // See http://msdn.microsoft.com/en-us/library/system.codedom.codechecksumpragma.checksumalgorithmid.aspx
+ private const string Sha1AlgorithmId = "{ff1816ec-aa5e-4d10-87f7-6f4963833460}";
private const int DisableAsyncWarning = 1998;
public CSharpCodeBuilder(CodeGeneratorContext context)
@@ -24,6 +26,17 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
{
var writer = new CSharpCodeWriter();
+ if (!Host.DesignTimeMode && !string.IsNullOrEmpty(Context.Checksum))
+ {
+ writer.Write("#pragma checksum \"")
+ .Write(Context.SourceFile)
+ .Write("\" \"")
+ .Write(Sha1AlgorithmId)
+ .Write("\" \"")
+ .Write(Context.Checksum)
+ .WriteLine("\"");
+ }
+
using (writer.BuildNamespace(Context.RootNamespace))
{
// Write out using directives
@@ -102,7 +115,7 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
string taskNamespace = typeof(Task).Namespace;
// We need to add the task namespace but ONLY if it hasn't been added by the default imports or using imports yet.
- if(!defaultImports.Contains(taskNamespace) && !usingVisitor.ImportedUsings.Contains(taskNamespace))
+ if (!defaultImports.Contains(taskNamespace) && !usingVisitor.ImportedUsings.Contains(taskNamespace))
{
writer.WriteUsing(taskNamespace);
}
diff --git a/src/Microsoft.AspNet.Razor/RazorTemplateEngine.cs b/src/Microsoft.AspNet.Razor/RazorTemplateEngine.cs
index d08470f532..c3d741c045 100644
--- a/src/Microsoft.AspNet.Razor/RazorTemplateEngine.cs
+++ b/src/Microsoft.AspNet.Razor/RazorTemplateEngine.cs
@@ -4,7 +4,10 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using System.IO;
+using System.Security.Cryptography;
+using System.Text;
using System.Threading;
using Microsoft.AspNet.Razor.Generator;
using Microsoft.AspNet.Razor.Generator.Compiler;
@@ -19,6 +22,7 @@ namespace Microsoft.AspNet.Razor
///
public class RazorTemplateEngine
{
+ private const int BufferSize = 1024;
public static readonly string DefaultClassName = "Template";
public static readonly string DefaultNamespace = String.Empty;
@@ -127,7 +131,12 @@ namespace Microsoft.AspNet.Razor
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so we don't want to dispose it")]
public GeneratorResults GenerateCode(ITextBuffer input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken)
{
- return GenerateCodeCore(input.ToDocument(), className, rootNamespace, sourceFileName, cancelToken);
+ return GenerateCodeCore(input.ToDocument(),
+ className,
+ rootNamespace,
+ sourceFileName,
+ checksum: null,
+ cancelToken: cancelToken);
}
// See GenerateCode override which takes ITextBuffer, and BufferingTextReader for details.
@@ -146,13 +155,83 @@ namespace Microsoft.AspNet.Razor
return GenerateCode(input, className, rootNamespace, sourceFileName, null);
}
+ ///
+ /// Parses the contents specified by the and returns the generated code.
+ ///
+ /// A that represents the contents to be parsed.
+ /// The name of the generated class. When null, defaults to
+ /// .
+ /// The namespace in which the generated class will reside. When null,
+ /// defaults to .
+ /// The file name to use in line pragmas, usually the original Razor file.
+ /// A that represents the results of parsing the content.
+ ///
+ /// This overload calculates the checksum of the contents of prior to code
+ /// generation. The checksum is used for producing the #pragma checksum line pragma required for
+ /// debugging.
+ ///
+ public GeneratorResults GenerateCode([NotNull] Stream inputStream,
+ string className,
+ string rootNamespace,
+ string sourceFileName)
+ {
+ MemoryStream memoryStream = null;
+ try
+ {
+ if (!inputStream.CanSeek)
+ {
+ memoryStream = new MemoryStream();
+ inputStream.CopyTo(memoryStream);
+
+ // We don't have to dispose the input stream since it is owned externally.
+ inputStream = memoryStream;
+ }
+
+ inputStream.Position = 0;
+ var checksum = ComputeChecksum(inputStream);
+ inputStream.Position = 0;
+
+ using (var reader = new StreamReader(inputStream,
+ Encoding.UTF8,
+ detectEncodingFromByteOrderMarks: true,
+ bufferSize: BufferSize,
+ leaveOpen: true))
+ {
+ var seekableStream = new SeekableTextReader(reader);
+ return GenerateCodeCore(seekableStream,
+ className,
+ rootNamespace,
+ sourceFileName,
+ checksum,
+ cancelToken: null);
+ }
+ }
+ finally
+ {
+ if (memoryStream != null)
+ {
+ memoryStream.Dispose();
+ }
+ }
+ }
+
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so we don't want to dispose it")]
public GeneratorResults GenerateCode(TextReader input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken)
{
- return GenerateCodeCore(new SeekableTextReader(input), className, rootNamespace, sourceFileName, cancelToken);
+ return GenerateCodeCore(new SeekableTextReader(input),
+ className,
+ rootNamespace,
+ sourceFileName,
+ checksum: null,
+ cancelToken: cancelToken);
}
- protected internal virtual GeneratorResults GenerateCodeCore(ITextDocument input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken)
+ protected internal virtual GeneratorResults GenerateCodeCore(ITextDocument input,
+ string className,
+ string rootNamespace,
+ string sourceFileName,
+ string checksum,
+ CancellationToken? cancelToken)
{
className = (className ?? Host.DefaultClassName) ?? DefaultClassName;
rootNamespace = (rootNamespace ?? Host.DefaultNamespace) ?? DefaultNamespace;
@@ -167,7 +246,11 @@ namespace Microsoft.AspNet.Razor
generator.DesignTimeMode = Host.DesignTimeMode;
generator.Visit(results);
- var builder = CreateCodeBuilder(generator.Context);
+
+ var codeGenerationContext = generator.Context;
+ codeGenerationContext.Checksum = checksum;
+
+ var builder = CreateCodeBuilder(codeGenerationContext);
var builderResult = builder.Build();
// Collect results and return
@@ -194,8 +277,24 @@ namespace Microsoft.AspNet.Razor
protected internal virtual CodeBuilder CreateCodeBuilder(CodeGeneratorContext context)
{
- return Host.DecorateCodeBuilder(Host.CodeLanguage.CreateCodeBuilder(context),
+ return Host.DecorateCodeBuilder(Host.CodeLanguage.CreateCodeBuilder(context),
context);
}
+
+ private static string ComputeChecksum(Stream inputStream)
+ {
+ byte[] hashedBytes;
+ using (var hashAlgorithm = SHA1.Create())
+ {
+ hashedBytes = hashAlgorithm.ComputeHash(inputStream);
+ }
+
+ var fileHashBuilder = new StringBuilder(hashedBytes.Length * 2);
+ foreach (var value in hashedBytes)
+ {
+ fileHashBuilder.Append(value.ToString("x2"));
+ }
+ return fileHashBuilder.ToString();
+ }
}
}
diff --git a/src/Microsoft.AspNet.Razor/project.json b/src/Microsoft.AspNet.Razor/project.json
index e061250651..aeba1cb444 100644
--- a/src/Microsoft.AspNet.Razor/project.json
+++ b/src/Microsoft.AspNet.Razor/project.json
@@ -15,7 +15,8 @@
"System.Runtime.InteropServices": "4.0.20.0",
"System.Threading": "4.0.0.0",
"System.Threading.Tasks": "4.0.10.0",
- "System.Threading.Thread": "4.0.0.0"
+ "System.Threading.Thread": "4.0.0.0",
+ "System.Security.Cryptography.Hashing.Algorithms": "4.0.0.0"
},
"frameworks": {
"net45": { },
diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/RazorCodeGeneratorTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/RazorCodeGeneratorTest.cs
index 9fc495ed10..56852dd1d0 100644
--- a/test/Microsoft.AspNet.Razor.Test/Generator/RazorCodeGeneratorTest.cs
+++ b/test/Microsoft.AspNet.Razor.Test/Generator/RazorCodeGeneratorTest.cs
@@ -99,8 +99,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
baselineName = name;
}
- string sourceLocation = string.Format("/CodeGenerator/{1}/Source/{0}.{2}", name, LanguageName, FileExtension);
- string source = TestFile.Create(string.Format("TestFiles/CodeGenerator/CS/Source/{0}.{1}", name, FileExtension)).ReadAllText();
+ string sourceLocation = string.Format("TestFiles/CodeGenerator/{1}/Source/{0}.{2}", name, LanguageName, FileExtension);
string expectedOutput = TestFile.Create(string.Format("TestFiles/CodeGenerator/CS/Output/{0}.{1}", baselineName, BaselineExtension)).ReadAllText();
// Set up the host and engine
@@ -136,11 +135,11 @@ namespace Microsoft.AspNet.Razor.Test.Generator
// Generate code for the file
GeneratorResults results = null;
- using (StringTextBuffer buffer = new StringTextBuffer(source))
+ using (var source = TestFile.Create(sourceLocation).OpenRead())
{
- results = engine.GenerateCode(buffer, className: name, rootNamespace: TestRootNamespaceName, sourceFileName: generatePragmas ? String.Format("{0}.{1}", name, FileExtension) : null);
+ var sourceFileName = generatePragmas ? String.Format("{0}.{1}", name, FileExtension) : null;
+ results = engine.GenerateCode(source, className: name, rootNamespace: TestRootNamespaceName, sourceFileName: sourceFileName);
}
-
// Only called if GENERATE_BASELINES is set, otherwise compiled out.
BaselineWriter.WriteBaseline(String.Format(@"test\Microsoft.AspNet.Razor.Test\TestFiles\CodeGenerator\{0}\Output\{1}.{2}", LanguageName, baselineName, BaselineExtension), results.GeneratedCode);
@@ -179,7 +178,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
for (var i = 0; i < expectedDesignTimePragmas.Count; i++)
{
- if(!expectedDesignTimePragmas[i].Equals(results.DesignTimeLineMappings[i]))
+ if (!expectedDesignTimePragmas[i].Equals(results.DesignTimeLineMappings[i]))
{
Assert.True(false, String.Format("Line mapping {0} is not equivalent.", i));
}
diff --git a/test/Microsoft.AspNet.Razor.Test/RazorTemplateEngineTest.cs b/test/Microsoft.AspNet.Razor.Test/RazorTemplateEngineTest.cs
index b739d171d5..09e934c585 100644
--- a/test/Microsoft.AspNet.Razor.Test/RazorTemplateEngineTest.cs
+++ b/test/Microsoft.AspNet.Razor.Test/RazorTemplateEngineTest.cs
@@ -2,7 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Text;
using System.Threading;
using System.Web.WebPages.TestUtils;
using Microsoft.AspNet.Razor.Generator;
@@ -10,6 +12,7 @@ using Microsoft.AspNet.Razor.Generator.Compiler.CSharp;
using Microsoft.AspNet.Razor.Parser;
using Microsoft.AspNet.Razor.Text;
using Moq;
+using Moq.Protected;
using Xunit;
namespace Microsoft.AspNet.Razor.Test
@@ -161,7 +164,7 @@ namespace Microsoft.AspNet.Razor.Test
// Assert
mockEngine.Verify(e => e.GenerateCodeCore(It.Is(l => l.ReadToEnd() == "foo"),
- className, ns, src, source.Token));
+ className, ns, src, null, source.Token));
}
[Fact]
@@ -212,6 +215,42 @@ namespace Microsoft.AspNet.Razor.Test
Assert.NotNull(results.DesignTimeLineMappings);
}
+ public static IEnumerable