diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs index 0422c39e69..09fb648d47 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs @@ -120,7 +120,14 @@ private GetOrAddResult GetOrAddCore(string relativePath, var fileInfo = _fileProvider.GetFileInfo(relativePath); if (!fileInfo.Exists) { - return null; + // Assigning to IsValidatedPreCompiled is an atomic operation and will result in a safe race + // if it is being concurrently updated and read. + cacheEntry.IsValidatedPreCompiled = true; + return new GetOrAddResult + { + CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType), + CompilerCacheEntry = cacheEntry + }; } var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs index 2633a64a81..4263c350b3 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs @@ -162,6 +162,57 @@ public async Task PrecompiledView_RendersCorrectly() File.WriteAllText(Path.Combine(viewsDirectory, "_GlobalImport.cshtml"), globalContent.TrimEnd(' ')); } } + + [Fact] + public async Task PrecompiledView_RendersWithoutSourceFile() + { + // Arrange + IServiceCollection serviceCollection = null; + var server = TestHelper.CreateServer(_app, SiteName, services => + { + _configureServices(services); + serviceCollection = services; + }); + var client = server.CreateClient(); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var applicationEnvironment = serviceProvider.GetRequiredService(); + + var viewsDirectory = Path.Combine(applicationEnvironment.ApplicationBasePath, "Views", "Home"); + var layoutContent = File.ReadAllText(Path.Combine(viewsDirectory, "Layout.cshtml")); + var indexContent = File.ReadAllText(Path.Combine(viewsDirectory, "Index.cshtml")); + var viewstartContent = File.ReadAllText(Path.Combine(viewsDirectory, "_ViewStart.cshtml")); + var globalContent = File.ReadAllText(Path.Combine(viewsDirectory, "_GlobalImport.cshtml")); + + // We will render a view that writes the fully qualified name of the Assembly containing the type of + // the view. If the view is precompiled, this assembly will be PrecompilationWebsite. + var assemblyNamePrefix = GetAssemblyNamePrefix(); + + try + { + // Act + await DeleteFile(viewsDirectory, "_ViewStart.cshtml"); + await DeleteFile(viewsDirectory, "_Layout.cshtml"); + await DeleteFile(viewsDirectory, "Index.cshtml"); + + var response = await client.GetAsync("http://localhost/Home/Index"); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var parsedResponse1 = new ParsedResponse(responseContent); + Assert.StartsWith(assemblyNamePrefix, parsedResponse1.ViewStart); + Assert.StartsWith(assemblyNamePrefix, parsedResponse1.Layout); + Assert.StartsWith(assemblyNamePrefix, parsedResponse1.Index); + } + finally + { + File.WriteAllText(Path.Combine(viewsDirectory, "Layout.cshtml"), layoutContent.TrimEnd(' ')); + File.WriteAllText(Path.Combine(viewsDirectory, "Index.cshtml"), indexContent.TrimEnd(' ')); + File.WriteAllText(Path.Combine(viewsDirectory, "_ViewStart.cshtml"), viewstartContent.TrimEnd(' ')); + File.WriteAllText(Path.Combine(viewsDirectory, "_GlobalImport.cshtml"), globalContent.TrimEnd(' ')); + } + } [Fact] public async Task PrecompiledView_UsesCompilationOptionsFromApplication() @@ -284,6 +335,17 @@ private static async Task TouchFile(string viewsDir, string file) return path; } + + private static async Task DeleteFile(string viewsDir, string file) + { + var path = Path.Combine(viewsDir, file); + File.Delete(path); + + // Delay to allow the file system watcher to catch up. + await Task.Delay(_cacheDelayInterval); + + return path; + } private sealed class ParsedResponse { diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs index 43526b2d91..f83caf1cbb 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs @@ -293,7 +293,7 @@ public void GetOrAdd_UsesValueFromCache_IfGlobalHasNotChanged() } [Fact] - public void GetOrAdd_ReturnsFileNotFoundResult_IfPrecompiledViewWasRemovedFromFileSystem() + public void GetOrAdd_ReturnsPrecompiledView_IfPrecompiledViewWasRemovedFromFileSystem() { // Arrange var precompiledViews = new ViewCollection(); @@ -305,8 +305,8 @@ public void GetOrAdd_ReturnsFileNotFoundResult_IfPrecompiledViewWasRemovedFromFi compile: _ => { throw new Exception("shouldn't be invoked"); }); // Assert - Assert.Same(CompilerCacheResult.FileNotFound, result); - Assert.Null(result.CompilationResult); + Assert.NotSame(CompilerCacheResult.FileNotFound, result); + Assert.NotNull(result.CompilationResult); } [Fact]