diff --git a/src/Microsoft.AspNet.Mvc.Core/ExpiringFileInfoCache.cs b/src/Microsoft.AspNet.Mvc.Core/ExpiringFileInfoCache.cs
new file mode 100644
index 0000000000..00b1cf382d
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ExpiringFileInfoCache.cs
@@ -0,0 +1,82 @@
+// 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 System.Collections.Concurrent;
+using Microsoft.AspNet.FileSystems;
+using Microsoft.Framework.OptionsModel;
+using Microsoft.Framework.Runtime;
+
+namespace Microsoft.AspNet.Mvc.Core
+{
+ ///
+ /// A default implementation for the
+ ///
+ public class ExpiringFileInfoCache : IFileInfoCache
+ {
+ private readonly ConcurrentDictionary _fileInfoCache =
+ new ConcurrentDictionary(StringComparer.Ordinal);
+
+ private readonly PhysicalFileSystem _fileSystem;
+ private readonly TimeSpan _offset;
+
+ protected virtual IFileSystem FileSystem
+ {
+ get
+ {
+ return _fileSystem;
+ }
+ }
+
+ protected virtual DateTime UtcNow
+ {
+ get
+ {
+ return DateTime.UtcNow;
+ }
+ }
+
+ public ExpiringFileInfoCache(IApplicationEnvironment env,
+ IOptionsAccessor optionsAccessor)
+ {
+ // TODO: Inject the IFileSystem but only when we get it from the host
+ _fileSystem = new PhysicalFileSystem(env.ApplicationBasePath);
+ _offset = optionsAccessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk;
+ }
+
+ ///
+ public IFileInfo GetFileInfo([NotNull] string virtualPath)
+ {
+ IFileInfo fileInfo;
+ ExpiringFileInfo expiringFileInfo;
+
+ var utcNow = UtcNow;
+
+ if (_fileInfoCache.TryGetValue(virtualPath, out expiringFileInfo)
+ && expiringFileInfo.ValidUntil > utcNow)
+ {
+ fileInfo = expiringFileInfo.FileInfo;
+ }
+ else
+ {
+ FileSystem.TryGetFileInfo(virtualPath, out fileInfo);
+
+ expiringFileInfo = new ExpiringFileInfo()
+ {
+ FileInfo = fileInfo,
+ ValidUntil = _offset == TimeSpan.MaxValue ? DateTime.MaxValue : utcNow.Add(_offset),
+ };
+
+ _fileInfoCache.AddOrUpdate(virtualPath, expiringFileInfo, (a, b) => expiringFileInfo);
+ }
+
+ return fileInfo;
+ }
+
+ private class ExpiringFileInfo
+ {
+ public IFileInfo FileInfo { get; set; }
+ public DateTime ValidUntil { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/IExpiringFileInfoCache.cs b/src/Microsoft.AspNet.Mvc.Core/IExpiringFileInfoCache.cs
new file mode 100644
index 0000000000..c60e4d74ab
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/IExpiringFileInfoCache.cs
@@ -0,0 +1,20 @@
+// 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.Core
+{
+ ///
+ /// Provides cached access to file infos.
+ ///
+ public interface IFileInfoCache
+ {
+ ///
+ /// Returns a cached for a given path.
+ ///
+ /// The virtual path.
+ ///
+ IFileInfo GetFileInfo(string virtualPath);
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
index 93132102fa..65a6bf1e91 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
+++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
@@ -27,7 +27,9 @@
+
+
@@ -207,6 +209,7 @@
+
diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs
index 4795eb9074..b9ca779677 100644
--- a/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs
@@ -10,9 +10,13 @@ using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
+ ///
+ /// Provides programmatic configuration for the MVC framework.
+ ///
public class MvcOptions
{
private AntiForgeryOptions _antiForgeryOptions = new AntiForgeryOptions();
+ private RazorViewEngineOptions _viewEngineOptions = new RazorViewEngineOptions();
public MvcOptions()
{
@@ -21,6 +25,9 @@ namespace Microsoft.AspNet.Mvc
ViewEngines = new List();
}
+ ///
+ /// Provides programmatic configuration for the anti-forgery token system.
+ ///
public AntiForgeryOptions AntiForgeryOptions
{
get
@@ -41,6 +48,32 @@ namespace Microsoft.AspNet.Mvc
}
}
+ ///
+ /// Provides programmatic configuration for the default .
+ ///
+ public RazorViewEngineOptions ViewEngineOptions
+ {
+ get
+ {
+ return _viewEngineOptions;
+ }
+
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value",
+ Resources.FormatPropertyOfTypeCannotBeNull("ViewEngineOptions",
+ typeof(MvcOptions)));
+ }
+
+ _viewEngineOptions = value;
+ }
+ }
+
+ ///
+ /// Get a list of the used by the .
+ ///
public List ModelBinders { get; private set; }
///
diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/RazorViewEngineOptions.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/RazorViewEngineOptions.cs
new file mode 100644
index 0000000000..b4dc8ae008
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/RazorViewEngineOptions.cs
@@ -0,0 +1,42 @@
+// 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.Core
+{
+ ///
+ /// Provides programmatic configuration for the default .
+ ///
+ public class RazorViewEngineOptions
+ {
+ private TimeSpan _expirationBeforeCheckingFilesOnDisk = TimeSpan.FromSeconds(2);
+
+ ///
+ /// Controls the caching behavior.
+ ///
+ ///
+ /// of or less, means no caching.
+ /// of means indefinite caching.
+ ///
+ public TimeSpan ExpirationBeforeCheckingFilesOnDisk
+ {
+ get
+ {
+ return _expirationBeforeCheckingFilesOnDisk;
+ }
+
+ set
+ {
+ if (value.TotalMilliseconds < 0)
+ {
+ _expirationBeforeCheckingFilesOnDisk = TimeSpan.Zero;
+ }
+ else
+ {
+ _expirationBeforeCheckingFilesOnDisk = value;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json
index 522996240a..1769c87200 100644
--- a/src/Microsoft.AspNet.Mvc.Core/project.json
+++ b/src/Microsoft.AspNet.Mvc.Core/project.json
@@ -4,6 +4,7 @@
"warningsAsErrors": true
},
"dependencies": {
+ "Microsoft.AspNet.FileSystems": "1.0.0-*",
"Microsoft.AspNet.Http": "1.0.0-*",
"Microsoft.AspNet.Mvc.Common": "",
"Microsoft.AspNet.Mvc.ModelBinding": "",
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs
index 461ba34304..85231041c1 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs
@@ -13,7 +13,8 @@ namespace Microsoft.AspNet.Mvc.Razor
{
public class RazorCompilationService : IRazorCompilationService
{
- private static readonly CompilerCache _cache = new CompilerCache();
+ // This class must be registered as a singleton service for the caching to work.
+ private readonly CompilerCache _cache = new CompilerCache();
private readonly IApplicationEnvironment _environment;
private readonly ICompilationService _baseCompilationService;
private readonly IMvcRazorHost _razorHost;
diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs
index 68869beb93..3a8884a404 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs
@@ -2,36 +2,35 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
-using Microsoft.AspNet.FileSystems;
+using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.Framework.DependencyInjection;
-using Microsoft.Framework.Runtime;
namespace Microsoft.AspNet.Mvc.Razor
{
public class VirtualPathViewFactory : IVirtualPathViewFactory
{
- private readonly PhysicalFileSystem _fileSystem;
private readonly IRazorCompilationService _compilationService;
private readonly ITypeActivator _activator;
private readonly IServiceProvider _serviceProvider;
+ private readonly IFileInfoCache _fileInfoCache;
- public VirtualPathViewFactory(IApplicationEnvironment env,
- IRazorCompilationService compilationService,
+ public VirtualPathViewFactory(IRazorCompilationService compilationService,
ITypeActivator typeActivator,
- IServiceProvider serviceProvider)
+ IServiceProvider serviceProvider,
+ IFileInfoCache fileInfoCache)
{
- // TODO: Continue to inject the IFileSystem but only when we get it from the host
- _fileSystem = new PhysicalFileSystem(env.ApplicationBasePath);
_compilationService = compilationService;
_activator = typeActivator;
_serviceProvider = serviceProvider;
+ _fileInfoCache = fileInfoCache;
}
public IView CreateInstance([NotNull] string virtualPath)
{
- IFileInfo fileInfo;
- if (_fileSystem.TryGetFileInfo(virtualPath, out fileInfo))
+ var fileInfo = _fileInfoCache.GetFileInfo(virtualPath);
+
+ if (fileInfo != null)
{
var result = _compilationService.Compile(fileInfo);
return (IView)_activator.CreateInstance(_serviceProvider, result.CompiledType);
diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs
index 966a937df1..5e703bdf40 100644
--- a/src/Microsoft.AspNet.Mvc/MvcServices.cs
+++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
+using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Razor;
@@ -41,9 +42,12 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Singleton();
yield return describe.Scoped();
- yield return describe.Transient();
- yield return describe.Transient();
+ yield return describe.Singleton();
+
yield return describe.Singleton();
+ // Virtual path view factory needs to stay scoped so views can get get scoped services.
+ yield return describe.Scoped();
+ yield return describe.Singleton();
yield return describe.Transient,
ReflectedActionDescriptorProvider>();
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ExpiringFileInfoCacheTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ExpiringFileInfoCacheTest.cs
new file mode 100644
index 0000000000..6e65601252
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ExpiringFileInfoCacheTest.cs
@@ -0,0 +1,429 @@
+// 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 System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using Microsoft.AspNet.FileSystems;
+using Microsoft.Framework.OptionsModel;
+using Microsoft.Framework.Runtime;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.Core.Test
+{
+ public class ExpiringFileInfoCacheTest
+ {
+ private const string FileName = "myView.cshtml";
+
+ public IApplicationEnvironment ApplicationEnvironment
+ {
+ get
+ {
+ var mock = new Mock(MockBehavior.Strict);
+ mock.Setup(ae => ae.ApplicationBasePath).Returns(Directory.GetCurrentDirectory());
+
+ return mock.Object;
+ }
+ }
+
+ public MvcOptions Options
+ {
+ get
+ {
+ return new MvcOptions();
+ }
+ }
+
+ public IOptionsAccessor OptionsAccessor
+ {
+ get
+ {
+ var options = Options;
+
+ var mock = new Mock>(MockBehavior.Strict);
+ mock.Setup(oa => oa.Options).Returns(options);
+
+ return mock.Object;
+ }
+ }
+
+ public ControllableExpiringFileInfoCache GetCache(IOptionsAccessor optionsAccessor)
+ {
+ return new ControllableExpiringFileInfoCache(ApplicationEnvironment, optionsAccessor);
+ }
+
+ public void CreateFile(string FileName, ControllableExpiringFileInfoCache cache)
+ {
+ var fileInfo = new DummyFileInfo()
+ {
+ Name = FileName,
+ LastModified = DateTime.Now,
+ };
+
+ cache.UnderlyingFileSystem.AddFile(fileInfo);
+ }
+
+ public void Sleep(ControllableExpiringFileInfoCache cache, int offsetMilliseconds)
+ {
+ cache.Sleep(offsetMilliseconds);
+ }
+
+ public void Sleep(IOptionsAccessor accessor, ControllableExpiringFileInfoCache cache, int offsetMilliSeconds)
+ {
+ var baseMilliSeconds = (int)accessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk.TotalMilliseconds;
+
+ cache.Sleep(baseMilliSeconds + offsetMilliSeconds);
+ }
+
+ public void SetExpiration(IOptionsAccessor accessor, TimeSpan expiration)
+ {
+ accessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk = expiration;
+ }
+
+ [Fact]
+ public void VerifyDefaultOptionsAreSetupCorrectly()
+ {
+ var optionsAccessor = OptionsAccessor;
+
+ // Assert
+ Assert.Equal(2000, optionsAccessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk.TotalMilliseconds);
+ }
+
+ [Fact]
+ public void GettingFileInfoReturnsTheSameDataWithDefaultOptions()
+ {
+ // Arrange
+ var cache = GetCache(OptionsAccessor);
+
+ CreateFile(FileName, cache);
+
+ // Act
+ var fileInfo1 = cache.GetFileInfo(FileName);
+ var fileInfo2 = cache.GetFileInfo(FileName);
+
+ // Assert
+ Assert.Same(fileInfo1, fileInfo2);
+
+ Assert.Equal(FileName, fileInfo1.Name);
+ }
+
+ [Fact]
+ public void GettingFileInfoReturnsTheSameDataWithDefaultOptionsEvenWhenFilesHaveChanged()
+ {
+ // Arrange
+ var cache = GetCache(OptionsAccessor);
+
+ CreateFile(FileName, cache);
+
+ // Act
+ var fileInfo1 = cache.GetFileInfo(FileName);
+
+ CreateFile(FileName, cache);
+
+ var fileInfo2 = cache.GetFileInfo(FileName);
+
+ // Assert
+ Assert.Same(fileInfo1, fileInfo2);
+
+ Assert.Equal(fileInfo1.LastModified, fileInfo2.LastModified);
+ Assert.Equal(FileName, fileInfo1.Name);
+ Assert.Equal(FileName, fileInfo2.Name);
+ }
+
+ [Fact]
+ public void GettingFileInfoReturnsNewDataWithDefaultOptionsAfterExpirationAndFileChange()
+ {
+ var optionsAccessor = OptionsAccessor;
+
+ // Arrange
+ var cache = GetCache(optionsAccessor);
+
+ CreateFile(FileName, cache);
+
+ // Act
+ var fileInfo1 = cache.GetFileInfo(FileName);
+
+ Sleep(optionsAccessor, cache, 500);
+ CreateFile(FileName, cache);
+
+ var fileInfo2 = cache.GetFileInfo(FileName);
+
+ // Assert
+ Assert.NotSame(fileInfo1, fileInfo2);
+
+ Assert.Equal(FileName, fileInfo1.Name);
+ Assert.Equal(FileName, fileInfo2.Name);
+ }
+
+ [Fact]
+ public void GettingFileInfoReturnsNewDataWithDefaultOptionsAfterExpiration()
+ {
+ // Arrange
+ var optionsAccessor = OptionsAccessor;
+
+ var cache = GetCache(optionsAccessor);
+
+ CreateFile(FileName, cache);
+
+ // Act
+ var fileInfo1 = cache.GetFileInfo(FileName);
+
+ Sleep(optionsAccessor, cache, 500);
+
+ var fileInfo2 = cache.GetFileInfo(FileName);
+
+ // Assert
+ Assert.NotSame(fileInfo1, fileInfo2);
+
+ Assert.Equal(fileInfo1.LastModified, fileInfo2.LastModified);
+ Assert.Equal(FileName, fileInfo1.Name);
+ Assert.Equal(FileName, fileInfo2.Name);
+ }
+
+ public static IEnumerable