Skip to content

Commit cbe1da3

Browse files
author
John Luo
committed
Convert DatabaseErrorPage middleware to exception filter
1 parent b2c7d51 commit cbe1da3

23 files changed

+1038
-349
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.Builder;
6+
using Microsoft.AspNetCore.Hosting;
7+
8+
namespace Microsoft.AspNetCore
9+
{
10+
internal class MigrationsEndPointStartupFilter : IStartupFilter
11+
{
12+
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
13+
{
14+
return app =>
15+
{
16+
//app.UseMigrationsEndPoint();
17+
next(app);
18+
};
19+
}
20+
}
21+
}

src/DefaultBuilder/src/WebHost.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.IO;
66
using System.Reflection;
77
using Microsoft.AspNetCore.Builder;
8+
//using Microsoft.AspNetCore.Diagnostics;
9+
//using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore;
810
using Microsoft.AspNetCore.HostFiltering;
911
using Microsoft.AspNetCore.Hosting;
1012
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
@@ -259,6 +261,12 @@ internal static void ConfigureWebDefaults(IWebHostBuilder builder)
259261
}
260262

261263
services.AddRouting();
264+
265+
//if (hostingContext.HostingEnvironment.IsDevelopment())
266+
//{
267+
// services.AddSingleton<IDeveloperPageExceptionFilter, DatabaseErrorHandler>();
268+
// services.AddTransient<IStartupFilter, MigrationsEndPointStartupFilter>();
269+
//}
262270
})
263271
.UseIIS()
264272
.UseIISIntegration();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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; set; }
12+
public bool DatabaseExists { get; set; }
13+
public bool PendingModelChanges { get; set; }
14+
public IEnumerable<string> PendingMigrations { get; set; }
15+
}
16+
}

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 DatabaseExceptionHandler instead.")]
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 DatabaseExceptionHandler instead.")]
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 DatabaseExceptionHandler instead.")]
3942
public static IApplicationBuilder UseDatabaseErrorPage(
4043
this IApplicationBuilder app, DatabaseErrorPageOptions options)
4144
{

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

Lines changed: 10 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public void Hold(Exception exception, Type contextType)
5656
/// consumes them to detect database related exception.
5757
/// </param>
5858
/// <param name="options">The options to control what information is displayed on the error page.</param>
59+
[Obsolete("This is obsolete and will be removed in a future version. Use DatabaseExceptionHandler instead.")]
5960
public DatabaseErrorPageMiddleware(
6061
RequestDelegate next,
6162
ILoggerFactory loggerFactory,
@@ -116,81 +117,18 @@ public virtual async Task Invoke(HttpContext httpContext)
116117
if (ShouldDisplayErrorPage(exception))
117118
{
118119
var contextType = _localDiagnostic.Value.ContextType;
119-
var context = (DbContext)httpContext.RequestServices.GetService(contextType);
120+
var details = await httpContext.GetContextDetailsAsync(contextType, _logger);
120121

121-
if (context == null)
122+
if (details != null && (details.PendingModelChanges || details.PendingMigrations.Count() > 0))
122123
{
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
124+
var page = new DatabaseErrorPage
133125
{
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-
}
126+
Model = new DatabaseErrorPageModel(exception, new DatabaseContextDetails[] { details }, _options)
127+
};
128+
129+
await page.ExecuteAsync(httpContext);
130+
131+
return;
194132
}
195133
}
196134
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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.EntityFrameworkCore.Infrastructure;
13+
using Microsoft.EntityFrameworkCore.Metadata;
14+
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
15+
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
16+
using Microsoft.EntityFrameworkCore.Migrations;
17+
using Microsoft.EntityFrameworkCore.Storage;
18+
using Microsoft.Extensions.DependencyInjection;
19+
using Microsoft.Extensions.Logging;
20+
using Microsoft.Extensions.Options;
21+
22+
namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
23+
{
24+
public class DatabaseErrorHandler : IDeveloperPageExceptionFilter
25+
{
26+
private readonly ILogger _logger;
27+
private readonly DatabaseErrorPageOptions _options;
28+
29+
public DatabaseErrorHandler(ILogger<DatabaseErrorHandler> logger, IOptions<DatabaseErrorPageOptions> options)
30+
{
31+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
32+
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
33+
}
34+
35+
public async Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next)
36+
{
37+
if (errorContext.Exception is DbException)
38+
{
39+
try
40+
{
41+
// Look for DbContext classes registered in the service provider
42+
// TODO: Decouple
43+
var registeredContexts = errorContext.HttpContext.RequestServices.GetServices<DbContextOptions>()
44+
.Select(o => o.ContextType);
45+
46+
if (registeredContexts.Any())
47+
{
48+
var contextDetails = new List<DatabaseContextDetails>();
49+
50+
foreach (var registeredContext in registeredContexts)
51+
{
52+
var details = await errorContext.HttpContext.GetContextDetailsAsync(registeredContext, _logger);
53+
54+
if (details != null)
55+
{
56+
contextDetails.Add(details);
57+
}
58+
}
59+
60+
if (contextDetails.Any(c => c.PendingModelChanges || c.PendingMigrations.Any()))
61+
{
62+
var page = new DatabaseErrorPage
63+
{
64+
Model = new DatabaseErrorPageModel(errorContext.Exception, contextDetails, _options)
65+
};
66+
67+
await page.ExecuteAsync(errorContext.HttpContext);
68+
return;
69+
}
70+
}
71+
}
72+
catch (Exception e)
73+
{
74+
_logger.DatabaseErrorPageMiddlewareException(e);
75+
}
76+
77+
await next(errorContext);
78+
}
79+
else
80+
{
81+
await next(errorContext);
82+
}
83+
}
84+
}
85+
}

src/Middleware/Diagnostics.EntityFrameworkCore/src/DiagnosticsEntityFrameworkCoreLoggerExtensions.cs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -14,11 +14,6 @@ internal static class DiagnosticsEntityFrameworkCoreLoggerExtensions
1414
new EventId(1, "NoContextType"),
1515
"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.");
1616

17-
private static readonly Action<ILogger, string, Exception> _invalidContextType = LoggerMessage.Define<string>(
18-
LogLevel.Error,
19-
new EventId(2, "InvalidContextType"),
20-
"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.");
21-
2217
private static readonly Action<ILogger, string, Exception> _contextNotRegistered = LoggerMessage.Define<string>(
2318
LogLevel.Error,
2419
new EventId(3, "ContextNotRegistered"),
@@ -85,11 +80,6 @@ public static void NoContextType(this ILogger logger)
8580
_noContextType(logger, null);
8681
}
8782

88-
public static void InvalidContextType(this ILogger logger, string contextTypeName)
89-
{
90-
_invalidContextType(logger, contextTypeName, null);
91-
}
92-
9383
public static void ContextNotRegistered(this ILogger logger, string contextTypeName)
9484
{
9585
_contextNotRegistered(logger, contextTypeName, null);
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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.Linq;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.EntityFrameworkCore;
9+
using Microsoft.EntityFrameworkCore.Infrastructure;
10+
using Microsoft.EntityFrameworkCore.Metadata;
11+
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
12+
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
13+
using Microsoft.EntityFrameworkCore.Migrations;
14+
using Microsoft.EntityFrameworkCore.Storage;
15+
using Microsoft.Extensions.DependencyInjection;
16+
using Microsoft.Extensions.Logging;
17+
18+
19+
namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
20+
{
21+
internal static class HttpContextDatabaseContextDetailsExtensions
22+
{
23+
public static async Task<DatabaseContextDetails> GetContextDetailsAsync(this HttpContext httpContext, Type dbcontextType, ILogger logger)
24+
{
25+
// TODO: Decouple
26+
var context = (DbContext)httpContext.RequestServices.GetService(dbcontextType);
27+
// TODO: Decouple
28+
var relationalDatabaseCreator = context.GetService<IDatabaseCreator>() as IRelationalDatabaseCreator;
29+
if (relationalDatabaseCreator == null)
30+
{
31+
logger.NotRelationalDatabase();
32+
}
33+
else
34+
{
35+
var databaseExists = await relationalDatabaseCreator.ExistsAsync();
36+
37+
if (databaseExists)
38+
{
39+
databaseExists = await relationalDatabaseCreator.HasTablesAsync();
40+
}
41+
42+
// TODO: Decouple
43+
var migrationsAssembly = context.GetService<IMigrationsAssembly>();
44+
// TODO: Decouple
45+
var modelDiffer = context.GetService<IMigrationsModelDiffer>();
46+
47+
var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
48+
// TODO: Decouple
49+
if (snapshotModel is IConventionModel conventionModel)
50+
{
51+
// TODO: Decouple
52+
var conventionSet = context.GetService<IConventionSetBuilder>().CreateConventionSet();
53+
54+
// TODO: Decouple
55+
var typeMappingConvention = conventionSet.ModelFinalizingConventions.OfType<TypeMappingConvention>().FirstOrDefault();
56+
if (typeMappingConvention != null)
57+
{
58+
typeMappingConvention.ProcessModelFinalizing(conventionModel.Builder, null);
59+
}
60+
61+
// TODO: Decouple
62+
var relationalModelConvention = conventionSet.ModelFinalizedConventions.OfType<RelationalModelConvention>().FirstOrDefault();
63+
if (relationalModelConvention != null)
64+
{
65+
snapshotModel = relationalModelConvention.ProcessModelFinalized(conventionModel);
66+
}
67+
}
68+
69+
// TODO: Decouple
70+
if (snapshotModel is IMutableModel mutableModel)
71+
{
72+
snapshotModel = mutableModel.FinalizeModel();
73+
}
74+
75+
// HasDifferences will return true if there is no model snapshot, but if there is an existing database
76+
// and no model snapshot then we don't want to show the error page since they are most likely targeting
77+
// and existing database and have just misconfigured their model
78+
79+
return new DatabaseContextDetails
80+
{
81+
Type = dbcontextType,
82+
DatabaseExists = databaseExists,
83+
PendingModelChanges = (!databaseExists || migrationsAssembly.ModelSnapshot != null)
84+
&& modelDiffer.HasDifferences(snapshotModel?.GetRelationalModel(), context.Model.GetRelationalModel()),
85+
PendingMigrations = databaseExists
86+
? await context.Database.GetPendingMigrationsAsync()
87+
: context.Database.GetMigrations()
88+
};
89+
}
90+
91+
return null;
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)