From cfe158cbed7483e8ab931e4cf47d578ba6c7170d Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 17 Aug 2020 11:13:17 -0700 Subject: [PATCH] Convert DatabaseErrorPage to exception filter (#24588) * Convert DatabaseErrorPage middleware to exception filter --- .../samples/ApiAuthSample/Startup.cs | 3 +- .../IdentitySample.DefaultUI/Startup.cs | 3 +- .../samples/IdentitySample.Mvc/Startup.cs | 3 +- .../NoIdentityStartup.cs | 3 +- .../Identity.DefaultUI.WebSite/StartupBase.cs | 7 +- .../StartupWithoutEndpointRouting.cs | 2 +- .../src/DatabaseContextDetails.cs | 24 + .../DatabaseDeveloperPageExceptionFilter.cs | 76 +++ ...perPageExceptionFilterServiceExtensions.cs | 34 ++ .../src/DatabaseErrorPageExtensions.cs | 3 + .../src/DatabaseErrorPageMiddleware.cs | 86 +--- ...ticsEntityFrameworkCoreLoggerExtensions.cs | 12 +- ...ContextDatabaseContextDetailsExtensions.cs | 87 ++++ ...ore.Diagnostics.EntityFrameworkCore.csproj | 1 + .../src/MigrationsEndPointMiddleware.cs | 48 +- .../src/Strings.resx | 21 +- .../src/Views/DatabaseErrorPage.Designer.cs | 484 ++++++++++++------ .../src/Views/DatabaseErrorPage.cshtml | 140 +++-- .../src/Views/DatabaseErrorPageModel.cs | 23 +- .../DatabaseErrorPageMiddlewareTest.cs | 28 +- .../MigrationsEndPointMiddlewareTest.cs | 4 +- .../test/UnitTests/DatabaseErrorPageTest.cs | 170 ++++-- .../test/UnitTests/Helpers/AssertHelpers.cs | 20 +- .../test/UnitTests/Helpers/StringHelpers.cs | 11 +- .../DatabaseErrorPageSample/Startup.cs | 2 + .../tools/RazorPageGenerator/Program.cs | 216 ++++++++ .../RazorPageGenerator.csproj | 18 + .../RazorPageGeneratorResults.cs | 12 + .../Mocks/StartupOpenIdConnectTesting.cs | 4 +- .../ForTesting/Mocks/StartupSocialTesting.cs | 3 +- src/MusicStore/samples/MusicStore/Startup.cs | 4 +- .../MusicStore/StartupNtlmAuthentication.cs | 3 +- .../MusicStore/StartupOpenIdConnect.cs | 4 +- .../content/BlazorServerWeb-CSharp/Startup.cs | 4 +- .../Server/Startup.cs | 5 +- .../content/RazorPagesWeb-CSharp/Startup.cs | 4 +- .../content/StarterWeb-CSharp/Startup.cs | 5 +- .../content/Angular-CSharp/Startup.cs | 5 +- .../content/React-CSharp/Startup.cs | 5 +- .../Identity.ExternalClaims/Startup.cs | 3 +- 40 files changed, 1142 insertions(+), 448 deletions(-) create mode 100644 src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseContextDetails.cs create mode 100644 src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilter.cs create mode 100644 src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilterServiceExtensions.cs create mode 100644 src/Middleware/Diagnostics.EntityFrameworkCore/src/HttpContextDatabaseContextDetailsExtensions.cs create mode 100644 src/Middleware/tools/RazorPageGenerator/Program.cs create mode 100644 src/Middleware/tools/RazorPageGenerator/RazorPageGenerator.csproj create mode 100644 src/Middleware/tools/RazorPageGenerator/RazorPageGeneratorResults.cs diff --git a/src/Identity/ApiAuthorization.IdentityServer/samples/ApiAuthSample/Startup.cs b/src/Identity/ApiAuthorization.IdentityServer/samples/ApiAuthSample/Startup.cs index de50786ef0..e734bb51aa 100644 --- a/src/Identity/ApiAuthorization.IdentityServer/samples/ApiAuthSample/Startup.cs +++ b/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 { diff --git a/src/Identity/samples/IdentitySample.DefaultUI/Startup.cs b/src/Identity/samples/IdentitySample.DefaultUI/Startup.cs index e80a9eb022..69a36323bc 100644 --- a/src/Identity/samples/IdentitySample.DefaultUI/Startup.cs +++ b/src/Identity/samples/IdentitySample.DefaultUI/Startup.cs @@ -49,6 +49,8 @@ namespace IdentitySample.DefaultUI services.AddDefaultIdentity(o => o.SignIn.RequireConfirmedAccount = true) .AddRoles() .AddEntityFrameworkStores(); + + services.AddDatabaseDeveloperPageExceptionFilter(); } @@ -58,7 +60,6 @@ namespace IdentitySample.DefaultUI if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); - app.UseDatabaseErrorPage(); } else { diff --git a/src/Identity/samples/IdentitySample.Mvc/Startup.cs b/src/Identity/samples/IdentitySample.Mvc/Startup.cs index bd822c7ed8..7415a7059c 100644 --- a/src/Identity/samples/IdentitySample.Mvc/Startup.cs +++ b/src/Identity/samples/IdentitySample.Mvc/Startup.cs @@ -53,6 +53,8 @@ namespace IdentitySample // Add application services. services.AddTransient(); services.AddTransient(); + + 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 { diff --git a/src/Identity/testassets/Identity.DefaultUI.WebSite/NoIdentityStartup.cs b/src/Identity/testassets/Identity.DefaultUI.WebSite/NoIdentityStartup.cs index 7c28385466..2f7165144d 100644 --- a/src/Identity/testassets/Identity.DefaultUI.WebSite/NoIdentityStartup.cs +++ b/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 { diff --git a/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs b/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs index 77706eeb79..91bb3bab86 100644 --- a/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs +++ b/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs @@ -49,9 +49,11 @@ namespace Identity.DefaultUI.WebSite services.AddDefaultIdentity() .AddRoles() .AddEntityFrameworkStores(); - + services.AddMvc(); services.AddSingleton(); + + 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 { diff --git a/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupWithoutEndpointRouting.cs b/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupWithoutEndpointRouting.cs index cc7f7367de..2056a56278 100644 --- a/src/Identity/testassets/Identity.DefaultUI.WebSite/StartupWithoutEndpointRouting.cs +++ b/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 { diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseContextDetails.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseContextDetails.cs new file mode 100644 index 0000000000..82c242cada --- /dev/null +++ b/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 PendingMigrations { get; } + + public DatabaseContextDetails(Type type, bool databaseExists, bool pendingModelChanges, IEnumerable pendingMigrations) + { + Type = type; + DatabaseExists = databaseExists; + PendingModelChanges = pendingModelChanges; + PendingMigrations = pendingMigrations; + } + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilter.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilter.cs new file mode 100644 index 0000000000..3ba74bc18f --- /dev/null +++ b/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 logger, IOptions options) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + } + + public async Task HandleExceptionAsync(ErrorContext errorContext, Func 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() + .Select(o => o.ContextType); + + if (registeredContexts.Any()) + { + var contextDetails = new List(); + + 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; + } + } + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilterServiceExtensions.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseDeveloperPageExceptionFilterServiceExtensions.cs new file mode 100644 index 0000000000..51041852b5 --- /dev/null +++ b/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 +{ + /// + /// Service extension methods for the . + /// + public static class DatabaseDeveloperPageExceptionFilterServiceExtensions + { + /// + /// Add response caching services. + /// + /// The for adding services. + /// + 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; + } + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageExtensions.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageExtensions.cs index 5b8170735c..5cfddc580a 100644 --- a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageExtensions.cs +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageExtensions.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Builder /// /// extension methods for the . /// + [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 { /// @@ -19,6 +20,7 @@ namespace Microsoft.AspNetCore.Builder /// /// The to register the middleware with. /// The same instance so that multiple calls can be chained. + [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 /// The to register the middleware with. /// A that specifies options for the middleware. /// The same instance so that multiple calls can be chained. + [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) { diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageMiddleware.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageMiddleware.cs index 8fea0d3ac1..1c196b9114 100644 --- a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageMiddleware.cs +++ b/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. /// /// The options to control what information is displayed on the error page. + [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() as IRelationalDatabaseCreator; - if (relationalDatabaseCreator == null) + var page = new DatabaseErrorPage { - _logger.NotRelationalDatabase(); - } - else - { - var databaseExists = await relationalDatabaseCreator.ExistsAsync(); + Model = new DatabaseErrorPageModel(exception, new DatabaseContextDetails[] { details }, _options, httpContext.Request.PathBase) + }; - if (databaseExists) - { - databaseExists = await relationalDatabaseCreator.HasTablesAsync(); - } + await page.ExecuteAsync(httpContext); - var migrationsAssembly = context.GetService(); - var modelDiffer = context.GetService(); - - var snapshotModel = migrationsAssembly.ModelSnapshot?.Model; - if (snapshotModel is IConventionModel conventionModel) - { - var conventionSet = context.GetService().CreateConventionSet(); - - var typeMappingConvention = conventionSet.ModelFinalizingConventions.OfType().FirstOrDefault(); - if (typeMappingConvention != null) - { - typeMappingConvention.ProcessModelFinalizing(conventionModel.Builder, null); - } - - var relationalModelConvention = conventionSet.ModelFinalizedConventions.OfType().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; - } - } + return; } } } diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DiagnosticsEntityFrameworkCoreLoggerExtensions.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DiagnosticsEntityFrameworkCoreLoggerExtensions.cs index 8128ac1baf..494213a900 100644 --- a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DiagnosticsEntityFrameworkCoreLoggerExtensions.cs +++ b/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 _invalidContextType = LoggerMessage.Define( - 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 _contextNotRegistered = LoggerMessage.Define( 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); diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/HttpContextDatabaseContextDetailsExtensions.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/HttpContextDatabaseContextDetailsExtensions.cs new file mode 100644 index 0000000000..c24e8cb641 --- /dev/null +++ b/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 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() as IRelationalDatabaseCreator; + if (relationalDatabaseCreator == null) + { + logger.NotRelationalDatabase(); + return null; + } + + var databaseExists = await relationalDatabaseCreator.ExistsAsync(); + + if (databaseExists) + { + databaseExists = await relationalDatabaseCreator.HasTablesAsync(); + } + + var migrationsAssembly = context.GetService(); + var modelDiffer = context.GetService(); + + var snapshotModel = migrationsAssembly.ModelSnapshot?.Model; + if (snapshotModel is IConventionModel conventionModel) + { + var conventionSet = context.GetService().CreateConventionSet(); + + var typeMappingConvention = conventionSet.ModelFinalizingConventions.OfType().FirstOrDefault(); + if (typeMappingConvention != null) + { + typeMappingConvention.ProcessModelFinalizing(conventionModel.Builder, null); + } + + var relationalModelConvention = conventionSet.ModelFinalizedConventions.OfType().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()); + } + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj index 8d46318059..36e5b56079 100644 --- a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointMiddleware.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointMiddleware.cs index b6c8b45a9a..251b326308 100644 --- a/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointMiddleware.cs +++ b/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,32 +117,25 @@ namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore return null; } + // Look for DbContext classes registered in the service provider + var registeredContexts = context.RequestServices.GetServices() + .Select(o => o.ContextType); + + if (!registeredContexts.Any(c => string.Equals(contextTypeName, c.AssemblyQualifiedName))) + { + var message = Strings.FormatMigrationsEndPointMiddleware_ContextNotRegistered(contextTypeName); + + logger.ContextNotRegistered(contextTypeName); + + await WriteErrorToResponse(context.Response, message); + + return null; + } + var contextType = Type.GetType(contextTypeName); - if (contextType == null) - { - var message = Strings.FormatMigrationsEndPointMiddleware_InvalidContextType(contextTypeName); - - logger.InvalidContextType(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); - - return null; - } - return db; } diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Strings.resx b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Strings.resx index 0c6bad1477..d6a16c14f3 100644 --- a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Strings.resx +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Strings.resx @@ -1,4 +1,4 @@ - +