Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Microsoft.AspNet.Mvc.Razor.Host/IMvcRazorHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,15 @@ namespace Microsoft.AspNet.Mvc.Razor
public interface IMvcRazorHost
{
GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream);

/// <summary>
/// Represent the prefix off the main entry class in the view.
/// </summary>
string MainClassNamePrefix { get; }

/// <summary>
/// Represent the namespace the main entry class in the view.
/// </summary>
string DefaultNamespace { get; }
}
}
10 changes: 8 additions & 2 deletions src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ public MvcRazorHost(IApplicationEnvironment appEnvironment)
public MvcRazorHost(string root) :
this(new PhysicalFileSystem(root))
{

}
#endif

Expand Down Expand Up @@ -101,6 +100,12 @@ public virtual string DefaultModel
get { return "dynamic"; }
}

/// <inheritdoc />
public string MainClassNamePrefix
{
get { return "ASPV_"; }
}

/// <summary>
/// Gets the list of chunks that are injected by default by this host.
/// </summary>
Expand All @@ -121,7 +126,8 @@ public virtual string ActivateAttribute
/// <inheritdoc />
public GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream)
{
var className = ParserHelpers.SanitizeClassName(rootRelativePath);
// Adding a prefix so that the main view class can be easily identified.
var className = MainClassNamePrefix + ParserHelpers.SanitizeClassName(rootRelativePath);
using (var reader = new StreamReader(inputStream))
{
var engine = new RazorTemplateEngine(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ private static string ReadContent(IFileInfo file)
}
}
}
catch (IOException)
catch (Exception)
{
// Don't throw if reading the file fails.
return string.Empty;
Expand Down
102 changes: 89 additions & 13 deletions src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,110 @@

using System;
using System.Collections.Concurrent;
using Microsoft.AspNet.FileSystems;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Microsoft.AspNet.Mvc.Razor
{
public class CompilerCache
{
private readonly ConcurrentDictionary<string, Type> _cache;
private readonly ConcurrentDictionary<string, CompilerCacheEntry> _cache;
private static readonly Type[] EmptyType = new Type[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Type.EmptyTypes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope - Not supported in core clr


public CompilerCache()
public CompilerCache([NotNull] IEnumerable<Assembly> assemblies)
: this(GetFileInfos(assemblies))
{
_cache = new ConcurrentDictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
}

public CompilationResult GetOrAdd(IFileInfo file, Func<CompilationResult> compile)
internal CompilerCache(IEnumerable<RazorFileInfoCollection> viewCollections) : this()
{
// Generate a content id
var contentId = file.PhysicalPath + '|' + file.LastModified.Ticks;
foreach (var viewCollection in viewCollections)
{
foreach (var fileInfo in viewCollection.FileInfos)
{
var containingAssembly = viewCollection.GetType().GetTypeInfo().Assembly;
var viewType = containingAssembly.GetType(fileInfo.FullTypeName);
var cacheEntry = new CompilerCacheEntry(fileInfo, viewType);

// There shouldn't be any duplicates and if there are any the first will win.
// If the result doesn't match the one on disk its going to recompile anyways.
_cache.TryAdd(fileInfo.RelativePath, cacheEntry);
}
}
}

internal CompilerCache()
{
_cache = new ConcurrentDictionary<string, CompilerCacheEntry>(StringComparer.OrdinalIgnoreCase);
}

Type compiledType;
if (!_cache.TryGetValue(contentId, out compiledType))
internal static IEnumerable<RazorFileInfoCollection>
GetFileInfos(IEnumerable<Assembly> assemblies)
{
return assemblies.SelectMany(a => a.ExportedTypes)
.Where(Match)
.Select(c => (RazorFileInfoCollection)Activator.CreateInstance(c));
}

private static bool Match(Type t)
{
var inAssemblyType = typeof(RazorFileInfoCollection);
if (inAssemblyType.IsAssignableFrom(t))
{
var result = compile();
_cache.TryAdd(contentId, result.CompiledType);
var hasParameterlessConstructor = t.GetConstructor(EmptyType) != null;

return result;
return hasParameterlessConstructor
&& !t.GetTypeInfo().IsAbstract
&& !t.GetTypeInfo().ContainsGenericParameters;
}

return CompilationResult.Successful(compiledType);
return false;
}

public CompilationResult GetOrAdd(RelativeFileInfo fileInfo, Func<CompilationResult> compile)
{
if (!_cache.TryGetValue(fileInfo.RelativePath, out var cacheEntry))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

{
return OnCacheMiss(fileInfo, compile);
}
else
{
if (cacheEntry.Length != fileInfo.FileInfo.Length)
{
// it's not a match, recompile
return OnCacheMiss(fileInfo, compile);
}

if (cacheEntry.LastModified == fileInfo.FileInfo.LastModified)
{
// Match, not update needed
return CompilationResult.Successful(cacheEntry.ViewType);
}

var hash = RazorFileHash.GetHash(fileInfo.FileInfo);

// Timestamp doesn't match but it might be because of deployment, compare the hash.
if (cacheEntry.IsPreCompiled &&
string.Equals(cacheEntry.Hash, hash, StringComparison.Ordinal))
{
// Cache hit, but we need to update the entry
return OnCacheMiss(fileInfo, () => CompilationResult.Successful(cacheEntry.ViewType));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create another OnCacheMiss(RelativeFileInfo, CompilationResult) that the other method calls into so you could avoid creating this delegate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meh, this is not a hot path by any means, will happen (maybe) once per view, I'm not concerned with this and it makes the code uglier.

}

// it's not a match, recompile
return OnCacheMiss(fileInfo, compile);
}
}

private CompilationResult OnCacheMiss(RelativeFileInfo file, Func<CompilationResult> compile)
{
var result = compile();

var cacheEntry = new CompilerCacheEntry(file, result.CompiledType);
_cache.AddOrUpdate(file.RelativePath, cacheEntry, (a, b) => cacheEntry);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryAdd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, this needs to replace


return result;
}
}
}
39 changes: 39 additions & 0 deletions src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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;

namespace Microsoft.AspNet.Mvc.Razor
{
public class CompilerCacheEntry
{
public CompilerCacheEntry([NotNull] RazorFileInfo info, [NotNull] Type viewType)
{
ViewType = viewType;
RelativePath = info.RelativePath;
Length = info.Length;
LastModified = info.LastModified;
Hash = info.Hash;
}

public CompilerCacheEntry([NotNull] RelativeFileInfo info, [NotNull] Type viewType)
{
ViewType = viewType;
RelativePath = info.RelativePath;
Length = info.FileInfo.Length;
LastModified = info.FileInfo.LastModified;
}

public Type ViewType { get; set; }
public string RelativePath { get; set; }
public long Length { get; set; }
public DateTime LastModified { get; set; }

/// <summary>
/// The file hash, should only be available for pre compiled files.
/// </summary>
public string Hash { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be called Checksum?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its really fingerprint :) potatos/potatos going to leave as is unless you have strong opinion on this


public bool IsPreCompiled { get { return Hash != null; } }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
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
Expand All @@ -31,27 +29,31 @@ public class RoslynCompilationService : ICompilationService

private readonly Lazy<List<MetadataReference>> _applicationReferences;

private readonly string _classPrefix;

/// <summary>
/// Initalizes a new instance of the <see cref="RoslynCompilationService"/> class.
/// </summary>
/// <param name="environment">The environment for the executing application.</param>
/// <param name="loaderEngine">The loader used to load compiled assemblies.</param>
/// <param name="libraryManager">The library manager that provides export and reference information.</param>
/// <param name="host">The <see cref="IMvcRazorHost"/> that was used to generate the code.</param>
public RoslynCompilationService(IApplicationEnvironment environment,
IAssemblyLoaderEngine loaderEngine,
ILibraryManager libraryManager)
ILibraryManager libraryManager,
IMvcRazorHost host)
{
_environment = environment;
_loader = loaderEngine;
_libraryManager = libraryManager;
_applicationReferences = new Lazy<List<MetadataReference>>(GetApplicationReferences);
_classPrefix = host.MainClassNamePrefix;
}

/// <inheritdoc />
public CompilationResult Compile(IFileInfo fileInfo, string compilationContent)
{
var sourceText = SourceText.From(compilationContent, Encoding.UTF8);
var syntaxTrees = new[] { CSharpSyntaxTree.ParseText(sourceText, path: fileInfo.PhysicalPath) };
var syntaxTrees = new[] { SyntaxTreeGenerator.Generate(compilationContent, fileInfo.PhysicalPath) };

var references = _applicationReferences.Value;

Expand Down Expand Up @@ -103,9 +105,10 @@ public CompilationResult Compile(IFileInfo fileInfo, string compilationContent)
}

var type = assembly.GetExportedTypes()
.First();
.First(t => t.Name.
StartsWith(_classPrefix, StringComparison.Ordinal));

return UncachedCompilationResult.Successful(type, compilationContent);
return UncachedCompilationResult.Successful(type);
}
}
}
Expand Down
52 changes: 52 additions & 0 deletions src/Microsoft.AspNet.Mvc.Razor/Compilation/SyntaxTreeGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.AspNet.Mvc.Razor
{
public static class SyntaxTreeGenerator
{
private static CSharpParseOptions DefaultOptions
{
get
{
return CSharpParseOptions.Default
.WithLanguageVersion(LanguageVersion.CSharp6);
}
}

public static SyntaxTree Generate([NotNull] string text, [NotNull] string path)
{
return GenerateCore(text, path, DefaultOptions);
}

public static SyntaxTree Generate([NotNull] string text,
[NotNull] string path,
[NotNull] CSharpParseOptions options)
{
return GenerateCore(text, path, options);
}

public static SyntaxTree GenerateCore([NotNull] string text,
[NotNull] string path,
[NotNull] CSharpParseOptions options)
{
var sourceText = SourceText.From(text, Encoding.UTF8);
var syntaxTree = CSharpSyntaxTree.ParseText(sourceText,
path: path,
options: options);

return syntaxTree;
}

public static CSharpParseOptions GetParseOptions(CSharpCompilation compilation)
{
return CSharpParseOptions.Default
.WithLanguageVersion(compilation.LanguageVersion);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ private UncachedCompilationResult()
{
}

public string RazorFileContent { get; private set; }

/// <summary>
/// Creates a <see cref="UncachedCompilationResult"/> that represents a success in compilation.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ public interface IRazorPageFactory
/// <summary>
/// Creates a <see cref="IRazorPage"/> for the specified path.
/// </summary>
/// <param name="path">The path to locate the page.</param>
/// <param name="relativePath">The path to locate the page.</param>
/// <returns>The IRazorPage instance if it exists, null otherwise.</returns>
IRazorPage CreateInstance(string path);
IRazorPage CreateInstance(string relativePath);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
// 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 Microsoft.AspNet.FileSystems;

namespace Microsoft.AspNet.Mvc.Razor
{
public interface IRazorCompilationService
{
CompilationResult Compile(IFileInfo fileInfo);
CompilationResult Compile(RelativeFileInfo fileInfo);
}
}
Loading