Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public void ConfigureServices(IServiceCollection services)

services.AddMvc()
.AddNewtonsoftJson();

services.AddDatabaseDeveloperPageExceptionFilter();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand All @@ -48,7 +50,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
Expand Down
3 changes: 2 additions & 1 deletion src/Identity/samples/IdentitySample.DefaultUI/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddDefaultIdentity<ApplicationUser>(o => o.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddDatabaseDeveloperPageExceptionFilter();
}


Expand All @@ -58,7 +60,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
Expand Down
3 changes: 2 additions & 1 deletion src/Identity/samples/IdentitySample.Mvc/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public void ConfigureServices(IServiceCollection services)
// 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.
Expand All @@ -61,7 +63,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public void ConfigureServices(IServiceCollection services)
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.
Expand All @@ -47,7 +49,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,22 @@ public virtual void ConfigureServices(IServiceCollection services)
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.
public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// This prevents running out of file watchers on some linux machines
DisableFilePolling(env);

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public override void ConfigureServices(IServiceCollection services)
{
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.
Expand All @@ -33,7 +34,6 @@ public override void Configure(IApplicationBuilder app, IWebHostEnvironment env)
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -19,6 +20,7 @@ public static class DatabaseErrorPageExtensions
/// </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)
Expand All @@ -36,6 +38,7 @@ public static IApplicationBuilder UseDatabaseErrorPage(this IApplicationBuilder
/// <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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@
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;

Expand Down Expand Up @@ -56,6 +50,7 @@ public void Hold(Exception exception, Type contextType)
/// 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,
Expand Down Expand Up @@ -101,7 +96,7 @@ public virtual async Task Invoke(HttpContext httpContext)
{
// 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.

Expand All @@ -116,81 +111,18 @@ public virtual async Task Invoke(HttpContext httpContext)
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;
}
}
}
Expand Down
Loading