| File: DatabaseErrorPageMiddleware.cs | Web Access |
| Project: src\aspnetcore\src\Middleware\Diagnostics.EntityFrameworkCore\src\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj (Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore; /// <summary> /// Captures synchronous and asynchronous database related exceptions from the pipeline that may be resolved using Entity Framework /// migrations. When these exceptions occur an HTML response with details of possible actions to resolve the issue is generated. /// </summary> [RequiresDynamicCode("DbContext migrations operations are not supported with NativeAOT")] public class DatabaseErrorPageMiddleware : IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object?>> { private static readonly AsyncLocal<DiagnosticHolder> _localDiagnostic = new AsyncLocal<DiagnosticHolder>(); private sealed class DiagnosticHolder { public void Hold(Exception exception, Type contextType) { Exception = exception; ContextType = contextType; } public Exception? Exception { get; private set; } public Type? ContextType { get; private set; } } private readonly RequestDelegate _next; private readonly DatabaseErrorPageOptions _options; private readonly ILogger _logger; /// <summary> /// Initializes a new instance of the <see cref="DatabaseErrorPageMiddleware" /> class /// </summary> /// <param name="next">Delegate to execute the next piece of middleware in the request pipeline.</param> /// <param name="loggerFactory"> /// The <see cref="ILoggerFactory" /> for the application. This middleware both produces logging messages and /// 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, IOptions<DatabaseErrorPageOptions> options) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(loggerFactory); ArgumentNullException.ThrowIfNull(options); _next = next; _options = options.Value; _logger = loggerFactory.CreateLogger<DatabaseErrorPageMiddleware>(); // Note: this currently leaks if the server hosting this middleware is disposed. // See aspnet/Home #2825 DiagnosticListener.AllListeners.Subscribe(this); } /// <summary> /// Process an individual request. /// </summary> /// <param name="httpContext">The HTTP context for the current request.</param> /// <returns>A task that represents the asynchronous operation.</returns> public virtual async Task Invoke(HttpContext httpContext) { ArgumentNullException.ThrowIfNull(httpContext); try { // 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. // We create it ahead of time so that any cloning just clones the reference // to the object that will hold any errors. _localDiagnostic.Value = new DiagnosticHolder(); await _next(httpContext); } catch (Exception exception) { try { if (ShouldDisplayErrorPage(exception)) { var contextType = _localDiagnostic.Value!.ContextType; var details = await httpContext.GetContextDetailsAsync(contextType!, _logger); if (details != null && (details.PendingModelChanges || details.PendingMigrations.Any())) { var page = new DatabaseErrorPage { Model = new DatabaseErrorPageModel(exception, new DatabaseContextDetails[] { details }, _options, httpContext.Request.PathBase) }; await page.ExecuteAsync(httpContext); return; } } } catch (Exception e) { _logger.DatabaseErrorPageMiddlewareException(e); } throw; } } private bool ShouldDisplayErrorPage(Exception exception) { _logger.AttemptingToMatchException(exception.GetType()); var lastRecordedException = _localDiagnostic.Value!.Exception; if (lastRecordedException == null) { _logger.NoRecordedException(); return false; } var match = false; for (var e = exception; e != null && !match; e = e.InnerException) { match = lastRecordedException == e; } if (!match) { _logger.NoMatch(); return false; } _logger.Matched(); return true; } void IObserver<DiagnosticListener>.OnNext(DiagnosticListener diagnosticListener) { if (diagnosticListener.Name == DbLoggerCategory.Name) { diagnosticListener.Subscribe(this); } } void IObserver<KeyValuePair<string, object?>>.OnNext(KeyValuePair<string, object?> keyValuePair) { switch (keyValuePair.Value) { // NB: _localDiagnostic.Value can be null when this middleware has been leaked. case DbContextErrorEventData contextErrorEventData: { _localDiagnostic.Value?.Hold(contextErrorEventData.Exception, contextErrorEventData.Context!.GetType()); break; } case DbContextTypeErrorEventData contextTypeErrorEventData: { _localDiagnostic.Value?.Hold(contextTypeErrorEventData.Exception, contextTypeErrorEventData.ContextType); break; } } } void IObserver<DiagnosticListener>.OnCompleted() { } void IObserver<DiagnosticListener>.OnError(Exception error) { } void IObserver<KeyValuePair<string, object?>>.OnCompleted() { } void IObserver<KeyValuePair<string, object?>>.OnError(Exception error) { } }