From 472e500864273724e866a980bf80eebd4f673fdd Mon Sep 17 00:00:00 2001 From: Yishai Galatzer Date: Mon, 14 Jul 2014 09:32:37 -0700 Subject: [PATCH] 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 --- .../ExpiringFileInfoCache.cs | 82 ++++ .../IExpiringFileInfoCache.cs | 20 + .../Microsoft.AspNet.Mvc.Core.kproj | 3 + src/Microsoft.AspNet.Mvc.Core/MvcOptions.cs | 33 ++ .../Rendering/RazorViewEngineOptions.cs | 42 ++ src/Microsoft.AspNet.Mvc.Core/project.json | 1 + .../Razor/RazorCompilationService.cs | 3 +- .../ViewEngine/VirtualPathViewFactory.cs | 19 +- src/Microsoft.AspNet.Mvc/MvcServices.cs | 8 +- .../ExpiringFileInfoCacheTest.cs | 429 ++++++++++++++++++ .../Microsoft.AspNet.Mvc.Core.Test.kproj | 1 + 11 files changed, 628 insertions(+), 13 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/ExpiringFileInfoCache.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/IExpiringFileInfoCache.cs create mode 100644 src/Microsoft.AspNet.Mvc.Core/Rendering/RazorViewEngineOptions.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ExpiringFileInfoCacheTest.cs 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 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 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 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 _fileInfos = new Dictionary(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 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(); } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index 7d33a4dabf..299a260a5c 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -30,6 +30,7 @@ +