Jelajahi Sumber

Convert DatabaseErrorPage to exception filter (#24588)

* Convert DatabaseErrorPage middleware to exception filter
John Luo 5 tahun lalu
induk
melakukan
cfe158cbed
40 mengubah file dengan 1099 tambahan dan 405 penghapusan
  1. 2 1
      src/Identity/ApiAuthorization.IdentityServer/samples/ApiAuthSample/Startup.cs
  2. 2 1
      src/Identity/samples/IdentitySample.DefaultUI/Startup.cs
  3. 2 1
      src/Identity/samples/IdentitySample.Mvc/Startup.cs
  4. 2 1
      src/Identity/testassets/Identity.DefaultUI.WebSite/NoIdentityStartup.cs
  5. 4 3
      src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs
  6. 1 1
      src/Identity/testassets/Identity.DefaultUI.WebSite/StartupWithoutEndpointRouting.cs
  7. 24 0
      src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseContextDetails.cs
  8. 76 0
      src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilter.cs
  9. 34 0
      src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilterServiceExtensions.cs
  10. 3 0
      src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageExtensions.cs
  11. 11 79
      src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageMiddleware.cs
  12. 1 11
      src/Middleware/Diagnostics.EntityFrameworkCore/src/DiagnosticsEntityFrameworkCoreLoggerExtensions.cs
  13. 87 0
      src/Middleware/Diagnostics.EntityFrameworkCore/src/HttpContextDatabaseContextDetailsExtensions.cs
  14. 1 0
      src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj
  15. 15 19
      src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointMiddleware.cs
  16. 12 9
      src/Middleware/Diagnostics.EntityFrameworkCore/src/Strings.resx
  17. 295 111
      src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPage.Designer.cs
  18. 86 56
      src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPage.cshtml
  19. 9 14
      src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPageModel.cs
  20. 23 5
      src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/DatabaseErrorPageMiddlewareTest.cs
  21. 2 2
      src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/MigrationsEndPointMiddlewareTest.cs
  22. 117 53
      src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseErrorPageTest.cs
  23. 13 7
      src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Helpers/AssertHelpers.cs
  24. 8 3
      src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Helpers/StringHelpers.cs
  25. 2 0
      src/Middleware/Diagnostics/test/testassets/DatabaseErrorPageSample/Startup.cs
  26. 216 0
      src/Middleware/tools/RazorPageGenerator/Program.cs
  27. 18 0
      src/Middleware/tools/RazorPageGenerator/RazorPageGenerator.csproj
  28. 12 0
      src/Middleware/tools/RazorPageGenerator/RazorPageGeneratorResults.cs
  29. 2 2
      src/MusicStore/samples/MusicStore/ForTesting/Mocks/StartupOpenIdConnectTesting.cs
  30. 1 2
      src/MusicStore/samples/MusicStore/ForTesting/Mocks/StartupSocialTesting.cs
  31. 2 2
      src/MusicStore/samples/MusicStore/Startup.cs
  32. 2 1
      src/MusicStore/samples/MusicStore/StartupNtlmAuthentication.cs
  33. 2 2
      src/MusicStore/samples/MusicStore/StartupOpenIdConnect.cs
  34. 1 3
      src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs
  35. 2 3
      src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Startup.cs
  36. 1 3
      src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs
  37. 2 3
      src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs
  38. 2 3
      src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Startup.cs
  39. 2 3
      src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/Startup.cs
  40. 2 1
      src/Security/samples/Identity.ExternalClaims/Startup.cs

+ 2 - 1
src/Identity/ApiAuthorization.IdentityServer/samples/ApiAuthSample/Startup.cs

@@ -40,6 +40,8 @@ namespace ApiAuthSample
 
             services.AddMvc()
                 .AddNewtonsoftJson();
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -48,7 +50,6 @@ namespace ApiAuthSample
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-                app.UseDatabaseErrorPage();
             }
             else
             {

+ 2 - 1
src/Identity/samples/IdentitySample.DefaultUI/Startup.cs

@@ -49,6 +49,8 @@ namespace IdentitySample.DefaultUI
             services.AddDefaultIdentity<ApplicationUser>(o => o.SignIn.RequireConfirmedAccount = true)
                  .AddRoles<IdentityRole>()
                  .AddEntityFrameworkStores<ApplicationDbContext>();
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
 
@@ -58,7 +60,6 @@ namespace IdentitySample.DefaultUI
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-                app.UseDatabaseErrorPage();
             }
             else
             {

+ 2 - 1
src/Identity/samples/IdentitySample.Mvc/Startup.cs

@@ -53,6 +53,8 @@ namespace IdentitySample
             // Add application services.
             services.AddTransient<IEmailSender, AuthMessageSender>();
             services.AddTransient<ISmsSender, AuthMessageSender>();
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -61,7 +63,6 @@ namespace IdentitySample
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-                app.UseDatabaseErrorPage();
             }
             else
             {

+ 2 - 1
src/Identity/testassets/Identity.DefaultUI.WebSite/NoIdentityStartup.cs

@@ -37,6 +37,8 @@ namespace Identity.DefaultUI.WebSite
                     options.Conventions.AuthorizePage("/Areas/Identity/Pages/Account/Logout");
                 })
                 .AddNewtonsoftJson();
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -47,7 +49,6 @@ namespace Identity.DefaultUI.WebSite
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-                app.UseDatabaseErrorPage();
             }
             else
             {

+ 4 - 3
src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs

@@ -49,9 +49,11 @@ namespace Identity.DefaultUI.WebSite
             services.AddDefaultIdentity<TUser>()
                 .AddRoles<IdentityRole>()
                 .AddEntityFrameworkStores<TContext>();
-                
+
             services.AddMvc();
             services.AddSingleton<IFileVersionProvider, FileVersionProvider>();
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -59,11 +61,10 @@ namespace Identity.DefaultUI.WebSite
         {
             // This prevents running out of file watchers on some linux machines
             DisableFilePolling(env);
-        
+
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-                app.UseDatabaseErrorPage();
             }
             else
             {

+ 1 - 1
src/Identity/testassets/Identity.DefaultUI.WebSite/StartupWithoutEndpointRouting.cs

@@ -22,6 +22,7 @@ namespace Identity.DefaultUI.WebSite
         {
             base.ConfigureServices(services);
             services.AddMvc(options => options.EnableEndpointRouting = false);
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -33,7 +34,6 @@ namespace Identity.DefaultUI.WebSite
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-                app.UseDatabaseErrorPage();
             }
             else
             {

+ 24 - 0
src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseContextDetails.cs

@@ -0,0 +1,24 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
+{
+    internal class DatabaseContextDetails
+    {
+        public Type Type { get; }
+        public bool DatabaseExists { get; }
+        public bool PendingModelChanges { get; }
+        public IEnumerable<string> PendingMigrations { get; }
+
+        public DatabaseContextDetails(Type type, bool databaseExists, bool pendingModelChanges, IEnumerable<string> pendingMigrations)
+        {
+            Type = type;
+            DatabaseExists = databaseExists;
+            PendingModelChanges = pendingModelChanges;
+            PendingMigrations = pendingMigrations;
+        }
+    }
+}

+ 76 - 0
src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilter.cs

@@ -0,0 +1,76 @@
+// 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.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+#nullable enable
+namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
+{
+    public sealed class DatabaseDeveloperPageExceptionFilter : IDeveloperPageExceptionFilter
+    {
+        private readonly ILogger _logger;
+        private readonly DatabaseErrorPageOptions _options;
+
+        public DatabaseDeveloperPageExceptionFilter(ILogger<DatabaseDeveloperPageExceptionFilter> logger, IOptions<DatabaseErrorPageOptions> options)
+        {
+            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+            _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
+        }
+
+        public async Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next)
+        {
+            if (!(errorContext.Exception is DbException))
+            {
+                await next(errorContext);
+            }
+
+            try
+            {
+                // Look for DbContext classes registered in the service provider
+                var registeredContexts = errorContext.HttpContext.RequestServices.GetServices<DbContextOptions>()
+                    .Select(o => o.ContextType);
+
+                if (registeredContexts.Any())
+                {
+                    var contextDetails = new List<DatabaseContextDetails>();
+
+                    foreach (var registeredContext in registeredContexts)
+                    {
+                        var details = await errorContext.HttpContext.GetContextDetailsAsync(registeredContext, _logger);
+
+                        if (details != null)
+                        {
+                            contextDetails.Add(details);
+                        }
+                    }
+
+                    if (contextDetails.Any(c => c.PendingModelChanges || c.PendingMigrations.Any()))
+                    {
+                        var page = new DatabaseErrorPage
+                        {
+                            Model = new DatabaseErrorPageModel(errorContext.Exception, contextDetails, _options, errorContext.HttpContext.Request.PathBase)
+                        };
+
+                        await page.ExecuteAsync(errorContext.HttpContext);
+                        return;
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                _logger.DatabaseErrorPageMiddlewareException(e);
+                return;
+            }
+        }
+    }
+}

+ 34 - 0
src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilterServiceExtensions.cs

@@ -0,0 +1,34 @@
+// 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.AspNetCore.Diagnostics;
+using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+#nullable enable
+namespace Microsoft.Extensions.DependencyInjection
+{
+    /// <summary>
+    /// Service extension methods for the <see cref="DatabaseDeveloperPageExceptionFilter"/>.
+    /// </summary>
+    public static class DatabaseDeveloperPageExceptionFilterServiceExtensions
+    {
+        /// <summary>
+        /// Add response caching services.
+        /// </summary>
+        /// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
+        /// <returns></returns>
+        public static IServiceCollection AddDatabaseDeveloperPageExceptionFilter(this IServiceCollection services)
+        {
+            if (services == null)
+            {
+                throw new ArgumentNullException(nameof(services));
+            }
+
+            services.TryAddEnumerable(new ServiceDescriptor(typeof(IDeveloperPageExceptionFilter), typeof(DatabaseDeveloperPageExceptionFilter), ServiceLifetime.Singleton));
+
+            return services;
+        }
+    }
+}

+ 3 - 0
src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageExtensions.cs

@@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Builder
     /// <summary>
     /// <see cref="IApplicationBuilder"/> extension methods for the <see cref="DatabaseErrorPageMiddleware"/>.
     /// </summary>
+    [Obsolete("This is obsolete and will be removed in a future version. Use DatabaseDeveloperPageExceptionFilter instead, see documentation at https://aka.ms/DatabaseDeveloperPageExceptionFilter.")]
     public static class DatabaseErrorPageExtensions
     {
         /// <summary>
@@ -19,6 +20,7 @@ namespace Microsoft.AspNetCore.Builder
         /// </summary>
         /// <param name="app">The <see cref="IApplicationBuilder"/> to register the middleware with.</param>
         /// <returns>The same <see cref="IApplicationBuilder"/> instance so that multiple calls can be chained.</returns>
+        [Obsolete("This is obsolete and will be removed in a future version. Use DatabaseDeveloperPageExceptionFilter instead, see documentation at https://aka.ms/DatabaseDeveloperPageExceptionFilter.")]
         public static IApplicationBuilder UseDatabaseErrorPage(this IApplicationBuilder app)
         {
             if (app == null)
@@ -36,6 +38,7 @@ namespace Microsoft.AspNetCore.Builder
         /// <param name="app">The <see cref="IApplicationBuilder"/> to register the middleware with.</param>
         /// <param name="options">A <see cref="DatabaseErrorPageOptions"/> that specifies options for the middleware.</param>
         /// <returns>The same <see cref="IApplicationBuilder"/> instance so that multiple calls can be chained.</returns>
+        [Obsolete("This is obsolete and will be removed in a future version. Use DatabaseDeveloperPageExceptionFilter instead, see documentation at https://aka.ms/DatabaseDeveloperPageExceptionFilter.")]
         public static IApplicationBuilder UseDatabaseErrorPage(
             this IApplicationBuilder app, DatabaseErrorPageOptions options)
         {

+ 11 - 79
src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageMiddleware.cs

@@ -12,12 +12,6 @@ using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views;
 using Microsoft.AspNetCore.Http;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Diagnostics;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Metadata.Conventions;
-using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
 
@@ -56,6 +50,7 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
         ///     consumes them to detect database related exception.
         /// </param>
         /// <param name="options">The options to control what information is displayed on the error page.</param>
+        [Obsolete("This is obsolete and will be removed in a future version. Use DatabaseDeveloperPageExceptionFilter instead, see documentation at https://aka.ms/DatabaseDeveloperPageExceptionFilter.")]
         public DatabaseErrorPageMiddleware(
             RequestDelegate next,
             ILoggerFactory loggerFactory,
@@ -101,7 +96,7 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
             {
                 // Because CallContext is cloned at each async operation we cannot
                 // lazily create the error object when an error is encountered, otherwise
-                // it will not be available to code outside of the current async context. 
+                // it will not be available to code outside of the current async context.
                 // We create it ahead of time so that any cloning just clones the reference
                 // to the object that will hold any errors.
 
@@ -116,81 +111,18 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
                     if (ShouldDisplayErrorPage(exception))
                     {
                         var contextType = _localDiagnostic.Value.ContextType;
-                        var context = (DbContext)httpContext.RequestServices.GetService(contextType);
+                        var details = await httpContext.GetContextDetailsAsync(contextType, _logger);
 
-                        if (context == null)
+                        if (details != null && (details.PendingModelChanges || details.PendingMigrations.Count() > 0))
                         {
-                            _logger.ContextNotRegisteredDatabaseErrorPageMiddleware(contextType.FullName);
-                        }
-                        else
-                        {
-                            var relationalDatabaseCreator = context.GetService<IDatabaseCreator>() as IRelationalDatabaseCreator;
-                            if (relationalDatabaseCreator == null)
-                            {
-                                _logger.NotRelationalDatabase();
-                            }
-                            else
+                            var page = new DatabaseErrorPage
                             {
-                                var databaseExists = await relationalDatabaseCreator.ExistsAsync();
-
-                                if (databaseExists)
-                                {
-                                    databaseExists = await relationalDatabaseCreator.HasTablesAsync();
-                                }
-
-                                var migrationsAssembly = context.GetService<IMigrationsAssembly>();
-                                var modelDiffer = context.GetService<IMigrationsModelDiffer>();
-
-                                var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
-                                if (snapshotModel is IConventionModel conventionModel)
-                                {
-                                    var conventionSet = context.GetService<IConventionSetBuilder>().CreateConventionSet();
-
-                                    var typeMappingConvention = conventionSet.ModelFinalizingConventions.OfType<TypeMappingConvention>().FirstOrDefault();
-                                    if (typeMappingConvention != null)
-                                    {
-                                        typeMappingConvention.ProcessModelFinalizing(conventionModel.Builder, null);
-                                    }
-
-                                    var relationalModelConvention = conventionSet.ModelFinalizedConventions.OfType<RelationalModelConvention>().FirstOrDefault();
-                                    if (relationalModelConvention != null)
-                                    {
-                                        snapshotModel = relationalModelConvention.ProcessModelFinalized(conventionModel);
-                                    }
-                                }
-
-                                if (snapshotModel is IMutableModel mutableModel)
-                                {
-                                    snapshotModel = mutableModel.FinalizeModel();
-                                }
-
-                                // HasDifferences will return true if there is no model snapshot, but if there is an existing database
-                                // and no model snapshot then we don't want to show the error page since they are most likely targeting
-                                // and existing database and have just misconfigured their model
-
-                                var pendingModelChanges
-                                    = (!databaseExists || migrationsAssembly.ModelSnapshot != null)
-                                      && modelDiffer.HasDifferences(snapshotModel?.GetRelationalModel(), context.Model.GetRelationalModel());
-
-                                var pendingMigrations
-                                    = (databaseExists
-                                        ? await context.Database.GetPendingMigrationsAsync()
-                                        : context.Database.GetMigrations())
-                                    .ToArray();
-
-                                if (pendingModelChanges || pendingMigrations.Length > 0)
-                                {
-                                    var page = new DatabaseErrorPage
-                                    {
-                                        Model = new DatabaseErrorPageModel(
-                                            contextType, exception, databaseExists, pendingModelChanges, pendingMigrations, _options)
-                                    };
-
-                                    await page.ExecuteAsync(httpContext);
-
-                                    return;
-                                }
-                            }
+                                Model = new DatabaseErrorPageModel(exception, new DatabaseContextDetails[] { details }, _options, httpContext.Request.PathBase)
+                            };
+
+                            await page.ExecuteAsync(httpContext);
+
+                            return;
                         }
                     }
                 }

+ 1 - 11
src/Middleware/Diagnostics.EntityFrameworkCore/src/DiagnosticsEntityFrameworkCoreLoggerExtensions.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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;
@@ -14,11 +14,6 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
             new EventId(1, "NoContextType"),
             "No context type was specified. Ensure the form data from the request includes a 'context' value, specifying the context type name to apply migrations for.");
 
-        private static readonly Action<ILogger, string, Exception> _invalidContextType = LoggerMessage.Define<string>(
-            LogLevel.Error,
-            new EventId(2, "InvalidContextType"),
-            "The context type '{ContextTypeName}' could not be loaded. Ensure this is the correct type name for the context you are trying to apply migrations for.");
-
         private static readonly Action<ILogger, string, Exception> _contextNotRegistered = LoggerMessage.Define<string>(
             LogLevel.Error,
             new EventId(3, "ContextNotRegistered"),
@@ -85,11 +80,6 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
             _noContextType(logger, null);
         }
 
-        public static void InvalidContextType(this ILogger logger, string contextTypeName)
-        {
-            _invalidContextType(logger, contextTypeName, null);
-        }
-
         public static void ContextNotRegistered(this ILogger logger, string contextTypeName)
         {
             _contextNotRegistered(logger, contextTypeName, null);

+ 87 - 0
src/Middleware/Diagnostics.EntityFrameworkCore/src/HttpContextDatabaseContextDetailsExtensions.cs

@@ -0,0 +1,87 @@
+// 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.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Metadata.Conventions;
+using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+#nullable enable
+namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
+{
+    internal static class HttpContextDatabaseContextDetailsExtensions
+    {
+        public static async ValueTask<DatabaseContextDetails?> GetContextDetailsAsync(this HttpContext httpContext, Type dbcontextType, ILogger logger)
+        {
+            var context = (DbContext?)httpContext.RequestServices.GetService(dbcontextType);
+
+            if (context == null)
+            {
+                logger.ContextNotRegisteredDatabaseErrorPageMiddleware(dbcontextType.FullName);
+                return null;
+            }
+
+            var relationalDatabaseCreator = context.GetService<IDatabaseCreator>() as IRelationalDatabaseCreator;
+            if (relationalDatabaseCreator == null)
+            {
+                logger.NotRelationalDatabase();
+                return null;
+            }
+
+            var databaseExists = await relationalDatabaseCreator.ExistsAsync();
+
+            if (databaseExists)
+            {
+                databaseExists = await relationalDatabaseCreator.HasTablesAsync();
+            }
+
+            var migrationsAssembly = context.GetService<IMigrationsAssembly>();
+            var modelDiffer = context.GetService<IMigrationsModelDiffer>();
+
+            var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
+            if (snapshotModel is IConventionModel conventionModel)
+            {
+                var conventionSet = context.GetService<IConventionSetBuilder>().CreateConventionSet();
+
+                var typeMappingConvention = conventionSet.ModelFinalizingConventions.OfType<TypeMappingConvention>().FirstOrDefault();
+                if (typeMappingConvention != null)
+                {
+                    typeMappingConvention.ProcessModelFinalizing(conventionModel.Builder, null);
+                }
+
+                var relationalModelConvention = conventionSet.ModelFinalizedConventions.OfType<RelationalModelConvention>().FirstOrDefault();
+                if (relationalModelConvention != null)
+                {
+                    snapshotModel = relationalModelConvention.ProcessModelFinalized(conventionModel);
+                }
+            }
+
+            if (snapshotModel is IMutableModel mutableModel)
+            {
+                snapshotModel = mutableModel.FinalizeModel();
+            }
+
+            // HasDifferences will return true if there is no model snapshot, but if there is an existing database
+            // and no model snapshot then we don't want to show the error page since they are most likely targeting
+            // and existing database and have just misconfigured their model
+
+            return new DatabaseContextDetails(
+                type: dbcontextType,
+                databaseExists: databaseExists,
+                pendingModelChanges: (!databaseExists || migrationsAssembly.ModelSnapshot != null)
+                    && modelDiffer.HasDifferences(snapshotModel?.GetRelationalModel(), context.Model.GetRelationalModel()),
+                pendingMigrations: databaseExists
+                    ? await context.Database.GetPendingMigrationsAsync()
+                    : context.Database.GetMigrations());
+        }
+    }
+}

+ 1 - 0
src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj

@@ -14,6 +14,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Diagnostics.Abstractions" />
     <Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
     <Reference Include="Microsoft.EntityFrameworkCore.Relational" />
   </ItemGroup>

+ 15 - 19
src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointMiddleware.cs

@@ -2,11 +2,13 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
+using System.Linq;
 using System.Net;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Http;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
 
@@ -72,9 +74,10 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
 
                 if (db != null)
                 {
+                    var dbName = db.GetType().FullName;
                     try
                     {
-                        _logger.ApplyingMigrations(db.GetType().FullName);
+                        _logger.ApplyingMigrations(dbName);
 
                         await db.Database.MigrateAsync();
 
@@ -82,13 +85,13 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
                         context.Response.Headers.Add("Pragma", new[] { "no-cache" });
                         context.Response.Headers.Add("Cache-Control", new[] { "no-cache,no-store" });
 
-                        _logger.MigrationsApplied(db.GetType().FullName);
+                        _logger.MigrationsApplied(dbName);
                     }
                     catch (Exception ex)
                     {
-                        var message = Strings.FormatMigrationsEndPointMiddleware_Exception(db.GetType().FullName) + ex;
+                        var message = Strings.FormatMigrationsEndPointMiddleware_Exception(dbName) + ex;
 
-                        _logger.MigrationsEndPointMiddlewareException(db.GetType().FullName, ex);
+                        _logger.MigrationsEndPointMiddlewareException(dbName, ex);
 
                         throw new InvalidOperationException(message, ex);
                     }
@@ -114,31 +117,24 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
                 return null;
             }
 
-            var contextType = Type.GetType(contextTypeName);
+            // Look for DbContext classes registered in the service provider
+            var registeredContexts = context.RequestServices.GetServices<DbContextOptions>()
+                .Select(o => o.ContextType);
 
-            if (contextType == null)
+            if (!registeredContexts.Any(c => string.Equals(contextTypeName, c.AssemblyQualifiedName)))
             {
-                var message = Strings.FormatMigrationsEndPointMiddleware_InvalidContextType(contextTypeName);
+                var message = Strings.FormatMigrationsEndPointMiddleware_ContextNotRegistered(contextTypeName);
 
-                logger.InvalidContextType(contextTypeName);
+                logger.ContextNotRegistered(contextTypeName);
 
                 await WriteErrorToResponse(context.Response, message);
 
                 return null;
             }
 
-            var db = (DbContext)context.RequestServices.GetService(contextType);
-
-            if (db == null)
-            {
-                var message = Strings.FormatMigrationsEndPointMiddleware_ContextNotRegistered(contextType.FullName);
-
-                logger.ContextNotRegistered(contextType.FullName);
-
-                await WriteErrorToResponse(context.Response, message);
+            var contextType = Type.GetType(contextTypeName);
 
-                return null;
-            }
+            var db = (DbContext)context.RequestServices.GetService(contextType);
 
             return db;
         }

+ 12 - 9
src/Middleware/Diagnostics.EntityFrameworkCore/src/Strings.resx

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <root>
   <!-- 
     Microsoft ResX Schema 
@@ -148,32 +148,29 @@
     <value>In Visual Studio, use the Package Manager Console to scaffold a new migration and apply it to the database:</value>
   </data>
   <data name="DatabaseErrorPage_NoDbOrMigrationsTitle" xml:space="preserve">
-    <value>Use migrations to create the database for {0}</value>
+    <value>Use migrations to create the database</value>
   </data>
   <data name="DatabaseErrorPage_PendingChangesInfoPMC" xml:space="preserve">
     <value>In Visual Studio, use the Package Manager Console to scaffold a new migration for these changes and apply them to the database:</value>
   </data>
   <data name="DatabaseErrorPage_PendingChangesTitle" xml:space="preserve">
-    <value>There are pending model changes for {0}</value>
+    <value>There are pending model changes</value>
   </data>
   <data name="DatabaseErrorPage_PendingMigrationsInfo" xml:space="preserve">
-    <value>There are migrations for {0} that have not been applied to the database</value>
+    <value>There are migrations that have not been applied to the following database(s):</value>
   </data>
   <data name="DatabaseErrorPage_PendingMigrationsTitle" xml:space="preserve">
-    <value>Applying existing migrations for {0} may resolve this issue</value>
+    <value>Applying existing migrations may resolve this issue</value>
   </data>
   <data name="DatabaseErrorPage_ApplyMigrationsCommandCLI" xml:space="preserve">
     <value>&gt; dotnet ef database update</value>
   </data>
   <data name="MigrationsEndPointMiddleware_ContextNotRegistered" xml:space="preserve">
-    <value>The context type '{0}' was not found in services. This usually means the context was not registered in services during startup. You probably want to call AddScoped&lt;{0}&gt;() inside the UseServices(...) call in your application startup code.</value>
+    <value>The context type '{0}' was not found in services. This usually means either the context is invalid or it was not registered in services during startup. You probably want to call AddDBContext&lt;&gt;() inside the ConfigureServices(...) call in your application startup code.</value>
   </data>
   <data name="MigrationsEndPointMiddleware_Exception" xml:space="preserve">
     <value>An error occurred while applying the migrations for '{0}'. See InnerException for details.</value>
   </data>
-  <data name="MigrationsEndPointMiddleware_InvalidContextType" xml:space="preserve">
-    <value>The context type '{0}' could not be loaded. Ensure this is the correct type name for the context you are trying to apply migrations for.</value>
-  </data>
   <data name="MigrationsEndPointMiddleware_NoContextType" xml:space="preserve">
     <value>No context type was specified. Ensure the form data from the request includes a 'context' value, specifying the context type name to apply migrations for.</value>
   </data>
@@ -198,4 +195,10 @@
   <data name="DatabaseErrorPage_HowToApplyFromCLI" xml:space="preserve">
     <value>Alternatively, you can apply pending migrations from a command prompt at your project directory:</value>
   </data>
+  <data name="DatabaseErrorPage_NoDbOrMigrationsInfo" xml:space="preserve">
+    <value>A database needs to be created for the following:</value>
+  </data>
+  <data name="DatabaseErrorPage_PendingChangesInfo" xml:space="preserve">
+    <value>Pending model changes are detected in the following:</value>
+  </data>
 </root>

+ 295 - 111
src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPage.Designer.cs

@@ -4,31 +4,40 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views
 {
     #line hidden
     using System.Threading.Tasks;
+#nullable restore
 #line 1 "DatabaseErrorPage.cshtml"
 using System;
 
 #line default
 #line hidden
+#nullable disable
+#nullable restore
 #line 2 "DatabaseErrorPage.cshtml"
 using System.Linq;
 
 #line default
 #line hidden
+#nullable disable
+#nullable restore
 #line 3 "DatabaseErrorPage.cshtml"
 using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore;
 
 #line default
 #line hidden
+#nullable disable
+#nullable restore
 #line 4 "DatabaseErrorPage.cshtml"
 using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views;
 
 #line default
 #line hidden
+#nullable disable
     internal class DatabaseErrorPage : Microsoft.Extensions.RazorViews.BaseView
     {
         #pragma warning disable 1998
         public async override global::System.Threading.Tasks.Task ExecuteAsync()
         {
+#nullable restore
 #line 5 "DatabaseErrorPage.cshtml"
   
     Response.StatusCode = 500;
@@ -37,6 +46,7 @@ using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views;
 
 #line default
 #line hidden
+#nullable disable
             WriteLiteral(@"<!DOCTYPE html>
 
 <html lang=""en"" xmlns=""http://www.w3.org/1999/xhtml"">
@@ -128,156 +138,265 @@ body .titleerror {
 </head>
 <body>
     <h1>");
+#nullable restore
 #line 113 "DatabaseErrorPage.cshtml"
    Write(Strings.DatabaseErrorPage_Title);
 
 #line default
 #line hidden
+#nullable disable
             WriteLiteral("</h1>\r\n    <p>\r\n");
+#nullable restore
 #line 115 "DatabaseErrorPage.cshtml"
          for (Exception ex = Model.Exception; ex != null; ex = ex.InnerException)
-            {
+        {
 
 #line default
 #line hidden
+#nullable disable
             WriteLiteral("            <span>");
+#nullable restore
 #line 117 "DatabaseErrorPage.cshtml"
              Write(ex.GetType().Name);
 
 #line default
 #line hidden
+#nullable disable
             WriteLiteral(": ");
+#nullable restore
 #line 117 "DatabaseErrorPage.cshtml"
                                  Write(ex.Message);
 
 #line default
 #line hidden
+#nullable disable
             WriteLiteral("</span>\r\n            <br />\r\n");
+#nullable restore
 #line 119 "DatabaseErrorPage.cshtml"
         }
 
 #line default
 #line hidden
+#nullable disable
             WriteLiteral("    </p>\r\n    <hr />\r\n\r\n");
+#nullable restore
 #line 123 "DatabaseErrorPage.cshtml"
-     if (!Model.DatabaseExists && !Model.PendingMigrations.Any())
-    {
+      
+        var contextWithNoDBOrMigrations = Model.ContextDetails.Where(c => !c.DatabaseExists && !c.PendingMigrations.Any());
+        if (contextWithNoDBOrMigrations.Any())
+        {
 
 #line default
 #line hidden
-            WriteLiteral("        <h2>");
-#line 125 "DatabaseErrorPage.cshtml"
-       Write(Strings.FormatDatabaseErrorPage_NoDbOrMigrationsTitle(Model.ContextType.Name));
+#nullable disable
+            WriteLiteral("            <div>\r\n                <h2>");
+#nullable restore
+#line 128 "DatabaseErrorPage.cshtml"
+               Write(Strings.DatabaseErrorPage_NoDbOrMigrationsTitle);
 
 #line default
 #line hidden
-            WriteLiteral("</h2>\r\n        <p>");
-#line 126 "DatabaseErrorPage.cshtml"
-      Write(Strings.DatabaseErrorPage_NoDbOrMigrationsInfoPMC);
+#nullable disable
+            WriteLiteral("</h2>\r\n                <p>");
+#nullable restore
+#line 129 "DatabaseErrorPage.cshtml"
+              Write(Strings.DatabaseErrorPage_NoDbOrMigrationsInfo);
 
 #line default
 #line hidden
-            WriteLiteral("</p>\r\n        <code> ");
-#line 127 "DatabaseErrorPage.cshtml"
-          Write(Strings.DatabaseErrorPage_AddMigrationCommandPMC);
+#nullable disable
+            WriteLiteral("</p>\r\n\r\n                <ul>\r\n");
+#nullable restore
+#line 132 "DatabaseErrorPage.cshtml"
+                     foreach (var context in contextWithNoDBOrMigrations)
+                    {
 
 #line default
 #line hidden
-            WriteLiteral("</code>\r\n        <br />\r\n        <code> ");
-#line 129 "DatabaseErrorPage.cshtml"
-          Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC);
+#nullable disable
+            WriteLiteral("                        <li>");
+#nullable restore
+#line 134 "DatabaseErrorPage.cshtml"
+                       Write(context.Type.Name);
 
 #line default
 #line hidden
-            WriteLiteral("</code>\r\n        <p>");
-#line 130 "DatabaseErrorPage.cshtml"
-      Write(Strings.DatabaseErrorPage_NoDbOrMigrationsInfoCLI);
+#nullable disable
+            WriteLiteral("</li>\r\n");
+#nullable restore
+#line 135 "DatabaseErrorPage.cshtml"
+                    }
 
 #line default
 #line hidden
-            WriteLiteral("</p>\r\n        <code> ");
-#line 131 "DatabaseErrorPage.cshtml"
-          Write(Strings.DatabaseErrorPage_AddMigrationCommandCLI);
+#nullable disable
+            WriteLiteral("                </ul>\r\n\r\n                <p>");
+#nullable restore
+#line 138 "DatabaseErrorPage.cshtml"
+              Write(Strings.DatabaseErrorPage_NoDbOrMigrationsInfoPMC);
 
 #line default
 #line hidden
-            WriteLiteral("</code>\r\n        <br />\r\n        <code> ");
-#line 133 "DatabaseErrorPage.cshtml"
-          Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI);
+#nullable disable
+            WriteLiteral("</p>\r\n                <code> ");
+#nullable restore
+#line 139 "DatabaseErrorPage.cshtml"
+                  Write(Strings.DatabaseErrorPage_AddMigrationCommandPMC);
 
 #line default
 #line hidden
-            WriteLiteral("</code>\r\n        <hr />\r\n");
-#line 135 "DatabaseErrorPage.cshtml"
-    }
-    else if (Model.PendingMigrations.Any())
-    {
+#nullable disable
+            WriteLiteral("</code>\r\n                <br />\r\n                <code> ");
+#nullable restore
+#line 141 "DatabaseErrorPage.cshtml"
+                  Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC);
 
 #line default
 #line hidden
-            WriteLiteral("        <div>\r\n            <h2>");
-#line 139 "DatabaseErrorPage.cshtml"
-           Write(Strings.FormatDatabaseErrorPage_PendingMigrationsTitle(Model.ContextType.Name));
+#nullable disable
+            WriteLiteral("</code>\r\n                <p>");
+#nullable restore
+#line 142 "DatabaseErrorPage.cshtml"
+              Write(Strings.DatabaseErrorPage_NoDbOrMigrationsInfoCLI);
 
 #line default
 #line hidden
-            WriteLiteral("</h2>\r\n            <p>");
-#line 140 "DatabaseErrorPage.cshtml"
-          Write(Strings.FormatDatabaseErrorPage_PendingMigrationsInfo(Model.ContextType.Name));
+#nullable disable
+            WriteLiteral("</p>\r\n                <code> ");
+#nullable restore
+#line 143 "DatabaseErrorPage.cshtml"
+                  Write(Strings.DatabaseErrorPage_AddMigrationCommandCLI);
 
 #line default
 #line hidden
-            WriteLiteral("</p>\r\n\r\n            <ul>\r\n");
-#line 143 "DatabaseErrorPage.cshtml"
-                 foreach (var migration in Model.PendingMigrations)
+#nullable disable
+            WriteLiteral("</code>\r\n                <br />\r\n                <code> ");
+#nullable restore
+#line 145 "DatabaseErrorPage.cshtml"
+                  Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI);
+
+#line default
+#line hidden
+#nullable disable
+            WriteLiteral("</code>\r\n                <hr />\r\n            </div>\r\n");
+#nullable restore
+#line 148 "DatabaseErrorPage.cshtml"
+        }
+
+        var contextWithPendingMigrations = Model.ContextDetails.Where(c => c.PendingMigrations.Any()).Except(contextWithNoDBOrMigrations);
+        if (contextWithPendingMigrations.Any())
+        {
+
+#line default
+#line hidden
+#nullable disable
+            WriteLiteral("            <div>\r\n                <h2>");
+#nullable restore
+#line 154 "DatabaseErrorPage.cshtml"
+               Write(Strings.DatabaseErrorPage_PendingMigrationsTitle);
+
+#line default
+#line hidden
+#nullable disable
+            WriteLiteral("</h2>\r\n                <p>");
+#nullable restore
+#line 155 "DatabaseErrorPage.cshtml"
+              Write(Strings.DatabaseErrorPage_PendingMigrationsInfo);
+
+#line default
+#line hidden
+#nullable disable
+            WriteLiteral("</p>\r\n\r\n");
+#nullable restore
+#line 157 "DatabaseErrorPage.cshtml"
+                 foreach (var context in contextWithPendingMigrations)
                 {
 
 #line default
 #line hidden
-            WriteLiteral("                    <li>");
-#line 145 "DatabaseErrorPage.cshtml"
-                   Write(migration);
+#nullable disable
+            WriteLiteral("                    <h3>");
+#nullable restore
+#line 159 "DatabaseErrorPage.cshtml"
+                   Write(context.Type.Name);
 
 #line default
 #line hidden
+#nullable disable
+            WriteLiteral("</h3>\r\n                    <ul>\r\n");
+#nullable restore
+#line 161 "DatabaseErrorPage.cshtml"
+                         foreach (var migration in context.PendingMigrations)
+                        {
+
+#line default
+#line hidden
+#nullable disable
+            WriteLiteral("                            <li>");
+#nullable restore
+#line 163 "DatabaseErrorPage.cshtml"
+                           Write(migration);
+
+#line default
+#line hidden
+#nullable disable
             WriteLiteral("</li>\r\n");
-#line 146 "DatabaseErrorPage.cshtml"
-                }
+#nullable restore
+#line 164 "DatabaseErrorPage.cshtml"
+                        }
 
 #line default
 #line hidden
-            WriteLiteral("            </ul>\r\n\r\n            <p>\r\n                <button id=\"applyMigrations\" onclick=\"ApplyMigrations()\">");
-#line 150 "DatabaseErrorPage.cshtml"
-                                                                    Write(Strings.DatabaseErrorPage_ApplyMigrationsButton);
+#nullable disable
+            WriteLiteral("                    </ul>\r\n");
+            WriteLiteral("                    <p>\r\n                        <button id=\"applyMigrations\" onclick=\"ApplyMigrations()\" data-assemblyname=\"");
+#nullable restore
+#line 168 "DatabaseErrorPage.cshtml"
+                                                                                               Write(JavaScriptEncode(context.Type.AssemblyQualifiedName));
 
 #line default
 #line hidden
-            WriteLiteral(@"</button>
-                <span id=""applyMigrationsError"" class=""error""></span>
-                <span id=""applyMigrationsSuccess""></span>
-            </p>
-            <script>
-                function ApplyMigrations() {
-                    applyMigrations.disabled = true;
-                    applyMigrationsError.innerHTML = """";
-                    applyMigrations.innerHTML = """);
-#line 158 "DatabaseErrorPage.cshtml"
+#nullable disable
+            WriteLiteral("\">");
+#nullable restore
+#line 168 "DatabaseErrorPage.cshtml"
+                                                                                                                                                      Write(Strings.DatabaseErrorPage_ApplyMigrationsButton);
+
+#line default
+#line hidden
+#nullable disable
+            WriteLiteral("</button>\r\n                        <span id=\"applyMigrationsError\" class=\"error\"></span>\r\n                        <span id=\"applyMigrationsSuccess\"></span>\r\n                    </p>\r\n");
+#nullable restore
+#line 172 "DatabaseErrorPage.cshtml"
+                }
+
+#line default
+#line hidden
+#nullable disable
+            WriteLiteral("\r\n                <script>\r\n                function ApplyMigrations() {\r\n                    applyMigrations.disabled = true;\r\n                    applyMigrationsError.innerHTML = \"\";\r\n                    applyMigrations.innerHTML = \"");
+#nullable restore
+#line 178 "DatabaseErrorPage.cshtml"
                                             Write(JavaScriptEncode(Strings.DatabaseErrorPage_ApplyMigrationsButtonRunning));
 
 #line default
 #line hidden
+#nullable disable
             WriteLiteral("\";\r\n\r\n                    var req = new XMLHttpRequest();\r\n\r\n                    req.onload = function (e) {\r\n                        if (req.status === 204) {\r\n                            applyMigrations.innerHTML = \"");
-#line 164 "DatabaseErrorPage.cshtml"
+#nullable restore
+#line 184 "DatabaseErrorPage.cshtml"
                                                     Write(JavaScriptEncode(Strings.DatabaseErrorPage_ApplyMigrationsButtonDone));
 
 #line default
 #line hidden
+#nullable disable
             WriteLiteral("\";\r\n                            applyMigrationsSuccess.innerHTML = \"");
-#line 165 "DatabaseErrorPage.cshtml"
+#nullable restore
+#line 185 "DatabaseErrorPage.cshtml"
                                                            Write(JavaScriptEncode(Strings.DatabaseErrorPage_MigrationsAppliedRefresh));
 
 #line default
 #line hidden
+#nullable disable
             WriteLiteral(@""";
                         } else {
                             ErrorApplyingMigrations();
@@ -288,18 +407,15 @@ body .titleerror {
                         ErrorApplyingMigrations();
                     };
 
-                    var formBody = ""context=");
-#line 175 "DatabaseErrorPage.cshtml"
-                                       Write(JavaScriptEncode(UrlEncode(Model.ContextType.AssemblyQualifiedName)));
-
-#line default
-#line hidden
-            WriteLiteral("\";\r\n                    req.open(\"POST\", \"");
-#line 176 "DatabaseErrorPage.cshtml"
-                                 Write(JavaScriptEncode(Model.Options.MigrationsEndPointPath.Value));
+                    var formBody = ""context="" + encodeURIComponent(document.getElementById('applyMigrations').getAttribute('data-assemblyname'));
+                    req.open(""POST"", """);
+#nullable restore
+#line 196 "DatabaseErrorPage.cshtml"
+                                 Write(JavaScriptEncode(Model.PathBase.Add(Model.Options.MigrationsEndPointPath).Value));
 
 #line default
 #line hidden
+#nullable disable
             WriteLiteral(@""", true);
                     req.setRequestHeader(""Content-type"", ""application/x-www-form-urlencoded"");
                     req.send(formBody);
@@ -307,100 +423,167 @@ body .titleerror {
 
                 function ErrorApplyingMigrations() {
                     applyMigrations.innerHTML = """);
-#line 182 "DatabaseErrorPage.cshtml"
+#nullable restore
+#line 202 "DatabaseErrorPage.cshtml"
                                             Write(JavaScriptEncode(Strings.DatabaseErrorPage_ApplyMigrationsButton));
 
 #line default
 #line hidden
+#nullable disable
             WriteLiteral("\";\r\n                    applyMigrationsError.innerHTML = \"");
-#line 183 "DatabaseErrorPage.cshtml"
+#nullable restore
+#line 203 "DatabaseErrorPage.cshtml"
                                                  Write(JavaScriptEncode(Strings.DatabaseErrorPage_ApplyMigrationsFailed));
 
 #line default
 #line hidden
-            WriteLiteral("\";\r\n                    applyMigrations.disabled = false;\r\n                }\r\n            </script>\r\n\r\n            <p>");
-#line 188 "DatabaseErrorPage.cshtml"
-          Write(Strings.DatabaseErrorPage_HowToApplyFromPMC);
+#nullable disable
+            WriteLiteral("\";\r\n                    applyMigrations.disabled = false;\r\n                }\r\n                </script>\r\n\r\n                <p>");
+#nullable restore
+#line 208 "DatabaseErrorPage.cshtml"
+              Write(Strings.DatabaseErrorPage_HowToApplyFromPMC);
 
 #line default
 #line hidden
-            WriteLiteral("</p>\r\n            <code>");
-#line 189 "DatabaseErrorPage.cshtml"
-             Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC);
+#nullable disable
+            WriteLiteral("</p>\r\n                <code>");
+#nullable restore
+#line 209 "DatabaseErrorPage.cshtml"
+                 Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC);
 
 #line default
 #line hidden
-            WriteLiteral("</code>\r\n            <p>");
-#line 190 "DatabaseErrorPage.cshtml"
-          Write(Strings.DatabaseErrorPage_HowToApplyFromCLI);
+#nullable disable
+            WriteLiteral("</code>\r\n                <p>");
+#nullable restore
+#line 210 "DatabaseErrorPage.cshtml"
+              Write(Strings.DatabaseErrorPage_HowToApplyFromCLI);
 
 #line default
 #line hidden
-            WriteLiteral("</p>\r\n            <code>");
-#line 191 "DatabaseErrorPage.cshtml"
-             Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI);
+#nullable disable
+            WriteLiteral("</p>\r\n                <code>");
+#nullable restore
+#line 211 "DatabaseErrorPage.cshtml"
+                 Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI);
 
 #line default
 #line hidden
-            WriteLiteral("</code>\r\n            <hr />\r\n        </div>\r\n");
-#line 194 "DatabaseErrorPage.cshtml"
-    }
-    else if (Model.PendingModelChanges)
-    {
+#nullable disable
+            WriteLiteral("</code>\r\n                <hr />\r\n            </div>\r\n");
+#nullable restore
+#line 214 "DatabaseErrorPage.cshtml"
+        }
+
+        var contextWithPendingModelChanges = Model.ContextDetails.Where(c => c.PendingModelChanges).Except(contextWithNoDBOrMigrations).Except(contextWithPendingMigrations);
+        if (contextWithPendingModelChanges.Any())
+        {
 
 #line default
 #line hidden
-            WriteLiteral("        <div>\r\n            <h2>");
-#line 198 "DatabaseErrorPage.cshtml"
-           Write(Strings.FormatDatabaseErrorPage_PendingChangesTitle(Model.ContextType.Name));
+#nullable disable
+            WriteLiteral("            <div>\r\n                <h2>");
+#nullable restore
+#line 220 "DatabaseErrorPage.cshtml"
+               Write(Strings.DatabaseErrorPage_PendingChangesTitle);
 
 #line default
 #line hidden
-            WriteLiteral("</h2>\r\n            <p>");
-#line 199 "DatabaseErrorPage.cshtml"
-          Write(Strings.DatabaseErrorPage_PendingChangesInfoPMC);
+#nullable disable
+            WriteLiteral("</h2>\r\n                <p>");
+#nullable restore
+#line 221 "DatabaseErrorPage.cshtml"
+              Write(Strings.DatabaseErrorPage_PendingChangesInfo);
 
 #line default
 #line hidden
-            WriteLiteral("</p>\r\n            <code>");
-#line 200 "DatabaseErrorPage.cshtml"
-             Write(Strings.DatabaseErrorPage_AddMigrationCommandPMC);
+#nullable disable
+            WriteLiteral("</p>\r\n                <ul>\r\n");
+#nullable restore
+#line 223 "DatabaseErrorPage.cshtml"
+                     foreach (var context in contextWithPendingModelChanges)
+                    {
 
 #line default
 #line hidden
-            WriteLiteral("</code>\r\n            <br />\r\n            <code>");
-#line 202 "DatabaseErrorPage.cshtml"
-             Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC);
+#nullable disable
+            WriteLiteral("                        <li>");
+#nullable restore
+#line 225 "DatabaseErrorPage.cshtml"
+                       Write(context.Type.Name);
 
 #line default
 #line hidden
-            WriteLiteral("</code>\r\n            <p>");
-#line 203 "DatabaseErrorPage.cshtml"
-          Write(Strings.DatabaseErrorPage_PendingChangesInfoCLI);
+#nullable disable
+            WriteLiteral("</li>\r\n");
+#nullable restore
+#line 226 "DatabaseErrorPage.cshtml"
+                    }
 
 #line default
 #line hidden
-            WriteLiteral("</p>\r\n            <code>");
-#line 204 "DatabaseErrorPage.cshtml"
-             Write(Strings.DatabaseErrorPage_AddMigrationCommandCLI);
+#nullable disable
+            WriteLiteral("                </ul>\r\n                <p>");
+#nullable restore
+#line 228 "DatabaseErrorPage.cshtml"
+              Write(Strings.DatabaseErrorPage_PendingChangesInfoPMC);
 
 #line default
 #line hidden
-            WriteLiteral("</code>\r\n            <br />\r\n            <code>");
-#line 206 "DatabaseErrorPage.cshtml"
-             Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI);
+#nullable disable
+            WriteLiteral("</p>\r\n                <code>");
+#nullable restore
+#line 229 "DatabaseErrorPage.cshtml"
+                 Write(Strings.DatabaseErrorPage_AddMigrationCommandPMC);
 
 #line default
 #line hidden
-            WriteLiteral("</code>\r\n            <hr />\r\n        </div>\r\n");
-#line 209 "DatabaseErrorPage.cshtml"
-    }
+#nullable disable
+            WriteLiteral("</code>\r\n                <br />\r\n                <code>");
+#nullable restore
+#line 231 "DatabaseErrorPage.cshtml"
+                 Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC);
+
+#line default
+#line hidden
+#nullable disable
+            WriteLiteral("</code>\r\n                <p>");
+#nullable restore
+#line 232 "DatabaseErrorPage.cshtml"
+              Write(Strings.DatabaseErrorPage_PendingChangesInfoCLI);
+
+#line default
+#line hidden
+#nullable disable
+            WriteLiteral("</p>\r\n                <code>");
+#nullable restore
+#line 233 "DatabaseErrorPage.cshtml"
+                 Write(Strings.DatabaseErrorPage_AddMigrationCommandCLI);
+
+#line default
+#line hidden
+#nullable disable
+            WriteLiteral("</code>\r\n                <br />\r\n                <code>");
+#nullable restore
+#line 235 "DatabaseErrorPage.cshtml"
+                 Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI);
+
+#line default
+#line hidden
+#nullable disable
+            WriteLiteral("</code>\r\n                <hr />\r\n            </div>\r\n");
+#nullable restore
+#line 238 "DatabaseErrorPage.cshtml"
+        }
+    
 
 #line default
 #line hidden
-            WriteLiteral("</body>\r\n</html>");
+#nullable disable
+            WriteLiteral("</body>\r\n</html>\r\n");
         }
         #pragma warning restore 1998
+#nullable restore
 #line 11 "DatabaseErrorPage.cshtml"
  
     public DatabaseErrorPageModel Model { get; set; }
@@ -417,6 +600,7 @@ body .titleerror {
 
 #line default
 #line hidden
+#nullable disable
     }
 }
 #pragma warning restore 1591

+ 86 - 56
src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPage.cshtml

@@ -35,45 +35,65 @@
     <h1>@Strings.DatabaseErrorPage_Title</h1>
     <p>
         @for (Exception ex = Model.Exception; ex != null; ex = ex.InnerException)
-            {
+        {
             <span>@ex.GetType().Name: @ex.Message</span>
             <br />
         }
     </p>
     <hr />
 
-    @if (!Model.DatabaseExists && !Model.PendingMigrations.Any())
-    {
-        <h2>@Strings.FormatDatabaseErrorPage_NoDbOrMigrationsTitle(Model.ContextType.Name)</h2>
-        <p>@Strings.DatabaseErrorPage_NoDbOrMigrationsInfoPMC</p>
-        <code> @Strings.DatabaseErrorPage_AddMigrationCommandPMC</code>
-        <br />
-        <code> @Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC</code>
-        <p>@Strings.DatabaseErrorPage_NoDbOrMigrationsInfoCLI</p>
-        <code> @Strings.DatabaseErrorPage_AddMigrationCommandCLI</code>
-        <br />
-        <code> @Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI</code>
-        <hr />
-    }
-    else if (Model.PendingMigrations.Any())
-    {
-        <div>
-            <h2>@Strings.FormatDatabaseErrorPage_PendingMigrationsTitle(Model.ContextType.Name)</h2>
-            <p>@Strings.FormatDatabaseErrorPage_PendingMigrationsInfo(Model.ContextType.Name)</p>
+    @{
+        var contextWithNoDBOrMigrations = Model.ContextDetails.Where(c => !c.DatabaseExists && !c.PendingMigrations.Any());
+        if (contextWithNoDBOrMigrations.Any())
+        {
+            <div>
+                <h2>@Strings.DatabaseErrorPage_NoDbOrMigrationsTitle</h2>
+                <p>@Strings.DatabaseErrorPage_NoDbOrMigrationsInfo</p>
 
-            <ul>
-                @foreach (var migration in Model.PendingMigrations)
+                <ul>
+                    @foreach (var context in contextWithNoDBOrMigrations)
+                    {
+                        <li>@context.Type.Name</li>
+                    }
+                </ul>
+
+                <p>@Strings.DatabaseErrorPage_NoDbOrMigrationsInfoPMC</p>
+                <code> @Strings.DatabaseErrorPage_AddMigrationCommandPMC</code>
+                <br />
+                <code> @Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC</code>
+                <p>@Strings.DatabaseErrorPage_NoDbOrMigrationsInfoCLI</p>
+                <code> @Strings.DatabaseErrorPage_AddMigrationCommandCLI</code>
+                <br />
+                <code> @Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI</code>
+                <hr />
+            </div>
+        }
+
+        var contextWithPendingMigrations = Model.ContextDetails.Where(c => c.PendingMigrations.Any()).Except(contextWithNoDBOrMigrations);
+        if (contextWithPendingMigrations.Any())
+        {
+            <div>
+                <h2>@Strings.DatabaseErrorPage_PendingMigrationsTitle</h2>
+                <p>@Strings.DatabaseErrorPage_PendingMigrationsInfo</p>
+
+                @foreach (var context in contextWithPendingMigrations)
                 {
-                    <li>@migration</li>
+                    <h3>@context.Type.Name</h3>
+                    <ul>
+                        @foreach (var migration in context.PendingMigrations)
+                        {
+                            <li>@migration</li>
+                        }
+                    </ul>
+
+                    <p>
+                        <button id="applyMigrations" onclick="ApplyMigrations()" data-assemblyname="@JavaScriptEncode(context.Type.AssemblyQualifiedName)">@Strings.DatabaseErrorPage_ApplyMigrationsButton</button>
+                        <span id="applyMigrationsError" class="error"></span>
+                        <span id="applyMigrationsSuccess"></span>
+                    </p>
                 }
-            </ul>
-
-            <p>
-                <button id="applyMigrations" onclick="ApplyMigrations()">@Strings.DatabaseErrorPage_ApplyMigrationsButton</button>
-                <span id="applyMigrationsError" class="error"></span>
-                <span id="applyMigrationsSuccess"></span>
-            </p>
-            <script>
+
+                <script>
                 function ApplyMigrations() {
                     applyMigrations.disabled = true;
                     applyMigrationsError.innerHTML = "";
@@ -94,8 +114,8 @@
                         ErrorApplyingMigrations();
                     };
 
-                    var formBody = "context=@JavaScriptEncode(UrlEncode(Model.ContextType.AssemblyQualifiedName))";
-                    req.open("POST", "@JavaScriptEncode(Model.Options.MigrationsEndPointPath.Value)", true);
+                    var formBody = "context=" + encodeURIComponent(document.getElementById('applyMigrations').getAttribute('data-assemblyname'));
+                    req.open("POST", "@JavaScriptEncode(Model.PathBase.Add(Model.Options.MigrationsEndPointPath).Value)", true);
                     req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
                     req.send(formBody);
                 }
@@ -105,29 +125,39 @@
                     applyMigrationsError.innerHTML = "@JavaScriptEncode(Strings.DatabaseErrorPage_ApplyMigrationsFailed)";
                     applyMigrations.disabled = false;
                 }
-            </script>
-
-            <p>@Strings.DatabaseErrorPage_HowToApplyFromPMC</p>
-            <code>@Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC</code>
-            <p>@Strings.DatabaseErrorPage_HowToApplyFromCLI</p>
-            <code>@Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI</code>
-            <hr />
-        </div>
-    }
-    else if (Model.PendingModelChanges)
-    {
-        <div>
-            <h2>@Strings.FormatDatabaseErrorPage_PendingChangesTitle(Model.ContextType.Name)</h2>
-            <p>@Strings.DatabaseErrorPage_PendingChangesInfoPMC</p>
-            <code>@Strings.DatabaseErrorPage_AddMigrationCommandPMC</code>
-            <br />
-            <code>@Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC</code>
-            <p>@Strings.DatabaseErrorPage_PendingChangesInfoCLI</p>
-            <code>@Strings.DatabaseErrorPage_AddMigrationCommandCLI</code>
-            <br />
-            <code>@Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI</code>
-            <hr />
-        </div>
+                </script>
+
+                <p>@Strings.DatabaseErrorPage_HowToApplyFromPMC</p>
+                <code>@Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC</code>
+                <p>@Strings.DatabaseErrorPage_HowToApplyFromCLI</p>
+                <code>@Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI</code>
+                <hr />
+            </div>
+        }
+
+        var contextWithPendingModelChanges = Model.ContextDetails.Where(c => c.PendingModelChanges).Except(contextWithNoDBOrMigrations).Except(contextWithPendingMigrations);
+        if (contextWithPendingModelChanges.Any())
+        {
+            <div>
+                <h2>@Strings.DatabaseErrorPage_PendingChangesTitle</h2>
+                <p>@Strings.DatabaseErrorPage_PendingChangesInfo</p>
+                <ul>
+                    @foreach (var context in contextWithPendingModelChanges)
+                    {
+                        <li>@context.Type.Name</li>
+                    }
+                </ul>
+                <p>@Strings.DatabaseErrorPage_PendingChangesInfoPMC</p>
+                <code>@Strings.DatabaseErrorPage_AddMigrationCommandPMC</code>
+                <br />
+                <code>@Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC</code>
+                <p>@Strings.DatabaseErrorPage_PendingChangesInfoCLI</p>
+                <code>@Strings.DatabaseErrorPage_AddMigrationCommandCLI</code>
+                <br />
+                <code>@Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI</code>
+                <hr />
+            </div>
+        }
     }
 </body>
-</html>
+</html>

+ 9 - 14
src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPageModel.cs

@@ -4,32 +4,27 @@
 using System;
 using System.Collections.Generic;
 using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
 
 namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views
 {
     internal class DatabaseErrorPageModel
     {
         public DatabaseErrorPageModel(
-            Type contextType,
             Exception exception,
-            bool databaseExists,
-            bool pendingModelChanges,
-            IEnumerable<string> pendingMigrations,
-            DatabaseErrorPageOptions options)
+            IEnumerable<DatabaseContextDetails> contextDetails,
+            DatabaseErrorPageOptions options,
+            PathString pathBase)
         {
-            ContextType = contextType;
             Exception = exception;
-            DatabaseExists = databaseExists;
-            PendingModelChanges = pendingModelChanges;
-            PendingMigrations = pendingMigrations;
+            ContextDetails = contextDetails;
             Options = options;
+            PathBase = pathBase;
         }
 
-        public virtual Type ContextType { get; }
         public virtual Exception Exception { get; }
-        public virtual bool DatabaseExists { get; }
-        public virtual bool PendingModelChanges { get; }
-        public virtual IEnumerable<string> PendingMigrations { get; }
+        public virtual IEnumerable<DatabaseContextDetails> ContextDetails { get; }
         public virtual DatabaseErrorPageOptions Options { get; }
+        public virtual PathString PathBase { get; }
     }
-}
+}

+ 23 - 5
src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/DatabaseErrorPageMiddlewareTest.cs

@@ -33,8 +33,10 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
                 {
                     webHostBuilder
                     .UseTestServer()
+#pragma warning disable CS0618 // Type or member is obsolete
                     .Configure(app => app
                     .UseDatabaseErrorPage()
+#pragma warning restore CS0618 // Type or member is obsolete
                     .UseMiddleware<SuccessMiddleware>());
                 }).Build();
 
@@ -68,8 +70,10 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
                 {
                     webHostBuilder
                     .UseTestServer()
+#pragma warning disable CS0618 // Type or member is obsolete
                     .Configure(app => app
                     .UseDatabaseErrorPage()
+#pragma warning restore CS0618 // Type or member is obsolete
                     .UseMiddleware<ExceptionMiddleware>());
                 }).Build();
 
@@ -140,7 +144,9 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
 
                 Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
                 var content = await response.Content.ReadAsStringAsync();
-                Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_NoDbOrMigrationsTitle", typeof(BloggingContext).Name), content);
+                Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_NoDbOrMigrationsTitle"), content);
+                Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_NoDbOrMigrationsInfo"), content);
+                Assert.Contains(typeof(BloggingContext).Name, content);
                 Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_AddMigrationCommandPMC").Replace(">", "&gt;"), content);
                 Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_ApplyMigrationsCommandPMC").Replace(">", "&gt;"), content);
             }
@@ -200,7 +206,9 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
                 Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
 
                 var content = await response.Content.ReadAsStringAsync();
-                Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingMigrationsTitle", typeof(BloggingContextWithMigrations).Name), content);
+                Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingMigrationsTitle"), content);
+                Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingMigrationsInfo"), content);
+                Assert.Contains(typeof(BloggingContextWithMigrations).Name, content);
                 Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_ApplyMigrationsCommandPMC").Replace(">", "&gt;"), content);
                 Assert.Contains("<li>111111111111111_MigrationOne</li>", content);
                 Assert.Contains("<li>222222222222222_MigrationTwo</li>", content);
@@ -237,7 +245,9 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
                 Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
 
                 var content = await response.Content.ReadAsStringAsync();
-                Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingChangesTitle", typeof(BloggingContextWithPendingModelChanges).Name), content);
+                Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingChangesTitle"), content);
+                Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingChangesInfo"), content);
+                Assert.Contains(typeof(BloggingContextWithPendingModelChanges).Name, content);
                 Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_AddMigrationCommandCLI").Replace(">", "&gt;"), content);
                 Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_AddMigrationCommandPMC").Replace(">", "&gt;"), content);
                 Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_ApplyMigrationsCommandCLI").Replace(">", "&gt;"), content);
@@ -283,7 +293,7 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
                 // Ensure the url we're going to test is what the page is using in it's JavaScript
                 var javaScriptEncoder = JavaScriptEncoder.Default;
                 Assert.Contains("req.open(\"POST\", \"" + JavaScriptEncode(expectedMigrationsEndpoint) + "\", true);", content);
-                Assert.Contains("var formBody = \"context=" + JavaScriptEncode(UrlEncode(expectedContextType)) + "\";", content);
+                Assert.Contains("data-assemblyname=\"" + JavaScriptEncode(expectedContextType) + "\"", content);
 
                 // Step Two: Request to migrations endpoint
                 var formData = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
@@ -333,10 +343,12 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
                     .UseTestServer()
                     .Configure(app =>
                     {
+#pragma warning disable CS0618 // Type or member is obsolete
                         app.UseDatabaseErrorPage(new DatabaseErrorPageOptions
                         {
                             MigrationsEndPointPath = new PathString(migrationsEndpoint)
                         });
+#pragma warning restore CS0618 // Type or member is obsolete
 
                         app.UseMiddleware<PendingMigrationsMiddleware>();
                     })
@@ -374,7 +386,9 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
                     .UseTestServer()
                     .Configure(app =>
                     {
+#pragma warning disable CS0618 // Type or member is obsolete
                         app.UseDatabaseErrorPage();
+#pragma warning restore CS0618 // Type or member is obsolete
                         app.UseMiddleware<ContextNotRegisteredInServicesMiddleware>();
 #pragma warning disable CS0618 // Type or member is obsolete
                         app.ApplicationServices.GetService<ILoggerFactory>().AddProvider(logProvider);
@@ -478,7 +492,9 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
                 Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
                 var content = await response.Content.ReadAsStringAsync();
                 Assert.Contains("I wrapped your exception", content);
-                Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_NoDbOrMigrationsTitle", typeof(BloggingContext).Name), content);
+                Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_NoDbOrMigrationsTitle"), content);
+                Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_NoDbOrMigrationsInfo"), content);
+                Assert.Contains(typeof(BloggingContext).Name, content);
             }
         }
 
@@ -513,7 +529,9 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
                     .UseTestServer()
                     .Configure(app =>
                     {
+#pragma warning disable CS0618 // Type or member is obsolete
                         app.UseDatabaseErrorPage();
+#pragma warning restore CS0618 // Type or member is obsolete
 
                         app.UseMiddleware<TMiddleware>();
 

+ 2 - 2
src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/MigrationsEndPointMiddlewareTest.cs

@@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
             var content = await response.Content.ReadAsStringAsync();
 
             Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
-            Assert.StartsWith(StringsHelpers.GetResourceString("FormatMigrationsEndPointMiddleware_InvalidContextType", typeName), content);
+            Assert.StartsWith(StringsHelpers.GetResourceString("FormatMigrationsEndPointMiddleware_ContextNotRegistered", typeName), content);
             Assert.True(content.Length > 512);
         }
 
@@ -228,7 +228,7 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
             var content = await response.Content.ReadAsStringAsync();
 
             Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
-            Assert.StartsWith(StringsHelpers.GetResourceString("FormatMigrationsEndPointMiddleware_ContextNotRegistered", typeof(BloggingContext)), content);
+            Assert.StartsWith(StringsHelpers.GetResourceString("FormatMigrationsEndPointMiddleware_ContextNotRegistered", typeof(BloggingContext).AssemblyQualifiedName), content);
             Assert.True(content.Length > 512);
         }
 

+ 117 - 53
src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseErrorPageTest.cs

@@ -1,18 +1,19 @@
 // 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.IO;
+using System.Text;
+using System.Threading.Tasks;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.Helpers;
 using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views;
 using Microsoft.AspNetCore.Http;
 using Microsoft.EntityFrameworkCore;
 using Moq;
-using System;
-using System.IO;
-using System.Text;
-using System.Threading.Tasks;
 using Xunit;
 
+#nullable enable
 namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
 {
     public class DatabaseErrorPageTest
@@ -23,12 +24,17 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
             var options = new DatabaseErrorPageOptions();
 
             var model = new DatabaseErrorPageModel(
-                contextType: typeof(BloggingContext),
-                exception: new Exception(),
-                databaseExists: false,
-                pendingModelChanges: false,
-                pendingMigrations: new string[] { },
-                options: options);
+                new Exception(),
+                new DatabaseContextDetails[]
+                {
+                    new DatabaseContextDetails(
+                        type: typeof(BloggingContext),
+                        databaseExists: false,
+                        pendingModelChanges: false,
+                        pendingMigrations: new string[] { })
+                },
+                options: options,
+                pathBase: PathString.Empty);
 
             var content = await ExecutePage(options, model);
 
@@ -43,12 +49,17 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
             var options = new DatabaseErrorPageOptions();
 
             var model = new DatabaseErrorPageModel(
-                contextType: typeof(BloggingContext),
-                exception: new Exception(),
-                databaseExists: false,
-                pendingModelChanges: false,
-                pendingMigrations: new[] { "111_MigrationOne" },
-                options: options);
+                new Exception(),
+                new DatabaseContextDetails[]
+                {
+                    new DatabaseContextDetails(
+                        type: typeof(BloggingContext),
+                        databaseExists: false,
+                        pendingModelChanges: false,
+                        pendingMigrations: new string[] { "111_MigrationOne" })
+                },
+                options: options,
+                pathBase: PathString.Empty);
 
             var content = await ExecutePage(options, model);
 
@@ -63,12 +74,17 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
             var options = new DatabaseErrorPageOptions();
 
             var model = new DatabaseErrorPageModel(
-                contextType: typeof(BloggingContext),
-                exception: new Exception(),
-                databaseExists: true,
-                pendingModelChanges: false,
-                pendingMigrations: new[] { "111_MigrationOne" },
-                options: options);
+                new Exception(),
+                new DatabaseContextDetails[]
+                {
+                    new DatabaseContextDetails(
+                        type: typeof(BloggingContext),
+                        databaseExists: true,
+                        pendingModelChanges: false,
+                        pendingMigrations: new string[] { "111_MigrationOne" })
+                },
+                options: options,
+                pathBase: PathString.Empty);
 
             var content = await ExecutePage(options, model);
 
@@ -83,12 +99,17 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
             var options = new DatabaseErrorPageOptions();
 
             var model = new DatabaseErrorPageModel(
-                contextType: typeof(BloggingContext),
-                exception: new Exception(),
-                databaseExists: true,
-                pendingModelChanges: true,
-                pendingMigrations: new[] { "111_MigrationOne" },
-                options: options);
+                new Exception(),
+                new DatabaseContextDetails[]
+                {
+                    new DatabaseContextDetails(
+                        type: typeof(BloggingContext),
+                        databaseExists: true,
+                        pendingModelChanges: true,
+                        pendingMigrations: new string[] { "111_MigrationOne" })
+                },
+                options: options,
+                pathBase: PathString.Empty);
 
             var content = await ExecutePage(options, model);
 
@@ -103,12 +124,17 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
             var options = new DatabaseErrorPageOptions();
 
             var model = new DatabaseErrorPageModel(
-                contextType: typeof(BloggingContext),
-                exception: new Exception(),
-                databaseExists: true,
-                pendingModelChanges: true,
-                pendingMigrations: new string[] { },
-                options: options);
+                new Exception(),
+                new DatabaseContextDetails[]
+                {
+                    new DatabaseContextDetails(
+                        type: typeof(BloggingContext),
+                        databaseExists: true,
+                        pendingModelChanges: true,
+                        pendingMigrations: new string[] { })
+                },
+                options: options,
+                pathBase: PathString.Empty);
 
             var content = await ExecutePage(options, model);
 
@@ -123,12 +149,17 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
             var options = new DatabaseErrorPageOptions();
 
             var model = new DatabaseErrorPageModel(
-                contextType: typeof(BloggingContext),
-                exception: new Exception("Something bad happened"),
-                databaseExists: false,
-                pendingModelChanges: false,
-                pendingMigrations: new string[] { },
-                options: options);
+                new Exception("Something bad happened"),
+                new DatabaseContextDetails[]
+                {
+                    new DatabaseContextDetails(
+                        type: typeof(BloggingContext),
+                        databaseExists: false,
+                        pendingModelChanges: false,
+                        pendingMigrations: new string[] { })
+                },
+                options: options,
+                pathBase: PathString.Empty);
 
             var content = await ExecutePage(options, model);
 
@@ -141,12 +172,17 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
             var options = new DatabaseErrorPageOptions();
 
             var model = new DatabaseErrorPageModel(
-                contextType: typeof(BloggingContext),
-                exception: new Exception("Something bad happened", new Exception("Because something more badder happened")),
-                databaseExists: false,
-                pendingModelChanges: false,
-                pendingMigrations: new string[] { },
-                options: options);
+                new Exception("Something bad happened", new Exception("Because something more badder happened")),
+                new DatabaseContextDetails[]
+                {
+                    new DatabaseContextDetails(
+                        type: typeof(BloggingContext),
+                        databaseExists: false,
+                        pendingModelChanges: false,
+                        pendingMigrations: new string[] { })
+                },
+                options: options,
+                pathBase: PathString.Empty);
 
             var content = await ExecutePage(options, model);
 
@@ -161,18 +197,46 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
             options.MigrationsEndPointPath = "/HitThisEndPoint";
 
             var model = new DatabaseErrorPageModel(
-               contextType: typeof(BloggingContext),
-               exception: new Exception(),
-               databaseExists: true,
-               pendingModelChanges: false,
-               pendingMigrations: new[] { "111_MigrationOne" },
-               options: options);
+                new Exception(),
+                new DatabaseContextDetails[]
+                {
+                    new DatabaseContextDetails(
+                        type: typeof(BloggingContext),
+                        databaseExists: true,
+                        pendingModelChanges: false,
+                        pendingMigrations: new string[] { "111_MigrationOne" })
+                },
+                options: options,
+                pathBase: PathString.Empty);
 
             var content = await ExecutePage(options, model);
 
             Assert.Contains(options.MigrationsEndPointPath.Value, content);
         }
 
+        [Fact]
+        public async Task PathBase_is_respected()
+        {
+            var options = new DatabaseErrorPageOptions();
+            options.MigrationsEndPointPath = "/HitThisEndPoint";
+
+            var model = new DatabaseErrorPageModel(
+                new Exception(),
+                new DatabaseContextDetails[]
+                {
+                    new DatabaseContextDetails(
+                        type: typeof(BloggingContext),
+                        databaseExists: true,
+                        pendingModelChanges: false,
+                        pendingMigrations: new string[] { "111_MigrationOne" })
+                },
+                options: options,
+                pathBase: "/PathBase");
+
+            var content = await ExecutePage(options, model);
+
+            Assert.Contains("/PathBase/HitThisEndPoint", content);
+        }
 
         private static async Task<string> ExecutePage(DatabaseErrorPageOptions options, DatabaseErrorPageModel model)
         {
@@ -196,4 +260,4 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests
 
         }
     }
-}
+}

+ 13 - 7
src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Helpers/AssertHelpers.cs

@@ -10,32 +10,38 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.Helpers
     {
         public static void DisplaysScaffoldFirstMigration(Type contextType, string content)
         {
-            Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_NoDbOrMigrationsTitle", contextType.Name), content);
+            Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_NoDbOrMigrationsTitle"), content);
+            Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_NoDbOrMigrationsInfo"), content);
         }
 
         public static void NotDisplaysScaffoldFirstMigration(Type contextType, string content)
         {
-            Assert.DoesNotContain(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_NoDbOrMigrationsTitle", contextType.Name), content);
+            Assert.DoesNotContain(StringsHelpers.GetResourceString("DatabaseErrorPage_NoDbOrMigrationsTitle"), content);
+            Assert.DoesNotContain(StringsHelpers.GetResourceString("DatabaseErrorPage_NoDbOrMigrationsInfo"), content);
         }
 
         public static void DisplaysApplyMigrations(Type contextType, string content)
         {
-            Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingMigrationsTitle", contextType.Name), content);
+            Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingMigrationsTitle"), content);
+            Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingMigrationsInfo"), content);
         }
 
         public static void NotDisplaysApplyMigrations(Type contextType, string content)
         {
-            Assert.DoesNotContain(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingMigrationsTitle", contextType.Name), content);
+            Assert.DoesNotContain(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingMigrationsTitle"), content);
+            Assert.DoesNotContain(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingMigrationsInfo"), content);
         }
 
         public static void DisplaysScaffoldNextMigraion(Type contextType, string content)
         {
-            Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingChangesTitle", contextType.Name), content);
+            Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingChangesTitle"), content);
+            Assert.Contains(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingChangesInfo"), content);
         }
 
         public static void NotDisplaysScaffoldNextMigraion(Type contextType, string content)
         {
-            Assert.DoesNotContain(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingChangesTitle", contextType.Name), content);
+            Assert.DoesNotContain(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingChangesTitle"), content);
+            Assert.DoesNotContain(StringsHelpers.GetResourceString("DatabaseErrorPage_PendingChangesInfo"), content);
         }
     }
-}
+}

+ 8 - 3
src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Helpers/StringHelpers.cs

@@ -12,8 +12,13 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.Helpers
         public static string GetResourceString(string stringName, params object[] parameters)
         {
             var strings = typeof(DatabaseErrorPageMiddleware).GetTypeInfo().Assembly.GetType("Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Strings").GetTypeInfo();
-            var method = strings.GetDeclaredMethods(stringName).Single();
-            return (string)method.Invoke(null, parameters);
+            var method = strings.GetDeclaredMethods(stringName).SingleOrDefault();
+            if (method != null)
+            {
+                return (string)method.Invoke(null, parameters);
+            }
+            var property = strings.GetDeclaredProperty(stringName);
+            return (string)property.GetValue(null);
         }
     }
-}
+}

+ 2 - 0
src/Middleware/Diagnostics/test/testassets/DatabaseErrorPageSample/Startup.cs

@@ -19,7 +19,9 @@ namespace DatabaseErrorPageSample
         public void Configure(IApplicationBuilder app)
         {
             app.UseDeveloperExceptionPage();
+#pragma warning disable CS0618 // Type or member is obsolete
             app.UseDatabaseErrorPage();
+#pragma warning restore CS0618 // Type or member is obsolete
             app.Run(context =>
             {
                 context.RequestServices.GetService<MyContext>().Blog.FirstOrDefault();

+ 216 - 0
src/Middleware/tools/RazorPageGenerator/Program.cs

@@ -0,0 +1,216 @@
+// 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.IO;
+using System.Linq;
+using System.Text;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.Extensions;
+
+namespace RazorPageGenerator
+{
+    public class Program
+    {
+        public static int Main(string[] args)
+        {
+            if (args == null || args.Length < 1)
+            {
+                Console.WriteLine("Invalid argument(s).");
+                Console.WriteLine(@"Usage:
+    dotnet razorpagegenerator <root-namespace-of-views> [path]
+Examples:
+    dotnet razorpagegenerator Microsoft.AspNetCore.Diagnostics.RazorViews
+        - processes all views in ""Views"" subfolders of the current directory
+    dotnet razorpagegenerator Microsoft.AspNetCore.Diagnostics.RazorViews c:\project
+        - processes all views in ""Views"" subfolders of c:\project directory
+");
+
+                return 1;
+            }
+
+            var rootNamespace = args[0];
+            var targetProjectDirectory = args.Length > 1 ? args[1] : Directory.GetCurrentDirectory();
+            var projectEngine = CreateProjectEngine(rootNamespace, targetProjectDirectory);
+            var results = MainCore(projectEngine, targetProjectDirectory);
+
+            foreach (var result in results)
+            {
+                File.WriteAllText(result.FilePath, result.GeneratedCode);
+            }
+
+            Console.WriteLine();
+            Console.WriteLine($"{results.Count} files successfully generated.");
+            Console.WriteLine();
+            return 0;
+        }
+
+        public static RazorProjectEngine CreateProjectEngine(string rootNamespace, string targetProjectDirectory, Action<RazorProjectEngineBuilder> configure = null)
+        {
+            var fileSystem = RazorProjectFileSystem.Create(targetProjectDirectory);
+            var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder =>
+            {
+                builder
+                    .SetNamespace(rootNamespace)
+                    .SetBaseType("Microsoft.Extensions.RazorViews.BaseView")
+                    .ConfigureClass((document, @class) =>
+                    {
+                        @class.ClassName = Path.GetFileNameWithoutExtension(document.Source.FilePath);
+                        @class.Modifiers.Clear();
+                        @class.Modifiers.Add("internal");
+                    });
+
+                SectionDirective.Register(builder);
+
+                builder.Features.Add(new SuppressChecksumOptionsFeature());
+                builder.Features.Add(new SuppressMetadataAttributesFeature());
+
+                if (configure != null)
+                {
+                    configure(builder);
+                }
+
+                builder.AddDefaultImports(@"
+@using System
+@using System.Threading.Tasks
+");
+            });
+            return projectEngine;
+        }
+
+        public static IList<RazorPageGeneratorResult> MainCore(RazorProjectEngine projectEngine, string targetProjectDirectory)
+        {
+            var viewDirectories = Directory.EnumerateDirectories(targetProjectDirectory, "Views", SearchOption.AllDirectories);
+            var fileCount = 0;
+
+            var results = new List<RazorPageGeneratorResult>();
+            foreach (var viewDir in viewDirectories)
+            {
+                Console.WriteLine();
+                Console.WriteLine("  Generating code files for views in {0}", viewDir);
+                var viewDirPath = viewDir.Substring(targetProjectDirectory.Length).Replace('\\', '/');
+                var cshtmlFiles = projectEngine.FileSystem.EnumerateItems(viewDirPath);
+
+                if (!cshtmlFiles.Any())
+                {
+                    Console.WriteLine("  No .cshtml files were found.");
+                    continue;
+                }
+
+                foreach (var item in cshtmlFiles)
+                {
+                    Console.WriteLine("    Generating code file for view {0}...", item.FileName);
+                    results.Add(GenerateCodeFile(projectEngine, item));
+                    Console.WriteLine("      Done!");
+                    fileCount++;
+                }
+            }
+
+            return results;
+        }
+
+        private static RazorPageGeneratorResult GenerateCodeFile(RazorProjectEngine projectEngine, RazorProjectItem projectItem)
+        {
+            var projectItemWrapper = new FileSystemRazorProjectItemWrapper(projectItem);
+            var codeDocument = projectEngine.Process(projectItemWrapper);
+            var cSharpDocument = codeDocument.GetCSharpDocument();
+            if (cSharpDocument.Diagnostics.Any())
+            {
+                var diagnostics = string.Join(Environment.NewLine, cSharpDocument.Diagnostics);
+                Console.WriteLine($"One or more parse errors encountered. This will not prevent the generator from continuing: {Environment.NewLine}{diagnostics}.");
+            }
+
+            var generatedCodeFilePath = Path.ChangeExtension(projectItem.PhysicalPath, ".Designer.cs");
+            return new RazorPageGeneratorResult
+            {
+                FilePath = generatedCodeFilePath,
+                GeneratedCode = cSharpDocument.GeneratedCode,
+            };
+        }
+
+        private class SuppressChecksumOptionsFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature
+        {
+            public int Order { get; set; }
+
+            public void Configure(RazorCodeGenerationOptionsBuilder options)
+            {
+                if (options == null)
+                {
+                    throw new ArgumentNullException(nameof(options));
+                }
+
+                options.SuppressChecksum = true;
+            }
+        }
+
+        private class SuppressMetadataAttributesFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature
+        {
+            public int Order { get; set; }
+
+            public void Configure(RazorCodeGenerationOptionsBuilder options)
+            {
+                if (options == null)
+                {
+                    throw new ArgumentNullException(nameof(options));
+                }
+
+                options.SuppressMetadataAttributes = true;
+            }
+        }
+
+        private class FileSystemRazorProjectItemWrapper : RazorProjectItem
+        {
+            private readonly RazorProjectItem _source;
+
+            public FileSystemRazorProjectItemWrapper(RazorProjectItem item)
+            {
+                _source = item;
+            }
+
+            public override string BasePath => _source.BasePath;
+
+            public override string FilePath => _source.FilePath;
+
+            // Mask the full name since we don't want a developer's local file paths to be commited.
+            public override string PhysicalPath => _source.FileName;
+
+            public override bool Exists => _source.Exists;
+
+            public override Stream Read()
+            {
+                var processedContent = ProcessFileIncludes();
+                return new MemoryStream(Encoding.UTF8.GetBytes(processedContent));
+            }
+
+            private string ProcessFileIncludes()
+            {
+                var basePath = System.IO.Path.GetDirectoryName(_source.PhysicalPath);
+                var cshtmlContent = File.ReadAllText(_source.PhysicalPath);
+
+                var startMatch = "<%$ include: ";
+                var endMatch = " %>";
+                var startIndex = 0;
+                while (startIndex < cshtmlContent.Length)
+                {
+                    startIndex = cshtmlContent.IndexOf(startMatch, startIndex);
+                    if (startIndex == -1)
+                    {
+                        break;
+                    }
+                    var endIndex = cshtmlContent.IndexOf(endMatch, startIndex);
+                    if (endIndex == -1)
+                    {
+                        throw new InvalidOperationException($"Invalid include file format in {_source.PhysicalPath}. Usage example: <%$ include: ErrorPage.js %>");
+                    }
+                    var includeFileName = cshtmlContent.Substring(startIndex + startMatch.Length, endIndex - (startIndex + startMatch.Length));
+                    Console.WriteLine("      Inlining file {0}", includeFileName);
+                    var includeFileContent = File.ReadAllText(System.IO.Path.Combine(basePath, includeFileName));
+                    cshtmlContent = cshtmlContent.Substring(0, startIndex) + includeFileContent + cshtmlContent.Substring(endIndex + endMatch.Length);
+                    startIndex = startIndex + includeFileContent.Length;
+                }
+                return cshtmlContent;
+            }
+        }
+    }
+}

+ 18 - 0
src/Middleware/tools/RazorPageGenerator/RazorPageGenerator.csproj

@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <Description>Builds Razor pages for views in a project. For internal use only.</Description>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <AssemblyName>dotnet-razorpagegenerator</AssemblyName>
+    <PackageId>RazorPageGenerator</PackageId>
+    <OutputType>Exe</OutputType>
+    <EnableApiCheck>false</EnableApiCheck>
+    <IsShipping>false</IsShipping>
+    <ExcludeFromSourceBuild>true</ExcludeFromSourceBuild>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Razor.Language" />
+  </ItemGroup>
+
+</Project>

+ 12 - 0
src/Middleware/tools/RazorPageGenerator/RazorPageGeneratorResults.cs

@@ -0,0 +1,12 @@
+// 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.
+
+namespace RazorPageGenerator
+{
+    public class RazorPageGeneratorResult
+    {
+        public string FilePath { get; set; }
+
+        public string GeneratedCode { get; set; }
+    }
+}

+ 2 - 2
src/MusicStore/samples/MusicStore/ForTesting/Mocks/StartupOpenIdConnectTesting.cs

@@ -103,6 +103,8 @@ namespace MusicStore
             {
                 options.AddPolicy("ManageStore", new AuthorizationPolicyBuilder().RequireClaim("ManageStore", "Allowed").Build());
             });
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
         public void Configure(IApplicationBuilder app)
@@ -123,8 +125,6 @@ namespace MusicStore
             // During development use the ErrorPage middleware to display error information in the browser
             app.UseDeveloperExceptionPage();
 
-            app.UseDatabaseErrorPage();
-
             // Configure Session.
             app.UseSession();
 

+ 1 - 2
src/MusicStore/samples/MusicStore/ForTesting/Mocks/StartupSocialTesting.cs

@@ -140,6 +140,7 @@ namespace MusicStore
                 options.Scope.Add("wl.signin");
             });
 
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
         public void Configure(IApplicationBuilder app)
@@ -160,8 +161,6 @@ namespace MusicStore
             // Note: Not recommended for production.
             app.UseDeveloperExceptionPage();
 
-            app.UseDatabaseErrorPage();
-
             // Configure Session.
             app.UseSession();
 

+ 2 - 2
src/MusicStore/samples/MusicStore/Startup.cs

@@ -119,6 +119,8 @@ namespace MusicStore
                 options.ClientId = "000000004012C08A";
                 options.ClientSecret = "GaMQ2hCnqAC6EcDLnXsAeBVIJOLmeutL";
             });
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
         //This method is invoked when ASPNETCORE_ENVIRONMENT is 'Development' or is not defined
@@ -132,8 +134,6 @@ namespace MusicStore
             // During development use the ErrorPage middleware to display error information in the browser
             app.UseDeveloperExceptionPage();
 
-            app.UseDatabaseErrorPage();
-
             Configure(app);
         }
 

+ 2 - 1
src/MusicStore/samples/MusicStore/StartupNtlmAuthentication.cs

@@ -94,6 +94,8 @@ namespace MusicStore
                         authBuilder.RequireClaim("ManageStore", "Allowed");
                     });
             });
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
         public void Configure(IApplicationBuilder app)
@@ -114,7 +116,6 @@ namespace MusicStore
             // request pipeline.
             // Note: Not recommended for production.
             app.UseDeveloperExceptionPage();
-            app.UseDatabaseErrorPage();
 
             app.Use((context, next) =>
             {

+ 2 - 2
src/MusicStore/samples/MusicStore/StartupOpenIdConnect.cs

@@ -102,6 +102,8 @@ namespace MusicStore
                 options.ClientId = "[ClientId]";
                 options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
             });
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
         public void Configure(IApplicationBuilder app)
@@ -122,8 +124,6 @@ namespace MusicStore
             // During development use the ErrorPage middleware to display error information in the browser
             app.UseDeveloperExceptionPage();
 
-            app.UseDatabaseErrorPage();
-
             // Configure Session.
             app.UseSession();
 

+ 1 - 3
src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs

@@ -122,6 +122,7 @@ namespace BlazorServerWeb_CSharp
 #endif
 #if (IndividualLocalAuth)
             services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
+            services.AddDatabaseDeveloperPageExceptionFilter();
 #endif
             services.AddSingleton<WeatherForecastService>();
         }
@@ -132,9 +133,6 @@ namespace BlazorServerWeb_CSharp
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-#if (IndividualLocalAuth)
-                app.UseDatabaseErrorPage();
-#endif
             }
             else
             {

+ 2 - 3
src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Startup.cs

@@ -49,6 +49,8 @@ namespace ComponentsWebAssembly_CSharp.Server
 #else
                 options.UseSqlite(
                     Configuration.GetConnectionString("DefaultConnection")));
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
 #endif
 
             services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
@@ -99,9 +101,6 @@ namespace ComponentsWebAssembly_CSharp.Server
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-#if (IndividualLocalAuth)
-                app.UseDatabaseErrorPage();
-#endif
                 app.UseWebAssemblyDebugging();
             }
             else

+ 1 - 3
src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs

@@ -68,6 +68,7 @@ namespace Company.WebApplication1
                 options.UseSqlite(
                     Configuration.GetConnectionString("DefaultConnection")));
 #endif
+            services.AddDatabaseDeveloperPageExceptionFilter();
             services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
                 .AddEntityFrameworkStores<ApplicationDbContext>();
 #elif (OrganizationalAuth)
@@ -120,9 +121,6 @@ namespace Company.WebApplication1
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-#if (IndividualLocalAuth)
-                app.UseDatabaseErrorPage();
-#endif
             }
             else
             {

+ 2 - 3
src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs

@@ -68,6 +68,8 @@ namespace Company.WebApplication1
                 options.UseSqlite(
                     Configuration.GetConnectionString("DefaultConnection")));
 #endif
+            services.AddDatabaseDeveloperPageExceptionFilter();
+
             services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
                 .AddEntityFrameworkStores<ApplicationDbContext>();
 #elif (OrganizationalAuth)
@@ -120,9 +122,6 @@ namespace Company.WebApplication1
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-#if (IndividualLocalAuth)
-                app.UseDatabaseErrorPage();
-#endif
             }
             else
             {

+ 2 - 3
src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Startup.cs

@@ -42,6 +42,8 @@ namespace Company.WebApplication1
 #else
                 options.UseSqlite(
                     Configuration.GetConnectionString("DefaultConnection")));
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
 #endif
 
             services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
@@ -70,9 +72,6 @@ namespace Company.WebApplication1
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-#if (IndividualLocalAuth)
-                app.UseDatabaseErrorPage();
-#endif
             }
             else
             {

+ 2 - 3
src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/Startup.cs

@@ -42,6 +42,8 @@ namespace Company.WebApplication1
 #else
                 options.UseSqlite(
                     Configuration.GetConnectionString("DefaultConnection")));
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
 #endif
 
             services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
@@ -72,9 +74,6 @@ namespace Company.WebApplication1
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-#if (IndividualLocalAuth)
-                app.UseDatabaseErrorPage();
-#endif
             }
             else
             {

+ 2 - 1
src/Security/samples/Identity.ExternalClaims/Startup.cs

@@ -64,6 +64,8 @@ namespace Identity.ExternalClaims
             // Register no-op EmailSender used by account confirmation and password reset during development
             // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713
             services.AddSingleton<IEmailSender, EmailSender>();
+
+            services.AddDatabaseDeveloperPageExceptionFilter();
         }
 
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -72,7 +74,6 @@ namespace Identity.ExternalClaims
             if (env.IsDevelopment())
             {
                 app.UseDeveloperExceptionPage();
-                app.UseDatabaseErrorPage();
             }
             else
             {