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:
parent
b156dee4f1
commit
07cc9e66c6
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue