Skip to content

Commit cfe158c

Browse files
author
John Luo
authored
Convert DatabaseErrorPage to exception filter (#24588)
* Convert DatabaseErrorPage middleware to exception filter
1 parent a6abd11 commit cfe158c

File tree

40 files changed

+1099
-405
lines changed

40 files changed

+1099
-405
lines changed

src/Identity/ApiAuthorization.IdentityServer/samples/ApiAuthSample/Startup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public void ConfigureServices(IServiceCollection services)
4040

4141
services.AddMvc()
4242
.AddNewtonsoftJson();
43+
44+
services.AddDatabaseDeveloperPageExceptionFilter();
4345
}
4446

4547
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -48,7 +50,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
4850
if (env.IsDevelopment())
4951
{
5052
app.UseDeveloperExceptionPage();
51-
app.UseDatabaseErrorPage();
5253
}
5354
else
5455
{

src/Identity/samples/IdentitySample.DefaultUI/Startup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public void ConfigureServices(IServiceCollection services)
4949
services.AddDefaultIdentity<ApplicationUser>(o => o.SignIn.RequireConfirmedAccount = true)
5050
.AddRoles<IdentityRole>()
5151
.AddEntityFrameworkStores<ApplicationDbContext>();
52+
53+
services.AddDatabaseDeveloperPageExceptionFilter();
5254
}
5355

5456

@@ -58,7 +60,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF
5860
if (env.IsDevelopment())
5961
{
6062
app.UseDeveloperExceptionPage();
61-
app.UseDatabaseErrorPage();
6263
}
6364
else
6465
{

src/Identity/samples/IdentitySample.Mvc/Startup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public void ConfigureServices(IServiceCollection services)
5353
// Add application services.
5454
services.AddTransient<IEmailSender, AuthMessageSender>();
5555
services.AddTransient<ISmsSender, AuthMessageSender>();
56+
57+
services.AddDatabaseDeveloperPageExceptionFilter();
5658
}
5759

5860
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -61,7 +63,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF
6163
if (env.IsDevelopment())
6264
{
6365
app.UseDeveloperExceptionPage();
64-
app.UseDatabaseErrorPage();
6566
}
6667
else
6768
{

src/Identity/testassets/Identity.DefaultUI.WebSite/NoIdentityStartup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public void ConfigureServices(IServiceCollection services)
3737
options.Conventions.AuthorizePage("/Areas/Identity/Pages/Account/Logout");
3838
})
3939
.AddNewtonsoftJson();
40+
41+
services.AddDatabaseDeveloperPageExceptionFilter();
4042
}
4143

4244
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -47,7 +49,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
4749
if (env.IsDevelopment())
4850
{
4951
app.UseDeveloperExceptionPage();
50-
app.UseDatabaseErrorPage();
5152
}
5253
else
5354
{

src/Identity/testassets/Identity.DefaultUI.WebSite/StartupBase.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,22 @@ public virtual void ConfigureServices(IServiceCollection services)
4949
services.AddDefaultIdentity<TUser>()
5050
.AddRoles<IdentityRole>()
5151
.AddEntityFrameworkStores<TContext>();
52-
52+
5353
services.AddMvc();
5454
services.AddSingleton<IFileVersionProvider, FileVersionProvider>();
55+
56+
services.AddDatabaseDeveloperPageExceptionFilter();
5557
}
5658

5759
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
5860
public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env)
5961
{
6062
// This prevents running out of file watchers on some linux machines
6163
DisableFilePolling(env);
62-
64+
6365
if (env.IsDevelopment())
6466
{
6567
app.UseDeveloperExceptionPage();
66-
app.UseDatabaseErrorPage();
6768
}
6869
else
6970
{

src/Identity/testassets/Identity.DefaultUI.WebSite/StartupWithoutEndpointRouting.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public override void ConfigureServices(IServiceCollection services)
2222
{
2323
base.ConfigureServices(services);
2424
services.AddMvc(options => options.EnableEndpointRouting = false);
25+
services.AddDatabaseDeveloperPageExceptionFilter();
2526
}
2627

2728
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -33,7 +34,6 @@ public override void Configure(IApplicationBuilder app, IWebHostEnvironment env)
3334
if (env.IsDevelopment())
3435
{
3536
app.UseDeveloperExceptionPage();
36-
app.UseDatabaseErrorPage();
3737
}
3838
else
3939
{
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
8+
{
9+
internal class DatabaseContextDetails
10+
{
11+
public Type Type { get; }
12+
public bool DatabaseExists { get; }
13+
public bool PendingModelChanges { get; }
14+
public IEnumerable<string> PendingMigrations { get; }
15+
16+
public DatabaseContextDetails(Type type, bool databaseExists, bool pendingModelChanges, IEnumerable<string> pendingMigrations)
17+
{
18+
Type = type;
19+
DatabaseExists = databaseExists;
20+
PendingModelChanges = pendingModelChanges;
21+
PendingMigrations = pendingMigrations;
22+
}
23+
}
24+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Data.Common;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Builder;
10+
using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views;
11+
using Microsoft.EntityFrameworkCore;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.Extensions.Logging;
14+
using Microsoft.Extensions.Options;
15+
16+
#nullable enable
17+
namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
18+
{
19+
public sealed class DatabaseDeveloperPageExceptionFilter : IDeveloperPageExceptionFilter
20+
{
21+
private readonly ILogger _logger;
22+
private readonly DatabaseErrorPageOptions _options;
23+
24+
public DatabaseDeveloperPageExceptionFilter(ILogger<DatabaseDeveloperPageExceptionFilter> logger, IOptions<DatabaseErrorPageOptions> options)
25+
{
26+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
27+
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
28+
}
29+
30+
public async Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next)
31+
{
32+
if (!(errorContext.Exception is DbException))
33+
{
34+
await next(errorContext);
35+
}
36+
37+
try
38+
{
39+
// Look for DbContext classes registered in the service provider
40+
var registeredContexts = errorContext.HttpContext.RequestServices.GetServices<DbContextOptions>()
41+
.Select(o => o.ContextType);
42+
43+
if (registeredContexts.Any())
44+
{
45+
var contextDetails = new List<DatabaseContextDetails>();
46+
47+
foreach (var registeredContext in registeredContexts)
48+
{
49+
var details = await errorContext.HttpContext.GetContextDetailsAsync(registeredContext, _logger);
50+
51+
if (details != null)
52+
{
53+
contextDetails.Add(details);
54+
}
55+
}
56+
57+
if (contextDetails.Any(c => c.PendingModelChanges || c.PendingMigrations.Any()))
58+
{
59+
var page = new DatabaseErrorPage
60+
{
61+
Model = new DatabaseErrorPageModel(errorContext.Exception, contextDetails, _options, errorContext.HttpContext.Request.PathBase)
62+
};
63+
64+
await page.ExecuteAsync(errorContext.HttpContext);
65+
return;
66+
}
67+
}
68+
}
69+
catch (Exception e)
70+
{
71+
_logger.DatabaseErrorPageMiddlewareException(e);
72+
return;
73+
}
74+
}
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Diagnostics;
6+
using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
8+
9+
#nullable enable
10+
namespace Microsoft.Extensions.DependencyInjection
11+
{
12+
/// <summary>
13+
/// Service extension methods for the <see cref="DatabaseDeveloperPageExceptionFilter"/>.
14+
/// </summary>
15+
public static class DatabaseDeveloperPageExceptionFilterServiceExtensions
16+
{
17+
/// <summary>
18+
/// Add response caching services.
19+
/// </summary>
20+
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
21+
/// <returns></returns>
22+
public static IServiceCollection AddDatabaseDeveloperPageExceptionFilter(this IServiceCollection services)
23+
{
24+
if (services == null)
25+
{
26+
throw new ArgumentNullException(nameof(services));
27+
}
28+
29+
services.TryAddEnumerable(new ServiceDescriptor(typeof(IDeveloperPageExceptionFilter), typeof(DatabaseDeveloperPageExceptionFilter), ServiceLifetime.Singleton));
30+
31+
return services;
32+
}
33+
}
34+
}

src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Builder
1111
/// <summary>
1212
/// <see cref="IApplicationBuilder"/> extension methods for the <see cref="DatabaseErrorPageMiddleware"/>.
1313
/// </summary>
14+
[Obsolete("This is obsolete and will be removed in a future version. Use DatabaseDeveloperPageExceptionFilter instead, see documentation at https://aka.ms/DatabaseDeveloperPageExceptionFilter.")]
1415
public static class DatabaseErrorPageExtensions
1516
{
1617
/// <summary>
@@ -19,6 +20,7 @@ public static class DatabaseErrorPageExtensions
1920
/// </summary>
2021
/// <param name="app">The <see cref="IApplicationBuilder"/> to register the middleware with.</param>
2122
/// <returns>The same <see cref="IApplicationBuilder"/> instance so that multiple calls can be chained.</returns>
23+
[Obsolete("This is obsolete and will be removed in a future version. Use DatabaseDeveloperPageExceptionFilter instead, see documentation at https://aka.ms/DatabaseDeveloperPageExceptionFilter.")]
2224
public static IApplicationBuilder UseDatabaseErrorPage(this IApplicationBuilder app)
2325
{
2426
if (app == null)
@@ -36,6 +38,7 @@ public static IApplicationBuilder UseDatabaseErrorPage(this IApplicationBuilder
3638
/// <param name="app">The <see cref="IApplicationBuilder"/> to register the middleware with.</param>
3739
/// <param name="options">A <see cref="DatabaseErrorPageOptions"/> that specifies options for the middleware.</param>
3840
/// <returns>The same <see cref="IApplicationBuilder"/> instance so that multiple calls can be chained.</returns>
41+
[Obsolete("This is obsolete and will be removed in a future version. Use DatabaseDeveloperPageExceptionFilter instead, see documentation at https://aka.ms/DatabaseDeveloperPageExceptionFilter.")]
3942
public static IApplicationBuilder UseDatabaseErrorPage(
4043
this IApplicationBuilder app, DatabaseErrorPageOptions options)
4144
{

src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageMiddleware.cs

Lines changed: 11 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,6 @@
1212
using Microsoft.AspNetCore.Http;
1313
using Microsoft.EntityFrameworkCore;
1414
using Microsoft.EntityFrameworkCore.Diagnostics;
15-
using Microsoft.EntityFrameworkCore.Infrastructure;
16-
using Microsoft.EntityFrameworkCore.Metadata;
17-
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
18-
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
19-
using Microsoft.EntityFrameworkCore.Migrations;
20-
using Microsoft.EntityFrameworkCore.Storage;
2115
using Microsoft.Extensions.Logging;
2216
using Microsoft.Extensions.Options;
2317

@@ -56,6 +50,7 @@ public void Hold(Exception exception, Type contextType)
5650
/// consumes them to detect database related exception.
5751
/// </param>
5852
/// <param name="options">The options to control what information is displayed on the error page.</param>
53+
[Obsolete("This is obsolete and will be removed in a future version. Use DatabaseDeveloperPageExceptionFilter instead, see documentation at https://aka.ms/DatabaseDeveloperPageExceptionFilter.")]
5954
public DatabaseErrorPageMiddleware(
6055
RequestDelegate next,
6156
ILoggerFactory loggerFactory,
@@ -101,7 +96,7 @@ public virtual async Task Invoke(HttpContext httpContext)
10196
{
10297
// Because CallContext is cloned at each async operation we cannot
10398
// lazily create the error object when an error is encountered, otherwise
104-
// it will not be available to code outside of the current async context.
99+
// it will not be available to code outside of the current async context.
105100
// We create it ahead of time so that any cloning just clones the reference
106101
// to the object that will hold any errors.
107102

@@ -116,81 +111,18 @@ public virtual async Task Invoke(HttpContext httpContext)
116111
if (ShouldDisplayErrorPage(exception))
117112
{
118113
var contextType = _localDiagnostic.Value.ContextType;
119-
var context = (DbContext)httpContext.RequestServices.GetService(contextType);
114+
var details = await httpContext.GetContextDetailsAsync(contextType, _logger);
120115

121-
if (context == null)
116+
if (details != null && (details.PendingModelChanges || details.PendingMigrations.Count() > 0))
122117
{
123-
_logger.ContextNotRegisteredDatabaseErrorPageMiddleware(contextType.FullName);
124-
}
125-
else
126-
{
127-
var relationalDatabaseCreator = context.GetService<IDatabaseCreator>() as IRelationalDatabaseCreator;
128-
if (relationalDatabaseCreator == null)
129-
{
130-
_logger.NotRelationalDatabase();
131-
}
132-
else
118+
var page = new DatabaseErrorPage
133119
{
134-
var databaseExists = await relationalDatabaseCreator.ExistsAsync();
135-
136-
if (databaseExists)
137-
{
138-
databaseExists = await relationalDatabaseCreator.HasTablesAsync();
139-
}
140-
141-
var migrationsAssembly = context.GetService<IMigrationsAssembly>();
142-
var modelDiffer = context.GetService<IMigrationsModelDiffer>();
143-
144-
var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
145-
if (snapshotModel is IConventionModel conventionModel)
146-
{
147-
var conventionSet = context.GetService<IConventionSetBuilder>().CreateConventionSet();
148-
149-
var typeMappingConvention = conventionSet.ModelFinalizingConventions.OfType<TypeMappingConvention>().FirstOrDefault();
150-
if (typeMappingConvention != null)
151-
{
152-
typeMappingConvention.ProcessModelFinalizing(conventionModel.Builder, null);
153-
}
154-
155-
var relationalModelConvention = conventionSet.ModelFinalizedConventions.OfType<RelationalModelConvention>().FirstOrDefault();
156-
if (relationalModelConvention != null)
157-
{
158-
snapshotModel = relationalModelConvention.ProcessModelFinalized(conventionModel);
159-
}
160-
}
161-
162-
if (snapshotModel is IMutableModel mutableModel)
163-
{
164-
snapshotModel = mutableModel.FinalizeModel();
165-
}
166-
167-
// HasDifferences will return true if there is no model snapshot, but if there is an existing database
168-
// and no model snapshot then we don't want to show the error page since they are most likely targeting
169-
// and existing database and have just misconfigured their model
170-
171-
var pendingModelChanges
172-
= (!databaseExists || migrationsAssembly.ModelSnapshot != null)
173-
&& modelDiffer.HasDifferences(snapshotModel?.GetRelationalModel(), context.Model.GetRelationalModel());
174-
175-
var pendingMigrations
176-
= (databaseExists
177-
? await context.Database.GetPendingMigrationsAsync()
178-
: context.Database.GetMigrations())
179-
.ToArray();
180-
181-
if (pendingModelChanges || pendingMigrations.Length > 0)
182-
{
183-
var page = new DatabaseErrorPage
184-
{
185-
Model = new DatabaseErrorPageModel(
186-
contextType, exception, databaseExists, pendingModelChanges, pendingMigrations, _options)
187-
};
188-
189-
await page.ExecuteAsync(httpContext);
190-
191-
return;
192-
}
193-
}
120+
Model = new DatabaseErrorPageModel(exception, new DatabaseContextDetails[] { details }, _options, httpContext.Request.PathBase)
121+
};
122+
123+
await page.ExecuteAsync(httpContext);
124+
125+
return;
194126
}
195127
}
196128
}

0 commit comments

Comments
 (0)