Move caching of compilation results to its own layer.

This will allow only creating the razor compilation when really needed, with the right lifetime.
This commit is contained in:
YishaiGalatzer 2014-10-11 15:35:11 -07:00
parent 13ee27c92c
commit 75084ba0cd
8 changed files with 55 additions and 69 deletions

View File

@ -9,13 +9,13 @@ using System.Reflection;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
public class CompilerCache public class CompilerCache : ICompilerCache
{ {
private readonly ConcurrentDictionary<string, CompilerCacheEntry> _cache; private readonly ConcurrentDictionary<string, CompilerCacheEntry> _cache;
private static readonly Type[] EmptyType = new Type[0]; private static readonly Type[] EmptyType = new Type[0];
public CompilerCache([NotNull] IEnumerable<Assembly> assemblies) public CompilerCache([NotNull] IAssemblyProvider provider)
: this(GetFileInfos(assemblies)) : this(GetFileInfos(provider.CandidateAssemblies))
{ {
} }

View File

@ -6,7 +6,7 @@ using System;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary> /// <summary>
/// An entry in <see cref="CompilerCache"/> that contain metadata about precompiled and dynamically compiled file. /// An entry in <see cref="ICompilerCache"/> that contain metadata about precompiled and dynamically compiled file.
/// </summary> /// </summary>
public class CompilerCacheEntry public class CompilerCacheEntry
{ {

View File

@ -0,0 +1,15 @@
// 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;
using Microsoft.AspNet.FileSystems;
namespace Microsoft.AspNet.Mvc.Razor
{
public interface ICompilerCache
{
CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo,
bool enableInstrumentation,
[NotNull] Func<CompilationResult> compile);
}
}

View File

@ -6,7 +6,7 @@ using System;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary> /// <summary>
/// Represents the result of compilation that does not come from the <see cref="CompilerCache" />. /// Represents the result of compilation that does not come from the <see cref="ICompilerCache" />.
/// </summary> /// </summary>
public class UncachedCompilationResult : CompilationResult public class UncachedCompilationResult : CompilationResult
{ {

View File

@ -16,42 +16,18 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </remarks> /// </remarks>
public class RazorCompilationService : IRazorCompilationService public class RazorCompilationService : IRazorCompilationService
{ {
private readonly IServiceProvider _serviceProvider; private readonly ICompilationService _compilationService;
private readonly CompilerCache _cache;
private ICompilationService _compilationService;
private ICompilationService CompilationService
{
get
{
if (_compilationService == null)
{
_compilationService = _serviceProvider.GetService<ICompilationService>();
}
return _compilationService;
}
}
private readonly IMvcRazorHost _razorHost; private readonly IMvcRazorHost _razorHost;
public RazorCompilationService(IServiceProvider serviceProvider, public RazorCompilationService(ICompilationService compilationService,
IAssemblyProvider _assemblyProvider,
IMvcRazorHost razorHost) IMvcRazorHost razorHost)
{ {
_serviceProvider = serviceProvider; _compilationService = compilationService;
_razorHost = razorHost; _razorHost = razorHost;
_cache = new CompilerCache(_assemblyProvider.CandidateAssemblies);
} }
/// <inheritdoc /> /// <inheritdoc />
public CompilationResult Compile([NotNull] RelativeFileInfo file, bool isInstrumented) public CompilationResult Compile([NotNull] RelativeFileInfo file, bool isInstrumented)
{
return _cache.GetOrAdd(file, isInstrumented, () => CompileCore(file, isInstrumented));
}
internal CompilationResult CompileCore(RelativeFileInfo file, bool isInstrumented)
{ {
_razorHost.EnableInstrumentation = isInstrumented; _razorHost.EnableInstrumentation = isInstrumented;
@ -68,7 +44,7 @@ namespace Microsoft.AspNet.Mvc.Razor
return CompilationResult.Failed(file.FileInfo, results.GeneratedCode, messages); return CompilationResult.Failed(file.FileInfo, results.GeneratedCode, messages);
} }
return CompilationService.Compile(file.FileInfo, results.GeneratedCode); return _compilationService.Compile(file.FileInfo, results.GeneratedCode);
} }
} }
} }

View File

@ -12,30 +12,36 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary> /// </summary>
public class VirtualPathRazorPageFactory : IRazorPageFactory public class VirtualPathRazorPageFactory : IRazorPageFactory
{ {
private IRazorCompilationService _compilationService;
private IRazorCompilationService CompilationService
{
get
{
if (_compilationService == null)
{
_compilationService = _serviceProvider.GetService<IRazorCompilationService>();
}
return _compilationService;
}
}
private readonly ITypeActivator _activator; private readonly ITypeActivator _activator;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IFileInfoCache _fileInfoCache; private readonly IFileInfoCache _fileInfoCache;
private readonly ICompilerCache _compilerCache;
private IRazorCompilationService _razorcompilationService;
private IRazorCompilationService RazorCompilationService
{
get
{
if (_razorcompilationService == null)
{
// it is ok to use the cached service provider because this service has
// a lifetime of Scoped.
_razorcompilationService = _serviceProvider.GetService<IRazorCompilationService>();
}
return _razorcompilationService;
}
}
public VirtualPathRazorPageFactory(ITypeActivator typeActivator, public VirtualPathRazorPageFactory(ITypeActivator typeActivator,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ICompilerCache compilerCache,
IFileInfoCache fileInfoCache) IFileInfoCache fileInfoCache)
{ {
_activator = typeActivator; _activator = typeActivator;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_compilerCache = compilerCache;
_fileInfoCache = fileInfoCache; _fileInfoCache = fileInfoCache;
} }
@ -58,7 +64,9 @@ namespace Microsoft.AspNet.Mvc.Razor
RelativePath = relativePath, RelativePath = relativePath,
}; };
var result = CompilationService.Compile(relativeFileInfo, enableInstrumentation); var result = _compilerCache.GetOrAdd(relativeFileInfo, enableInstrumentation, () =>
RazorCompilationService.Compile(relativeFileInfo, enableInstrumentation));
var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType); var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType);
page.Path = relativePath; page.Path = relativePath;

View File

@ -51,6 +51,7 @@ namespace Microsoft.AspNet.Mvc
// The host is designed to be discarded after consumption and is very inexpensive to initialize. // The host is designed to be discarded after consumption and is very inexpensive to initialize.
yield return describe.Transient<IMvcRazorHost, MvcRazorHost>(); yield return describe.Transient<IMvcRazorHost, MvcRazorHost>();
yield return describe.Singleton<ICompilerCache, CompilerCache>();
yield return describe.Singleton<ICompilationService, RoslynCompilationService>(); yield return describe.Singleton<ICompilationService, RoslynCompilationService>();
yield return describe.Singleton<IRazorCompilationService, RazorCompilationService>(); yield return describe.Singleton<IRazorCompilationService, RazorCompilationService>();

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
[Theory] [Theory]
[InlineData(@"src\work\myapp", @"src\work\myapp\views\index\home.cshtml")] [InlineData(@"src\work\myapp", @"src\work\myapp\views\index\home.cshtml")]
[InlineData(@"src\work\myapp\", @"src\work\myapp\views\index\home.cshtml")] [InlineData(@"src\work\myapp\", @"src\work\myapp\views\index\home.cshtml")]
public void CompileCoreCalculatesRootRelativePath(string appPath, string viewPath) public void CompileCalculatesRootRelativePath(string appPath, string viewPath)
{ {
// Arrange // Arrange
var host = new Mock<IMvcRazorHost>(); var host = new Mock<IMvcRazorHost>();
@ -27,11 +27,6 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
.Returns(GetGeneratorResult()) .Returns(GetGeneratorResult())
.Verifiable(); .Verifiable();
var ap = new Mock<IAssemblyProvider>();
ap.SetupGet(e => e.CandidateAssemblies)
.Returns(Enumerable.Empty<Assembly>())
.Verifiable();
var fileInfo = new Mock<IFileInfo>(); var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.PhysicalPath).Returns(viewPath); fileInfo.Setup(f => f.PhysicalPath).Returns(viewPath);
fileInfo.Setup(f => f.CreateReadStream()).Returns(Stream.Null); fileInfo.Setup(f => f.CreateReadStream()).Returns(Stream.Null);
@ -40,11 +35,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
compiler.Setup(c => c.Compile(fileInfo.Object, It.IsAny<string>())) compiler.Setup(c => c.Compile(fileInfo.Object, It.IsAny<string>()))
.Returns(CompilationResult.Successful(typeof(RazorCompilationServiceTest))); .Returns(CompilationResult.Successful(typeof(RazorCompilationServiceTest)));
var serviceProvider = new Mock<IServiceProvider>(); var razorService = new RazorCompilationService(compiler.Object, host.Object);
serviceProvider.Setup(sp => sp.GetService(It.Is<Type>(t => t == typeof(ICompilationService))))
.Returns(compiler.Object);
var razorService = new RazorCompilationService(serviceProvider.Object, ap.Object, host.Object);
var relativeFileInfo = new RelativeFileInfo() var relativeFileInfo = new RelativeFileInfo()
{ {
@ -53,7 +44,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
}; };
// Act // Act
razorService.CompileCore(relativeFileInfo, isInstrumented: false); razorService.Compile(relativeFileInfo, isInstrumented: false);
// Assert // Assert
host.Verify(); host.Verify();
@ -62,25 +53,20 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]
public void CompileCoreSetsEnableInstrumentationOnHost(bool enableInstrumentation) public void CompileSetsEnableInstrumentationOnHost(bool enableInstrumentation)
{ {
// Arrange // Arrange
var host = new Mock<IMvcRazorHost>(); var host = new Mock<IMvcRazorHost>();
host.SetupAllProperties(); host.SetupAllProperties();
host.Setup(h => h.GenerateCode(It.IsAny<string>(), It.IsAny<Stream>())) host.Setup(h => h.GenerateCode(It.IsAny<string>(), It.IsAny<Stream>()))
.Returns(GetGeneratorResult()); var assemblyProvider = new Mock<IAssemblyProvider>(); .Returns(GetGeneratorResult());
assemblyProvider.SetupGet(e => e.CandidateAssemblies)
.Returns(Enumerable.Empty<Assembly>());
var compiler = new Mock<ICompilationService>(); var compiler = new Mock<ICompilationService>();
compiler.Setup(c => c.Compile(It.IsAny<IFileInfo>(), It.IsAny<string>())) compiler.Setup(c => c.Compile(It.IsAny<IFileInfo>(), It.IsAny<string>()))
.Returns(CompilationResult.Successful(GetType())); .Returns(CompilationResult.Successful(GetType()));
var serviceProvider = new Mock<IServiceProvider>(); var razorService = new RazorCompilationService(compiler.Object, host.Object);
serviceProvider.Setup(sp => sp.GetService(It.Is<Type>(t => t == typeof(ICompilationService))))
.Returns(compiler.Object);
var razorService = new RazorCompilationService(serviceProvider.Object, assemblyProvider.Object, host.Object);
var relativeFileInfo = new RelativeFileInfo() var relativeFileInfo = new RelativeFileInfo()
{ {
FileInfo = Mock.Of<IFileInfo>(), FileInfo = Mock.Of<IFileInfo>(),
@ -88,7 +74,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
}; };
// Act // Act
razorService.CompileCore(relativeFileInfo, isInstrumented: enableInstrumentation); razorService.Compile(relativeFileInfo, isInstrumented: enableInstrumentation);
// Assert // Assert
Assert.Equal(enableInstrumentation, host.Object.EnableInstrumentation); Assert.Equal(enableInstrumentation, host.Object.EnableInstrumentation);