Add a feature to disable file watching in Razor pages (#8369)

* Add a feature to disable file watching in Razor pages

Fixes https://github.com/aspnet/Mvc/issues/8362
This commit is contained in:
Pranav K 2018-09-06 10:16:31 -07:00 committed by GitHub
parent b156dee4f1
commit 07cc9e66c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 685 additions and 79 deletions

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. 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.Collections.ObjectModel;
using System.Diagnostics;

View File

@ -159,6 +159,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IPostConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>());
services.TryAddSingleton<
IRazorViewEngineFileProviderAccessor,
DefaultRazorViewEngineFileProviderAccessor>();

View File

@ -116,6 +116,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
}
}
public bool AllowRecompilingViewsOnFileChange { get; set; }
/// <inheritdoc />
public Task<CompiledViewDescriptor> CompileAsync(string relativePath)
{
@ -254,16 +256,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Used to validate and recompile
NormalizedPath = normalizedPath,
ExpirationTokens = new List<IChangeToken>(),
};
var checksums = precompiledView.Item.GetChecksumMetadata();
for (var i = 0; i < checksums.Count; i++)
{
// We rely on Razor to provide the right set of checksums. Trust the compiler, it has to do a good job,
// so it probably will.
item.ExpirationTokens.Add(_fileProvider.Watch(checksums[i].Identifier));
}
ExpirationTokens = GetExpirationTokens(precompiledView),
};
// We also need to create a new descriptor, because the original one doesn't have expiration tokens on
// it. These will be used by the view location cache, which is like an L1 cache for views (this class is
@ -282,10 +277,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
private ViewCompilerWorkItem CreateRuntimeCompilationWorkItem(string normalizedPath)
{
var expirationTokens = new List<IChangeToken>()
IList<IChangeToken> expirationTokens = Array.Empty<IChangeToken>();
if (AllowRecompilingViewsOnFileChange)
{
_fileProvider.Watch(normalizedPath),
};
var changeToken = _fileProvider.Watch(normalizedPath);
expirationTokens = new List<IChangeToken> { changeToken };
}
var projectItem = _projectEngine.FileSystem.GetItem(normalizedPath);
if (!projectItem.Exists)
@ -313,9 +311,46 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
_logger.ViewCompilerFoundFileToCompile(normalizedPath);
GetChangeTokensFromImports(expirationTokens, projectItem);
return new ViewCompilerWorkItem()
{
SupportsCompilation = true,
NormalizedPath = normalizedPath,
ExpirationTokens = expirationTokens,
};
}
private IList<IChangeToken> GetExpirationTokens(CompiledViewDescriptor precompiledView)
{
if (!AllowRecompilingViewsOnFileChange)
{
return Array.Empty<IChangeToken>();
}
var checksums = precompiledView.Item.GetChecksumMetadata();
var expirationTokens = new List<IChangeToken>(checksums.Count);
for (var i = 0; i < checksums.Count; i++)
{
// We rely on Razor to provide the right set of checksums. Trust the compiler, it has to do a good job,
// so it probably will.
expirationTokens.Add(_fileProvider.Watch(checksums[i].Identifier));
}
return expirationTokens;
}
private void GetChangeTokensFromImports(IList<IChangeToken> expirationTokens, RazorProjectItem projectItem)
{
if (!AllowRecompilingViewsOnFileChange)
{
return;
}
// OK this means we can do compilation. For now let's just identify the other files we need to watch
// so we can create the cache entry. Compilation will happen after we release the lock.
var importFeature = _projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().FirstOrDefault();
// There should always be an import feature unless someone has misconfigured their RazorProjectEngine.
@ -330,14 +365,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
expirationTokens.Add(_fileProvider.Watch(physicalImport.FilePath));
}
return new ViewCompilerWorkItem()
{
SupportsCompilation = true,
NormalizedPath = normalizedPath,
ExpirationTokens = expirationTokens,
};
}
protected virtual CompiledViewDescriptor CompileAndEmit(string relativePath)

View File

@ -80,7 +80,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
#pragma warning restore CS0618 // Type or member is obsolete
feature.ViewDescriptors,
_compilationMemoryCacheProvider.CompilationMemoryCache,
_logger);
_logger)
{
AllowRecompilingViewsOnFileChange = _viewEngineOptions.AllowRecompilingViewsOnFileChange,
};
}
}
}

View File

@ -7,3 +7,4 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.RazorPages.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -2,8 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.FileProviders;
@ -13,10 +15,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// <summary>
/// Provides programmatic configuration for the <see cref="RazorViewEngine"/>.
/// </summary>
public class RazorViewEngineOptions
public class RazorViewEngineOptions : IEnumerable<ICompatibilitySwitch>
{
private readonly ICompatibilitySwitch[] _switches;
private readonly CompatibilitySwitch<bool> _allowRecompilingViewsOnFileChange;
private Action<RoslynCompilationContext> _compilationCallback = c => { };
public RazorViewEngineOptions()
{
_allowRecompilingViewsOnFileChange = new CompatibilitySwitch<bool>(nameof(AllowRecompilingViewsOnFileChange));
_switches = new[]
{
_allowRecompilingViewsOnFileChange,
};
}
/// <summary>
/// Gets a <see cref="IList{IViewLocationExpander}"/> used by the <see cref="RazorViewEngine"/>.
/// </summary>
@ -181,5 +194,52 @@ namespace Microsoft.AspNetCore.Mvc.Razor
_compilationCallback = value;
}
}
/// <summary>
/// Gets or sets a value that determines if Razor files (Razor Views and Razor Pages) are recompiled and updated
/// if files change on disk.
/// <para>
/// When <see langword="true"/>, MVC will use <see cref="IFileProvider.Watch(string)"/> to watch for changes to
/// Razor files in configured <see cref="IFileProvider"/> instances.
/// </para>
/// </summary>
/// <value>
/// The default value is <see langword="true"/> if the version is <see cref = "CompatibilityVersion.Version_2_1" />
/// or earlier. If the version is later and <see cref= "IHostingEnvironment.EnvironmentName" /> is <c>Development</c>,
/// the default value is <see langword="true"/>. Otherwise, the default value is <see langword="false" />.
/// </value>
/// <remarks>
/// <para>
/// This property is associated with a compatibility switch and can provide a different behavior depending on
/// the configured compatibility version for the application. See <see cref="CompatibilityVersion"/> for
/// guidance and examples of setting the application's compatibility version.
/// </para>
/// <para>
/// Configuring the desired value of the compatibility switch by calling this property's setter will take
/// precedence over the value implied by the application's <see cref="CompatibilityVersion"/>.
/// </para>
/// <para>
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_1"/> or
/// lower then this setting will have the value <see langword="true"/> unless explicitly configured.
/// </para>
/// <para>
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_2"/> or
/// higher then this setting will have the value <see langword="false"/> unless
/// <see cref="IHostingEnvironment.EnvironmentName"/> is <c>Development</c> or the value is explicitly configured.
/// </para>
/// </remarks>
public bool AllowRecompilingViewsOnFileChange
{
// Note: When compatibility switches are removed in 3.0, this property should be retained as a regular boolean property.
get => _allowRecompilingViewsOnFileChange.Value;
set => _allowRecompilingViewsOnFileChange.Value = value;
}
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator()
{
return ((IEnumerable<ICompatibilitySwitch>)_switches).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();
}
}

View File

@ -2,27 +2,45 @@
// 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 Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
namespace Microsoft.AspNetCore.Mvc.Razor
{
/// <summary>
/// Sets up default options for <see cref="RazorViewEngineOptions"/>.
/// </summary>
public class RazorViewEngineOptionsSetup : IConfigureOptions<RazorViewEngineOptions>
internal class RazorViewEngineOptionsSetup :
ConfigureCompatibilityOptions<RazorViewEngineOptions>,
IConfigureOptions<RazorViewEngineOptions>
{
private readonly IHostingEnvironment _hostingEnvironment;
/// <summary>
/// Initializes a new instance of <see cref="RazorViewEngineOptions"/>.
/// </summary>
/// <param name="hostingEnvironment"><see cref="IHostingEnvironment"/> for the application.</param>
public RazorViewEngineOptionsSetup(IHostingEnvironment hostingEnvironment)
public RazorViewEngineOptionsSetup(
IHostingEnvironment hostingEnvironment,
ILoggerFactory loggerFactory,
IOptions<MvcCompatibilityOptions> compatibilityOptions)
: base(loggerFactory, compatibilityOptions)
{
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
}
protected override IReadOnlyDictionary<string, object> DefaultValues
{
get
{
var values = new Dictionary<string, object>();
if (Version < CompatibilityVersion.Version_2_2)
{
// Default to true in 2.1 or earlier. In 2.2, we have to conditionally enable this
// and consequently this switch has no default value.
values[nameof(RazorViewEngineOptions.AllowRecompilingViewsOnFileChange)] = true;
}
return values;
}
}
public void Configure(RazorViewEngineOptions options)
{
if (options == null)
@ -41,6 +59,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
options.AreaViewLocationFormats.Add("/Areas/{2}/Views/{1}/{0}" + RazorViewEngine.ViewExtension);
options.AreaViewLocationFormats.Add("/Areas/{2}/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
options.AreaViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
if (_hostingEnvironment.IsDevelopment())
{
options.AllowRecompilingViewsOnFileChange = true;
}
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.Extensions.FileProviders;
@ -18,11 +19,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
private readonly IFileProvider _fileProvider;
private readonly string[] _searchPatterns;
private readonly string[] _additionalFilesToTrack;
private readonly bool _watchForChanges;
public PageActionDescriptorChangeProvider(
RazorTemplateEngine templateEngine,
IRazorViewEngineFileProviderAccessor fileProviderAccessor,
IOptions<RazorPagesOptions> razorPagesOptions)
IOptions<RazorPagesOptions> razorPagesOptions,
IOptions<RazorViewEngineOptions> razorViewEngineOptions)
{
if (templateEngine == null)
{
@ -39,6 +42,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
throw new ArgumentNullException(nameof(razorPagesOptions));
}
_watchForChanges = razorViewEngineOptions.Value.AllowRecompilingViewsOnFileChange;
if (!_watchForChanges)
{
// No need to do any additional work if we aren't going to be watching for file changes.
return;
}
_fileProvider = fileProviderAccessor.FileProvider;
var rootDirectory = razorPagesOptions.Value.RootDirectory;
@ -84,6 +94,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public IChangeToken GetChangeToken()
{
if (!_watchForChanges)
{
return NullChangeToken.Singleton;
}
var changeTokens = new IChangeToken[_additionalFilesToTrack.Length + _searchPatterns.Length];
for (var i = 0; i < _additionalFilesToTrack.Length; i++)
{

View File

@ -0,0 +1,131 @@
// Copyright (c) .NET Foundation. 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.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
// Verifies that updating Razor files (views and pages) with AllowRecompilingViewsOnFileChange=true works
public class RazorFileUpdateTests : IClassFixture<MvcTestFixture<RazorWebSite.Startup>>
{
public RazorFileUpdateTests(MvcTestFixture<RazorWebSite.Startup> fixture)
{
var factory = fixture.WithWebHostBuilder(builder =>
{
builder.UseStartup<RazorWebSite.Startup>();
builder.ConfigureTestServices(services =>
{
services.Configure<RazorViewEngineOptions>(options => options.AllowRecompilingViewsOnFileChange = true);
});
});
Client = factory.CreateDefaultClient();
}
public HttpClient Client { get; }
[Fact]
public async Task RazorViews_AreUpdatedOnChange()
{
// Arrange
var expected1 = "Original content";
var expected2 = "New content";
var path = "/Views/UpdateableShared/_Partial.cshtml";
// Act - 1
var body = await Client.GetStringAsync("/UpdateableFileProvider");
// Assert - 1
Assert.Equal(expected1, body.Trim(), ignoreLineEndingDifferences: true);
// Act - 2
await UpdateFile(path, expected2);
body = await Client.GetStringAsync("/UpdateableFileProvider");
// Assert - 2
Assert.Equal(expected2, body.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public async Task RazorViews_AreUpdatedWhenViewImportsChange()
{
// Arrange
var content = "@GetType().Assembly.FullName";
await UpdateFile("/Views/UpdateableIndex/Index.cshtml", content);
var initial = await Client.GetStringAsync("/UpdateableFileProvider");
// Act
// Trigger a change in ViewImports
await UpdateFile("/Views/UpdateableIndex/_ViewImports.cshtml", string.Empty);
var updated = await Client.GetStringAsync("/UpdateableFileProvider");
// Assert
Assert.NotEqual(initial, updated);
}
[Fact]
public async Task RazorPages_AreUpdatedOnChange()
{
// Arrange
var expected1 = "Original content";
var expected2 = "New content";
// Act - 1
var body = await Client.GetStringAsync("/UpdateablePage");
// Assert - 1
Assert.Equal(expected1, body.Trim(), ignoreLineEndingDifferences: true);
// Act - 2
await UpdateRazorPages();
await UpdateFile("/Pages/UpdateablePage.cshtml", "@page" + Environment.NewLine + expected2);
body = await Client.GetStringAsync("/UpdateablePage");
// Assert - 2
Assert.Equal(expected2, body.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public async Task RazorPages_AreUpdatedWhenViewImportsChange()
{
// Arrange
var content = "@GetType().Assembly.FullName";
await UpdateFile("/Pages/UpdateablePage.cshtml", "@page" + Environment.NewLine + content);
var initial = await Client.GetStringAsync("/UpdateablePage");
// Act
// Trigger a change in ViewImports
await UpdateRazorPages();
await UpdateFile("/Pages/UpdateablePage.cshtml", "@page" + Environment.NewLine + content);
var updated = await Client.GetStringAsync("/UpdateablePage");
// Assert
Assert.NotEqual(initial, updated);
}
private async Task UpdateFile(string path, string content)
{
var updateContent = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "path", path },
{ "content", content },
});
var response = await Client.PostAsync($"/UpdateableFileProvider/Update", updateContent);
response.EnsureSuccessStatusCode();
}
private async Task UpdateRazorPages()
{
var response = await Client.PostAsync($"/UpdateableFileProvider/UpdateRazorPages", new StringContent(string.Empty));
response.EnsureSuccessStatusCode();
}
}
}

View File

@ -270,28 +270,6 @@ Hello from Shared/_EmbeddedPartial
Assert.Equal(expected, body.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public async Task RazorViewEngine_UpdatesViewsReferencedViaRelativePathsOnChange()
{
// Arrange
var expected1 = "Original content";
var expected2 = "New content";
// Act - 1
var body = await Client.GetStringAsync("/UpdateableFileProvider");
// Assert - 1
Assert.Equal(expected1, body.Trim(), ignoreLineEndingDifferences: true);
// Act - 2
var response = await Client.PostAsync("/UpdateableFileProvider/Update", new StringContent(string.Empty));
response.EnsureSuccessStatusCode();
body = await Client.GetStringAsync("/UpdateableFileProvider");
// Assert - 1
Assert.Equal(expected2, body.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public async Task LayoutValueIsPassedBetweenNestedViewStarts()
{

View File

@ -37,6 +37,25 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var result1 = await viewCompiler.CompileAsync(path);
var result2 = await viewCompiler.CompileAsync(path);
// Assert
Assert.Same(result1, result2);
Assert.Null(result1.ViewAttribute);
Assert.Empty(result1.ExpirationTokens);
}
[Fact]
public async Task CompileAsync_ReturnsResultWithExpirationToken_WhenWatchingForFileChanges()
{
// Arrange
var path = "/file/does-not-exist";
var fileProvider = new TestFileProvider();
var viewCompiler = GetViewCompiler(fileProvider);
viewCompiler.AllowRecompilingViewsOnFileChange = true;
// Act
var result1 = await viewCompiler.CompileAsync(path);
var result2 = await viewCompiler.CompileAsync(path);
// Assert
Assert.Same(result1, result2);
Assert.Null(result1.ViewAttribute);
@ -57,6 +76,24 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Act
var result = await viewCompiler.CompileAsync(path);
// Assert
Assert.NotNull(result.ViewAttribute);
Assert.Empty(result.ExpirationTokens);
}
[Fact]
public async Task CompileAsync_AddsChangeTokensForViewStartsIfFileExists_WhenWatchingForFileChanges()
{
// Arrange
var path = "/file/exists/FilePath.cshtml";
var fileProvider = new TestFileProvider();
fileProvider.AddFile(path, "Content");
var viewCompiler = GetViewCompiler(fileProvider);
viewCompiler.AllowRecompilingViewsOnFileChange = true;
// Act
var result = await viewCompiler.CompileAsync(path);
// Assert
Assert.NotNull(result.ViewAttribute);
Assert.Collection(
@ -92,13 +129,40 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
}
[Fact]
public async Task CompileAsync_InvalidatesCache_IfChangeTokenExpires()
public async Task CompileAsync_DoesNotInvalidCache_IfChangeTokenChanges()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = fileProvider.AddFile(path, "some content");
var viewCompiler = GetViewCompiler(fileProvider);
var changeToken = fileProvider.Watch(path);
// Act 1
var result1 = await viewCompiler.CompileAsync(path);
// Assert 1
Assert.NotNull(result1.ViewAttribute);
// Act 2
fileProvider.DeleteFile(path);
fileProvider.GetChangeToken(path).HasChanged = true;
viewCompiler.Compile = _ => throw new Exception("Can't call me");
var result2 = await viewCompiler.CompileAsync(path);
// Assert 2
Assert.Same(result1, result2);
}
[Fact]
public async Task CompileAsync_InvalidatesCache_IfChangeTokenExpires_WhenWatchingForFileChanges()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = fileProvider.AddFile(path, "some content");
var viewCompiler = GetViewCompiler(fileProvider);
viewCompiler.AllowRecompilingViewsOnFileChange = true;
// Act 1
var result1 = await viewCompiler.CompileAsync(path);
@ -125,6 +189,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var fileProvider = new TestFileProvider();
var fileInfo = fileProvider.AddFile(path, "some content");
var viewCompiler = GetViewCompiler(fileProvider);
viewCompiler.AllowRecompilingViewsOnFileChange = true;
var expected2 = new CompiledViewDescriptor();
// Act 1
@ -151,6 +216,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var fileProvider = new TestFileProvider();
var fileInfo = fileProvider.AddFile(path, "some content");
var viewCompiler = GetViewCompiler(fileProvider);
viewCompiler.AllowRecompilingViewsOnFileChange = true;
var expected2 = new CompiledViewDescriptor();
// Act 1
@ -327,6 +393,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
};
var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
viewCompiler.AllowRecompilingViewsOnFileChange = true;
// Act
var result = await viewCompiler.CompileAsync(path);
@ -371,7 +438,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
}
[Fact]
public async Task CompileAsync_PrecompiledViewWithChecksum_CanRecompile()
public async Task CompileAsync_PrecompiledViewWithChecksum_DoesNotAddExpirationTokens()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
@ -392,11 +459,43 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
// Act
var result = await viewCompiler.CompileAsync(path);
// Assert
Assert.Same(precompiledView.Item, result.Item);
Assert.Empty(result.ExpirationTokens);
}
[Fact]
public async Task CompileAsync_PrecompiledViewWithChecksum_CanRecompile()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = fileProvider.AddFile(path, "some content");
var expected2 = new CompiledViewDescriptor();
var precompiledView = new CompiledViewDescriptor
{
RelativePath = path,
Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[]
{
new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path),
}),
};
var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
viewCompiler.AllowRecompilingViewsOnFileChange = true;
// Act - 1
var result = await viewCompiler.CompileAsync(path);
// Assert - 1
Assert.Same(precompiledView.Item, result.Item);
Assert.NotEmpty(result.ExpirationTokens);
// Act - 2
fileInfo.Content = "some other content";
@ -427,6 +526,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
};
var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
viewCompiler.AllowRecompilingViewsOnFileChange = true;
// Act - 1
var result = await viewCompiler.CompileAsync(path);
@ -463,6 +563,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
};
var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
viewCompiler.AllowRecompilingViewsOnFileChange = true;
viewCompiler.Compile = _ => expected1;
// Act - 1
@ -504,6 +605,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
};
var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
viewCompiler.AllowRecompilingViewsOnFileChange = true;
// Act - 1
var result = await viewCompiler.CompileAsync(path);

View File

@ -2,7 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
@ -19,7 +23,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var hostingEnv = new Mock<IHostingEnvironment>();
hostingEnv.SetupGet(e => e.ContentRootFileProvider)
.Returns(expected);
var optionsSetup = new RazorViewEngineOptionsSetup(hostingEnv.Object);
var optionsSetup = GetSetup(hostingEnvironment: hostingEnv.Object);
// Act
optionsSetup.Configure(options);
@ -28,5 +33,128 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var fileProvider = Assert.Single(options.FileProviders);
Assert.Same(expected, fileProvider);
}
[Fact]
public void PostConfigure_SetsAllowRecompilingViewsOnFileChange_For21()
{
// Arrange
var options = new RazorViewEngineOptions();
var optionsSetup = GetSetup(CompatibilityVersion.Version_2_1);
// Act
optionsSetup.Configure(options);
optionsSetup.PostConfigure(string.Empty, options);
// Assert
Assert.True(options.AllowRecompilingViewsOnFileChange);
}
[Theory]
[InlineData(CompatibilityVersion.Version_2_2)]
[InlineData(CompatibilityVersion.Latest)]
public void PostConfigure_SetsAllowRecompilingViewsOnFileChange_InDevelopmentMode(CompatibilityVersion compatibilityVersion)
{
// Arrange
var options = new RazorViewEngineOptions();
var hostingEnv = Mock.Of<IHostingEnvironment>(env => env.EnvironmentName == EnvironmentName.Development);
var optionsSetup = GetSetup(compatibilityVersion, hostingEnv);
// Act
optionsSetup.Configure(options);
optionsSetup.PostConfigure(string.Empty, options);
// Assert
Assert.True(options.AllowRecompilingViewsOnFileChange);
}
[Theory]
[InlineData(CompatibilityVersion.Version_2_2)]
[InlineData(CompatibilityVersion.Latest)]
public void PostConfigure_DoesNotSetAllowRecompilingViewsOnFileChange_WhenNotInDevelopment(CompatibilityVersion compatibilityVersion)
{
// Arrange
var options = new RazorViewEngineOptions();
var hostingEnv = Mock.Of<IHostingEnvironment>(env => env.EnvironmentName == EnvironmentName.Staging);
var optionsSetup = GetSetup(compatibilityVersion, hostingEnv);
// Act
optionsSetup.Configure(options);
optionsSetup.PostConfigure(string.Empty, options);
// Assert
Assert.False(options.AllowRecompilingViewsOnFileChange);
}
[Fact]
public void RazorViewEngineOptionsSetup_DoesNotOverwriteAllowRecompilingViewsOnFileChange_In21CompatMode()
{
// Arrange
var hostingEnv = Mock.Of<IHostingEnvironment>(env => env.EnvironmentName == EnvironmentName.Staging);
var compatibilityVersion = new MvcCompatibilityOptions { CompatibilityVersion = CompatibilityVersion.Version_2_1 };
var optionsSetup = GetSetup(CompatibilityVersion.Version_2_1, hostingEnv);
var serviceProvider = new ServiceCollection()
.AddOptions()
.AddSingleton<IConfigureOptions<RazorViewEngineOptions>>(optionsSetup)
.Configure<RazorViewEngineOptions>(o => o.AllowRecompilingViewsOnFileChange = false)
.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>();
// Assert
Assert.False(options.Value.AllowRecompilingViewsOnFileChange);
}
[Fact]
public void RazorViewEngineOptionsSetup_ConfiguresAllowRecompilingViewsOnFileChange()
{
// Arrange
var hostingEnv = Mock.Of<IHostingEnvironment>(env => env.EnvironmentName == EnvironmentName.Production);
var compatibilityVersion = new MvcCompatibilityOptions { CompatibilityVersion = CompatibilityVersion.Version_2_2 };
var optionsSetup = GetSetup(CompatibilityVersion.Version_2_2, hostingEnv);
var serviceProvider = new ServiceCollection()
.AddOptions()
.AddSingleton<IConfigureOptions<RazorViewEngineOptions>>(optionsSetup)
.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>();
// Assert
Assert.False(options.Value.AllowRecompilingViewsOnFileChange);
}
[Fact]
public void RazorViewEngineOptionsSetup_DoesNotOverwriteAllowRecompilingViewsOnFileChange()
{
// Arrange
var hostingEnv = Mock.Of<IHostingEnvironment>(env => env.EnvironmentName == EnvironmentName.Production);
var optionsSetup = GetSetup(CompatibilityVersion.Version_2_2, hostingEnv);
var serviceProvider = new ServiceCollection()
.AddOptions()
.AddSingleton<IConfigureOptions<RazorViewEngineOptions>>(optionsSetup)
.Configure<RazorViewEngineOptions>(o => o.AllowRecompilingViewsOnFileChange = true)
.BuildServiceProvider();
// Act
var options = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>();
// Assert
Assert.True(options.Value.AllowRecompilingViewsOnFileChange);
}
private static RazorViewEngineOptionsSetup GetSetup(
CompatibilityVersion compatibilityVersion = CompatibilityVersion.Latest,
IHostingEnvironment hostingEnvironment = null)
{
hostingEnvironment = hostingEnvironment ?? Mock.Of<IHostingEnvironment>();
var compatibilityOptions = new MvcCompatibilityOptions { CompatibilityVersion = compatibilityVersion };
return new RazorViewEngineOptionsSetup(
hostingEnvironment,
NullLoggerFactory.Instance,
Options.Create(compatibilityOptions));
}
}
}

View File

@ -8,6 +8,7 @@ using System.Threading;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Routing;
@ -1979,7 +1980,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
IEnumerable<string> areaViewLocationFormats = null,
IEnumerable<string> pageViewLocationFormats = null)
{
var optionsSetup = new RazorViewEngineOptionsSetup(Mock.Of<IHostingEnvironment>());
var optionsSetup = new RazorViewEngineOptionsSetup(
Mock.Of<IHostingEnvironment>(),
NullLoggerFactory.Instance,
Options.Create(new MvcCompatibilityOptions()));
var options = new RazorViewEngineOptions();
optionsSetup.Configure(options);

View File

@ -30,8 +30,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var templateEngine = new RazorTemplateEngine(
RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem).Engine,
fileSystem);
var options = Options.Create(new RazorPagesOptions());
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options);
var razorPageOptions = Options.Create(new RazorPagesOptions());
var razorViewEngineOptions = Options.Create(new RazorViewEngineOptions { AllowRecompilingViewsOnFileChange = true });
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, razorPageOptions, razorViewEngineOptions);
// Act
var changeToken = changeProvider.GetChangeToken();
@ -57,8 +58,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
fileSystem);
var options = Options.Create(new RazorPagesOptions());
options.Value.RootDirectory = rootDirectory;
var razorViewEngineOptions = Options.Create(new RazorViewEngineOptions { AllowRecompilingViewsOnFileChange = true });
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options);
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options, razorViewEngineOptions);
// Act
var changeToken = changeProvider.GetChangeToken();
@ -81,7 +83,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem).Engine,
fileSystem);
var options = Options.Create(new RazorPagesOptions { AllowAreas = true });
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options);
var razorViewEngineOptions = Options.Create(new RazorViewEngineOptions { AllowRecompilingViewsOnFileChange = true });
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options, razorViewEngineOptions);
// Act
var changeToken = changeProvider.GetChangeToken();
@ -104,8 +107,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
templateEngine.Options.ImportsFileName = "_ViewImports.cshtml";
var options = Options.Create(new RazorPagesOptions());
options.Value.RootDirectory = "/dir1/dir2";
var razorViewEngineOptions = Options.Create(new RazorViewEngineOptions { AllowRecompilingViewsOnFileChange = true });
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options);
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options, razorViewEngineOptions);
// Act & Assert
var compositeChangeToken = Assert.IsType<CompositeChangeToken>(changeProvider.GetChangeToken());
@ -131,7 +135,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
options.Value.RootDirectory = "/dir1/dir2";
options.Value.AllowAreas = true;
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options);
var razorViewEngineOptions = Options.Create(new RazorViewEngineOptions { AllowRecompilingViewsOnFileChange = true });
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options, razorViewEngineOptions);
// Act & Assert
var compositeChangeToken = Assert.IsType<CompositeChangeToken>(changeProvider.GetChangeToken());
@ -155,8 +160,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
fileSystem);
templateEngine.Options.ImportsFileName = "_ViewImports.cshtml";
var options = Options.Create(new RazorPagesOptions { AllowAreas = false });
var razorViewEngineOptions = Options.Create(new RazorViewEngineOptions { AllowRecompilingViewsOnFileChange = true });
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options);
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options, razorViewEngineOptions);
// Act & Assert
var compositeChangeToken = Assert.IsType<CompositeChangeToken>(changeProvider.GetChangeToken());
@ -164,5 +170,27 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
changeToken => Assert.Same(fileProvider.GetChangeToken("/_ViewImports.cshtml"), changeToken),
changeToken => Assert.Same(fileProvider.GetChangeToken("/Pages/**/*.cshtml"), changeToken));
}
[Fact]
public void GetChangeToken_DoesNotWatch_WhenOptionIsReset()
{
// Arrange
var fileProvider = new Mock<IFileProvider>(MockBehavior.Strict);
var accessor = Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == fileProvider.Object);
var fileSystem = new FileProviderRazorProjectFileSystem(accessor, _hostingEnvironment);
var templateEngine = new RazorTemplateEngine(
RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem).Engine,
fileSystem);
templateEngine.Options.ImportsFileName = "_ViewImports.cshtml";
var options = Options.Create(new RazorPagesOptions());
var razorViewEngineOptions = Options.Create(new RazorViewEngineOptions());
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options, razorViewEngineOptions);
// Act & Assert
var compositeChangeToken = Assert.IsType<NullChangeToken>(changeProvider.GetChangeToken());
fileProvider.Verify(f => f.Watch(It.IsAny<string>()), Times.Never());
}
}
}

View File

@ -2,9 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
@ -186,7 +187,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
private static RazorViewEngineOptions GetViewEngineOptions()
{
var defaultSetup = new RazorViewEngineOptionsSetup(Mock.Of<IHostingEnvironment>());
var defaultSetup = new RazorViewEngineOptionsSetup(
Mock.Of<IHostingEnvironment>(),
NullLoggerFactory.Instance,
Options.Create(new MvcCompatibilityOptions()));
var options = new RazorViewEngineOptions();
defaultSetup.Configure(options);

View File

@ -1,11 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.IntegrationTest
@ -32,6 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().Value;
var razorPagesOptions = services.GetRequiredService<IOptions<RazorPagesOptions>>().Value;
var apiBehaviorOptions = services.GetRequiredService<IOptions<ApiBehaviorOptions>>().Value;
var razorViewEngineOptions = services.GetRequiredService<IOptions<RazorViewEngineOptions>>().Value;
// Assert
Assert.False(mvcOptions.AllowCombiningAuthorizeFilters);
@ -44,6 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.Null(mvcOptions.MaxValidationDepth);
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.True(apiBehaviorOptions.SuppressMapClientErrors);
Assert.True(razorViewEngineOptions.AllowRecompilingViewsOnFileChange);
}
[Fact]
@ -61,6 +66,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().Value;
var razorPagesOptions = services.GetRequiredService<IOptions<RazorPagesOptions>>().Value;
var apiBehaviorOptions = services.GetRequiredService<IOptions<ApiBehaviorOptions>>().Value;
var razorViewEngineOptions = services.GetRequiredService<IOptions<RazorViewEngineOptions>>().Value;
// Assert
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
@ -73,6 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.Null(mvcOptions.MaxValidationDepth);
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.True(apiBehaviorOptions.SuppressMapClientErrors);
Assert.True(razorViewEngineOptions.AllowRecompilingViewsOnFileChange);
}
[Fact]
@ -90,6 +97,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().Value;
var razorPagesOptions = services.GetRequiredService<IOptions<RazorPagesOptions>>().Value;
var apiBehaviorOptions = services.GetRequiredService<IOptions<ApiBehaviorOptions>>().Value;
var razorViewEngineOptions = services.GetRequiredService<IOptions<RazorViewEngineOptions>>().Value;
// Assert
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
@ -102,6 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.Equal(32, mvcOptions.MaxValidationDepth);
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.False(apiBehaviorOptions.SuppressMapClientErrors);
Assert.False(razorViewEngineOptions.AllowRecompilingViewsOnFileChange);
}
[Fact]
@ -119,6 +128,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().Value;
var razorPagesOptions = services.GetRequiredService<IOptions<RazorPagesOptions>>().Value;
var apiBehaviorOptions = services.GetRequiredService<IOptions<ApiBehaviorOptions>>().Value;
var razorViewEngineOptions = services.GetRequiredService<IOptions<RazorViewEngineOptions>>().Value;
// Assert
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
@ -131,11 +141,13 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.Equal(32, mvcOptions.MaxValidationDepth);
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.False(apiBehaviorOptions.SuppressMapClientErrors);
Assert.False(razorViewEngineOptions.AllowRecompilingViewsOnFileChange);
}
// This just does the minimum needed to be able to resolve these options.
private static void AddHostingServices(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton(Mock.Of<IHostingEnvironment>());
serviceCollection.AddLogging();
serviceCollection.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
}

View File

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. 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.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
namespace RazorPagesWebSite
{
public class NonWatchingPhysicalFileProvider : PhysicalFileProvider, IFileProvider
{
public NonWatchingPhysicalFileProvider(string root) : base(root)
{
}
IChangeToken IFileProvider.Watch(string filter) => throw new ArgumentException("This method should not be called.");
}
}

View File

@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesWebSite.Conventions;
@ -11,11 +12,18 @@ namespace RazorPagesWebSite
{
public class StartupWithBasePath
{
private readonly IHostingEnvironment _hostingEnvironment;
public StartupWithBasePath(IHostingEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => options.LoginPath = "/Login");
services.AddMvc()
var builder = services.AddMvc()
.AddCookieTempDataProvider()
.AddRazorPagesOptions(options =>
{
@ -27,6 +35,14 @@ namespace RazorPagesWebSite
options.Conventions.Add(new CustomModelTypeConvention());
})
.SetCompatibilityVersion(CompatibilityVersion.Latest);
// Ensure we don't have code paths that call IFileProvider.Watch in the default code path.
// Comment this code block if you happen to run this site in Development.
builder.AddRazorOptions(options =>
{
options.FileProviders.Clear();
options.FileProviders.Add(new NonWatchingPhysicalFileProvider(_hostingEnvironment.ContentRootPath));
});
}
public void Configure(IApplicationBuilder app)

View File

@ -10,9 +10,16 @@ namespace RazorWebSite
public IActionResult Index() => View("/Views/UpdateableIndex/Index.cshtml");
[HttpPost]
public IActionResult Update([FromServices] UpdateableFileProvider fileProvider)
public IActionResult Update([FromServices] UpdateableFileProvider fileProvider, string path, string content)
{
fileProvider.UpdateContent("/Views/UpdateableShared/_Partial.cshtml", "New content");
fileProvider.UpdateContent(path, content);
return Ok();
}
[HttpPost]
public IActionResult UpdateRazorPages([FromServices] UpdateableFileProvider fileProvider)
{
fileProvider.CancelRazorPages();
return Ok();
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
@ -13,8 +14,14 @@ namespace RazorWebSite
{
public class UpdateableFileProvider : IFileProvider
{
public CancellationTokenSource _pagesTokenSource = new CancellationTokenSource();
private readonly Dictionary<string, TestFileInfo> _content = new Dictionary<string, TestFileInfo>()
{
{
"/Views/UpdateableIndex/_ViewImports.cshtml",
new TestFileInfo(string.Empty)
},
{
"/Views/UpdateableIndex/Index.cshtml",
new TestFileInfo(@"@Html.Partial(""../UpdateableShared/_Partial.cshtml"")")
@ -23,9 +30,21 @@ namespace RazorWebSite
"/Views/UpdateableShared/_Partial.cshtml",
new TestFileInfo("Original content")
},
{
"/Pages/UpdateablePage.cshtml",
new TestFileInfo("@page" + Environment.NewLine + "Original content")
},
};
public IDirectoryContents GetDirectoryContents(string subpath) => new NotFoundDirectoryContents();
public IDirectoryContents GetDirectoryContents(string subpath)
{
if (subpath == "/Pages")
{
return new PagesDirectoryContents();
}
return new NotFoundDirectoryContents();
}
public void UpdateContent(string subpath, string content)
{
@ -34,10 +53,16 @@ namespace RazorWebSite
_content[subpath] = new TestFileInfo(content);
}
public void CancelRazorPages()
{
var oldToken = _pagesTokenSource;
_pagesTokenSource = new CancellationTokenSource();
oldToken.Cancel();
}
public IFileInfo GetFileInfo(string subpath)
{
TestFileInfo fileInfo;
if (!_content.TryGetValue(subpath, out fileInfo))
if (!_content.TryGetValue(subpath, out var fileInfo))
{
fileInfo = new TestFileInfo(null);
}
@ -47,8 +72,12 @@ namespace RazorWebSite
public IChangeToken Watch(string filter)
{
TestFileInfo fileInfo;
if (_content.TryGetValue(filter, out fileInfo))
if (filter == "/Pages/**/*.cshtml")
{
return new CancellationChangeToken(_pagesTokenSource.Token);
}
if (_content.TryGetValue(filter, out var fileInfo))
{
return fileInfo.ChangeToken;
}
@ -71,7 +100,7 @@ namespace RazorWebSite
public bool IsDirectory => false;
public DateTimeOffset LastModified => DateTimeOffset.MinValue;
public long Length => -1;
public string Name => null;
public string Name { get; set; }
public string PhysicalPath => null;
public CancellationTokenSource TokenSource { get; } = new CancellationTokenSource();
public CancellationChangeToken ChangeToken { get; }
@ -81,5 +110,23 @@ namespace RazorWebSite
return new MemoryStream(Encoding.UTF8.GetBytes(_content));
}
}
private class PagesDirectoryContents : IDirectoryContents
{
public bool Exists => true;
public IEnumerator<IFileInfo> GetEnumerator()
{
var file = new TestFileInfo("@page" + Environment.NewLine + "Original content")
{
Name = "UpdateablePage.cshtml"
};
var files = new List<IFileInfo> { file };
return files.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}