Cache file info access in viewengine
Move compilation and VirtualPathViewFactory to be singletons And cache access to files. The cache time is controlled by MVC options. The cache is implemented in the ExpiringFileInfoCache.cs
This commit is contained in:
parent
87c430ae19
commit
472e500864
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A default implementation for the <see cref="IFileInfoCache" interface./>
|
||||
/// </summary>
|
||||
public class ExpiringFileInfoCache : IFileInfoCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ExpiringFileInfo> _fileInfoCache =
|
||||
new ConcurrentDictionary<string, ExpiringFileInfo>(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<MvcOptions> optionsAccessor)
|
||||
{
|
||||
// TODO: Inject the IFileSystem but only when we get it from the host
|
||||
_fileSystem = new PhysicalFileSystem(env.ApplicationBasePath);
|
||||
_offset = optionsAccessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides cached access to file infos.
|
||||
/// </summary>
|
||||
public interface IFileInfoCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a cached <see cref="IFileInfo" /> for a given path.
|
||||
/// </summary>
|
||||
/// <param name="virtualPath">The virtual path.</param>
|
||||
/// <returns></returns>
|
||||
IFileInfo GetFileInfo(string virtualPath);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,9 @@
|
|||
<Compile Include="ActionDescriptor.cs" />
|
||||
<Compile Include="ActionDescriptorProviderContext.cs" />
|
||||
<Compile Include="ActionDescriptorsCollection.cs" />
|
||||
<Compile Include="ExpiringFileInfoCache.cs" />
|
||||
<Compile Include="Extensions\ViewEngineDescriptorExtensions.cs" />
|
||||
<Compile Include="IExpiringFileInfoCache.cs" />
|
||||
<Compile Include="ReflectedActionDescriptor.cs" />
|
||||
<Compile Include="ReflectedActionDescriptorProvider.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\IReflectedApplicationModelConvention.cs" />
|
||||
|
|
@ -207,6 +209,7 @@
|
|||
<Compile Include="Rendering\SelectListItem.cs" />
|
||||
<Compile Include="Rendering\UnobtrusiveValidationAttributesGenerator.cs" />
|
||||
<Compile Include="Rendering\ViewEngineDescriptor.cs" />
|
||||
<Compile Include="Rendering\RazorViewEngineOptions.cs" />
|
||||
<Compile Include="Rendering\ViewEngineResult.cs" />
|
||||
<Compile Include="RouteAttribute.cs" />
|
||||
<Compile Include="RouteConstraintAttribute.cs" />
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@ using Microsoft.AspNet.Mvc.Rendering;
|
|||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides programmatic configuration for the MVC framework.
|
||||
/// </summary>
|
||||
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<ViewEngineDescriptor>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides programmatic configuration for the anti-forgery token system.
|
||||
/// </summary>
|
||||
public AntiForgeryOptions AntiForgeryOptions
|
||||
{
|
||||
get
|
||||
|
|
@ -41,6 +48,32 @@ namespace Microsoft.AspNet.Mvc
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides programmatic configuration for the default <see cref="IViewEngine" />.
|
||||
/// </summary>
|
||||
public RazorViewEngineOptions ViewEngineOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
return _viewEngineOptions;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException("value",
|
||||
Resources.FormatPropertyOfTypeCannotBeNull("ViewEngineOptions",
|
||||
typeof(MvcOptions)));
|
||||
}
|
||||
|
||||
_viewEngineOptions = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of the <see cref="ModelBinderDescriptor" /> used by the <see cref="CompositeModelBinder" />.
|
||||
/// </summary>
|
||||
public List<ModelBinderDescriptor> ModelBinders { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides programmatic configuration for the default <see cref="Microsoft.AspNet.Mvc.Rendering.IViewEngine"/>.
|
||||
/// </summary>
|
||||
public class RazorViewEngineOptions
|
||||
{
|
||||
private TimeSpan _expirationBeforeCheckingFilesOnDisk = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>
|
||||
/// Controls the <see cref="ExpiringFileInfoCache" /> caching behavior.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="TimeSpan"/> of <see cref="TimeSpan.Zero"/> or less, means no caching.
|
||||
/// <see cref="TimeSpan"/> of <see cref="TimeSpan.MaxValue"/> means indefinite caching.
|
||||
/// </remarks>
|
||||
public TimeSpan ExpirationBeforeCheckingFilesOnDisk
|
||||
{
|
||||
get
|
||||
{
|
||||
return _expirationBeforeCheckingFilesOnDisk;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value.TotalMilliseconds < 0)
|
||||
{
|
||||
_expirationBeforeCheckingFilesOnDisk = TimeSpan.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
_expirationBeforeCheckingFilesOnDisk = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<IViewEngineProvider, DefaultViewEngineProvider>();
|
||||
yield return describe.Scoped<ICompositeViewEngine, CompositeViewEngine>();
|
||||
yield return describe.Transient<IRazorCompilationService, RazorCompilationService>();
|
||||
yield return describe.Transient<IVirtualPathViewFactory, VirtualPathViewFactory>();
|
||||
yield return describe.Singleton<IRazorCompilationService, RazorCompilationService>();
|
||||
|
||||
yield return describe.Singleton<IRazorViewActivator, RazorViewActivator>();
|
||||
// Virtual path view factory needs to stay scoped so views can get get scoped services.
|
||||
yield return describe.Scoped<IVirtualPathViewFactory, VirtualPathViewFactory>();
|
||||
yield return describe.Singleton<IFileInfoCache, ExpiringFileInfoCache>();
|
||||
|
||||
yield return describe.Transient<INestedProvider<ActionDescriptorProviderContext>,
|
||||
ReflectedActionDescriptorProvider>();
|
||||
|
|
|
|||
|
|
@ -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<IApplicationEnvironment>(MockBehavior.Strict);
|
||||
mock.Setup(ae => ae.ApplicationBasePath).Returns(Directory.GetCurrentDirectory());
|
||||
|
||||
return mock.Object;
|
||||
}
|
||||
}
|
||||
|
||||
public MvcOptions Options
|
||||
{
|
||||
get
|
||||
{
|
||||
return new MvcOptions();
|
||||
}
|
||||
}
|
||||
|
||||
public IOptionsAccessor<MvcOptions> OptionsAccessor
|
||||
{
|
||||
get
|
||||
{
|
||||
var options = Options;
|
||||
|
||||
var mock = new Mock<IOptionsAccessor<MvcOptions>>(MockBehavior.Strict);
|
||||
mock.Setup(oa => oa.Options).Returns(options);
|
||||
|
||||
return mock.Object;
|
||||
}
|
||||
}
|
||||
|
||||
public ControllableExpiringFileInfoCache GetCache(IOptionsAccessor<MvcOptions> 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<MvcOptions> accessor, ControllableExpiringFileInfoCache cache, int offsetMilliSeconds)
|
||||
{
|
||||
var baseMilliSeconds = (int)accessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk.TotalMilliseconds;
|
||||
|
||||
cache.Sleep(baseMilliSeconds + offsetMilliSeconds);
|
||||
}
|
||||
|
||||
public void SetExpiration(IOptionsAccessor<MvcOptions> 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<object[]> ImmediateExpirationTimespans
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[]
|
||||
{
|
||||
TimeSpan.FromSeconds(0.0)
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
TimeSpan.FromSeconds(-1.0)
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
TimeSpan.MinValue
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("ImmediateExpirationTimespans")]
|
||||
public void GettingFileInfoReturnsNewDataWithCustomImmediateExpiration(TimeSpan expiration)
|
||||
{
|
||||
// Arrange
|
||||
var optionsAccessor = OptionsAccessor;
|
||||
SetExpiration(optionsAccessor, expiration);
|
||||
|
||||
string FileName = "myfile4.cshtml";
|
||||
var cache = GetCache(optionsAccessor);
|
||||
|
||||
CreateFile(FileName, cache);
|
||||
|
||||
// Act
|
||||
var fileInfo1 = cache.GetFileInfo(FileName);
|
||||
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<object[]> CustomExpirationTimespans
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[]
|
||||
{
|
||||
TimeSpan.FromSeconds(1.0)
|
||||
};
|
||||
|
||||
yield return new object[]
|
||||
{
|
||||
TimeSpan.FromSeconds(3.0)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("CustomExpirationTimespans")]
|
||||
public void GettingFileInfoReturnsNewDataWithCustomExpiration(TimeSpan expiration)
|
||||
{
|
||||
// Arrange
|
||||
var optionsAccessor = OptionsAccessor;
|
||||
SetExpiration(optionsAccessor, expiration);
|
||||
|
||||
string FileName = "myfile5.cshtml";
|
||||
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(FileName, fileInfo1.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("CustomExpirationTimespans")]
|
||||
public void GettingFileInfoReturnsSameDataWithCustomExpiration(TimeSpan expiration)
|
||||
{
|
||||
// Arrange
|
||||
var optionsAccessor = OptionsAccessor;
|
||||
SetExpiration(optionsAccessor, expiration);
|
||||
|
||||
string FileName = "myfile6.cshtml";
|
||||
var cache = GetCache(optionsAccessor);
|
||||
|
||||
CreateFile(FileName, cache);
|
||||
|
||||
// Act
|
||||
var fileInfo1 = cache.GetFileInfo(FileName);
|
||||
|
||||
Sleep(optionsAccessor, cache, -500);
|
||||
|
||||
var fileInfo2 = cache.GetFileInfo(FileName);
|
||||
|
||||
// Assert
|
||||
Assert.Same(fileInfo1, fileInfo2);
|
||||
|
||||
Assert.Equal(FileName, fileInfo1.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GettingFileInfoReturnsSameDataWithMaxExpiration()
|
||||
{
|
||||
// Arrange
|
||||
var optionsAccessor = OptionsAccessor;
|
||||
SetExpiration(optionsAccessor, TimeSpan.MaxValue);
|
||||
|
||||
string FileName = "myfile7.cshtml";
|
||||
var cache = GetCache(optionsAccessor);
|
||||
|
||||
CreateFile(FileName, cache);
|
||||
|
||||
// Act
|
||||
var fileInfo1 = cache.GetFileInfo(FileName);
|
||||
|
||||
Sleep(cache, 2500);
|
||||
|
||||
var fileInfo2 = cache.GetFileInfo(FileName);
|
||||
|
||||
// Assert
|
||||
Assert.Same(fileInfo1, fileInfo2);
|
||||
|
||||
Assert.Equal(FileName, fileInfo1.Name);
|
||||
}
|
||||
|
||||
public class ControllableExpiringFileInfoCache : ExpiringFileInfoCache
|
||||
{
|
||||
public ControllableExpiringFileInfoCache(IApplicationEnvironment env,
|
||||
IOptionsAccessor<MvcOptions> optionsAccessor)
|
||||
: base(env, optionsAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
private DateTime? _internalUtcNow { get; set; }
|
||||
private DummyFileSystem _underlyingFileSystem = new DummyFileSystem();
|
||||
|
||||
protected override DateTime UtcNow
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_internalUtcNow == null)
|
||||
{
|
||||
_internalUtcNow = base.UtcNow;
|
||||
}
|
||||
|
||||
return _internalUtcNow.Value.AddTicks(1);
|
||||
}
|
||||
}
|
||||
|
||||
protected override IFileSystem FileSystem
|
||||
{
|
||||
get
|
||||
{
|
||||
return UnderlyingFileSystem;
|
||||
}
|
||||
}
|
||||
|
||||
public void Sleep(int milliSeconds)
|
||||
{
|
||||
if (milliSeconds <= 0)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
_internalUtcNow = UtcNow.AddMilliseconds(milliSeconds);
|
||||
}
|
||||
|
||||
public DummyFileSystem UnderlyingFileSystem
|
||||
{
|
||||
get
|
||||
{
|
||||
return _underlyingFileSystem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DummyFileSystem : IFileSystem
|
||||
{
|
||||
private Dictionary<string, IFileInfo> _fileInfos = new Dictionary<string, IFileInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public void AddFile(IFileInfo fileInfo)
|
||||
{
|
||||
if (_fileInfos.ContainsKey(fileInfo.Name))
|
||||
{
|
||||
_fileInfos[fileInfo.Name] = fileInfo;
|
||||
}
|
||||
else
|
||||
{
|
||||
_fileInfos.Add(fileInfo.Name, fileInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetDirectoryContents(string subpath, out IEnumerable<IFileInfo> contents)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool TryGetFileInfo(string subpath, out IFileInfo fileInfo)
|
||||
{
|
||||
IFileInfo knownInfo;
|
||||
if (_fileInfos.TryGetValue(subpath, out knownInfo))
|
||||
{
|
||||
fileInfo = new DummyFileInfo()
|
||||
{
|
||||
Name = knownInfo.Name,
|
||||
LastModified = knownInfo.LastModified,
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fileInfo = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DummyFileInfo : IFileInfo
|
||||
{
|
||||
public DateTime LastModified { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public long Length { get { throw new NotImplementedException(); } }
|
||||
public bool IsDirectory { get { throw new NotImplementedException(); } }
|
||||
public string PhysicalPath { get { throw new NotImplementedException(); } }
|
||||
public Stream CreateReadStream() { throw new NotImplementedException(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
<Compile Include="ActionResults\RedirectResultTest.cs" />
|
||||
<Compile Include="ActionSelectionConventionTests.cs" />
|
||||
<Compile Include="AntiXsrf\AntiForgeryOptionsTests.cs" />
|
||||
<Compile Include="ExpiringFileInfoCacheTest.cs" />
|
||||
<Compile Include="Extensions\ViewEngineDscriptorExtensionsTest.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\ReflectedParameterModelTests.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\ReflectedActionModelTests.cs" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue