diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionDescriptorCollectionProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionDescriptorCollectionProvider.cs index 1090a11a97..49cb219178 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionDescriptorCollectionProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionDescriptorCollectionProvider.cs @@ -43,6 +43,11 @@ public ActionDescriptorCollectionProvider( private IChangeToken GetCompositeChangeToken() { + if (_actionDescriptorChangeProviders.Length == 1) + { + return _actionDescriptorChangeProviders[0].GetChangeToken(); + } + var changeTokens = new IChangeToken[_actionDescriptorChangeProviders.Length]; for (var i = 0; i < _actionDescriptorChangeProviders.Length; i++) { diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionDescriptorChangeProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionDescriptorChangeProvider.cs index 7e5ea4a82a..635de93e85 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionDescriptorChangeProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionDescriptorChangeProvider.cs @@ -2,8 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; +using System.Linq; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -14,11 +17,18 @@ public class PageActionDescriptorChangeProvider : IActionDescriptorChangeProvide { private readonly IFileProvider _fileProvider; private readonly string _searchPattern; + private readonly string[] _additionalFilesToTrack; public PageActionDescriptorChangeProvider( + RazorTemplateEngine templateEngine, IRazorViewEngineFileProviderAccessor fileProviderAccessor, IOptions razorPagesOptions) { + if (templateEngine == null) + { + throw new ArgumentNullException(nameof(templateEngine)); + } + if (fileProviderAccessor == null) { throw new ArgumentNullException(nameof(fileProviderAccessor)); @@ -30,9 +40,35 @@ public PageActionDescriptorChangeProvider( } _fileProvider = fileProviderAccessor.FileProvider; - _searchPattern = razorPagesOptions.Value.RootDirectory.TrimEnd('/') + "/**/*.cshtml"; + + var rootDirectory = razorPagesOptions.Value.RootDirectory; + Debug.Assert(!string.IsNullOrEmpty(rootDirectory)); + rootDirectory = rootDirectory.TrimEnd('/'); + + var importFileAtPagesRoot = rootDirectory + "/" + templateEngine.Options.ImportsFileName; + _additionalFilesToTrack = templateEngine.GetImportItems(importFileAtPagesRoot) + .Select(item => item.Path) + .ToArray(); + + _searchPattern = rootDirectory + "/**/*.cshtml"; } - public IChangeToken GetChangeToken() => _fileProvider.Watch(_searchPattern); + public IChangeToken GetChangeToken() + { + var wildcardChangeToken = _fileProvider.Watch(_searchPattern); + if (_additionalFilesToTrack.Length == 0) + { + return wildcardChangeToken; + } + + var changeTokens = new IChangeToken[_additionalFilesToTrack.Length + 1]; + for (var i = 0; i < _additionalFilesToTrack.Length; i++) + { + changeTokens[i] = _fileProvider.Watch(_additionalFilesToTrack[i]); + } + + changeTokens[changeTokens.Length - 1] = wildcardChangeToken; + return new CompositeChangeToken(changeTokens); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionDescriptorChangeProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionDescriptorChangeProviderTest.cs index ecf5f140e7..0d318a6fbf 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionDescriptorChangeProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionDescriptorChangeProviderTest.cs @@ -1,8 +1,11 @@ // 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.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; using Moq; using Xunit; @@ -14,13 +17,16 @@ public class PageActionDescriptorChangeProviderTest public void GetChangeToken_WatchesAllCshtmlFilesUnderFileSystemRoot() { // Arrange - var options = new TestOptionsManager(); var fileProvider = new Mock(); + var templateEngine = new RazorTemplateEngine( + RazorEngine.Create(), + new FileProviderRazorProject(fileProvider.Object)); + var options = new TestOptionsManager(); var fileProviderAccessor = new Mock(); fileProviderAccessor .Setup(f => f.FileProvider) .Returns(fileProvider.Object); - var changeProvider = new PageActionDescriptorChangeProvider(fileProviderAccessor.Object, options); + var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, fileProviderAccessor.Object, options); // Act var changeToken = changeProvider.GetChangeToken(); @@ -35,14 +41,17 @@ public void GetChangeToken_WatchesAllCshtmlFilesUnderFileSystemRoot() public void GetChangeToken_WatchesAllCshtmlFilesUnderSpecifiedRootDirectory(string rootDirectory) { // Arrange + var fileProvider = new Mock(); + var templateEngine = new RazorTemplateEngine( + RazorEngine.Create(), + new FileProviderRazorProject(fileProvider.Object)); var options = new TestOptionsManager(); options.Value.RootDirectory = rootDirectory; - var fileProvider = new Mock(); var fileProviderAccessor = new Mock(); fileProviderAccessor .Setup(f => f.FileProvider) .Returns(fileProvider.Object); - var changeProvider = new PageActionDescriptorChangeProvider(fileProviderAccessor.Object, options); + var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, fileProviderAccessor.Object, options); // Act var changeToken = changeProvider.GetChangeToken(); @@ -50,5 +59,30 @@ public void GetChangeToken_WatchesAllCshtmlFilesUnderSpecifiedRootDirectory(stri // Assert fileProvider.Verify(f => f.Watch("/pages-base-dir/**/*.cshtml")); } + + [Fact] + public void GetChangeToken_WatchesViewImportsOutsidePagesRoot() + { + // Arrange + var fileProvider = new TestFileProvider(); + var templateEngine = new RazorTemplateEngine( + RazorEngine.Create(), + new FileProviderRazorProject(fileProvider)); + templateEngine.Options.ImportsFileName = "_ViewImports.cshtml"; + var options = new TestOptionsManager(); + options.Value.RootDirectory = "/dir1/dir2"; + var fileProviderAccessor = new Mock(); + fileProviderAccessor + .Setup(f => f.FileProvider) + .Returns(fileProvider); + var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, fileProviderAccessor.Object, options); + + // Act & Assert + var compositeChangeToken = Assert.IsType(changeProvider.GetChangeToken()); + Assert.Collection(compositeChangeToken.ChangeTokens, + changeToken => Assert.Same(fileProvider.GetChangeToken("/dir1/_ViewImports.cshtml"), changeToken), + changeToken => Assert.Same(fileProvider.GetChangeToken("/_ViewImports.cshtml"), changeToken), + changeToken => Assert.Same(fileProvider.GetChangeToken("/dir1/dir2/**/*.cshtml"), changeToken)); + } } }