diff --git a/.gitmodules b/.gitmodules index 8476ec5888..0af0534995 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,10 +14,6 @@ path = modules/CORS url = https://github.com/aspnet/CORS.git branch = release/2.2 -[submodule "modules/Diagnostics"] - path = modules/Diagnostics - url = https://github.com/aspnet/Diagnostics.git - branch = release/2.2 [submodule "modules/EntityFrameworkCore"] path = modules/EntityFrameworkCore url = https://github.com/aspnet/EntityFrameworkCore.git diff --git a/build/CodeSign.props b/build/CodeSign.props index 9355942767..b26ed9e067 100644 --- a/build/CodeSign.props +++ b/build/CodeSign.props @@ -39,6 +39,8 @@ + + diff --git a/build/artifacts.props b/build/artifacts.props index 7fd49d0e97..29d082173e 100644 --- a/build/artifacts.props +++ b/build/artifacts.props @@ -65,7 +65,6 @@ - @@ -184,9 +183,7 @@ - - diff --git a/build/buildorder.props b/build/buildorder.props index e46fa3e2d0..57d6541aa8 100644 --- a/build/buildorder.props +++ b/build/buildorder.props @@ -19,7 +19,6 @@ - diff --git a/build/dependencies.props b/build/dependencies.props index 65183fdf94..a30da30215 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -33,6 +33,7 @@ 2.2.0 2.2.0 2.2.0 + 2.2.0 2.2.0 2.2.0 2.2.0 @@ -57,6 +58,8 @@ 2.2.0 2.2.0 2.2.0 + 2.2.0 + 2.2.0 2.2.0 2.2.0 2.2.0 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index a77757b97f..8a7f7efbde 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -40,6 +40,8 @@ + + diff --git a/build/submodules.props b/build/submodules.props index da1a794e15..ecec882447 100644 --- a/build/submodules.props +++ b/build/submodules.props @@ -52,7 +52,6 @@ - diff --git a/eng/Baseline.props b/eng/Baseline.props index 1d647402db..262c43c3ba 100644 --- a/eng/Baseline.props +++ b/eng/Baseline.props @@ -114,6 +114,44 @@ + + + 2.2.0 + + + + + 2.2.0 + + + + + + + + 2.2.0 + + + + + + + + + + 2.2.0 + + + + + + + + + + + + 2.2.0 @@ -212,6 +250,15 @@ + + + 2.2.0 + + + + + + 2.2.0 @@ -333,6 +380,15 @@ + + + 2.2.0 + + + + + + 2.2.0 diff --git a/eng/Dependencies.props b/eng/Dependencies.props index d32aec7bd4..286c298e36 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -14,6 +14,7 @@ + diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 164c258a97..aec24922ce 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -42,6 +42,10 @@ + + + + diff --git a/eng/dependencies.temp.props b/eng/dependencies.temp.props index 3ebcecb640..b932bd9841 100644 --- a/eng/dependencies.temp.props +++ b/eng/dependencies.temp.props @@ -1,12 +1,10 @@ - - diff --git a/eng/tools/BaselineGenerator/baseline.xml b/eng/tools/BaselineGenerator/baseline.xml index c37bf56d4f..e15e9099cb 100644 --- a/eng/tools/BaselineGenerator/baseline.xml +++ b/eng/tools/BaselineGenerator/baseline.xml @@ -16,6 +16,10 @@ + + + + @@ -26,6 +30,7 @@ + @@ -36,5 +41,6 @@ + diff --git a/modules/Diagnostics b/modules/Diagnostics deleted file mode 160000 index c802d5ef5f..0000000000 --- a/modules/Diagnostics +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c802d5ef5fba1ba8dfbcb8c3741af2ba15e9d1aa diff --git a/src/Middleware/Diagnostics.Abstractions/src/CompilationFailure.cs b/src/Middleware/Diagnostics.Abstractions/src/CompilationFailure.cs new file mode 100644 index 0000000000..4a100e3dc0 --- /dev/null +++ b/src/Middleware/Diagnostics.Abstractions/src/CompilationFailure.cs @@ -0,0 +1,83 @@ +// 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.Collections.Generic; + +namespace Microsoft.AspNetCore.Diagnostics +{ + /// + /// Describes a failure compiling a specific file. + /// + public class CompilationFailure + { + /// + /// Initializes a new instance of . + /// + /// Path for the file that produced the compilation failure. + /// Contents of the file being compiled. + /// For templated languages (such as Asp.Net Core Razor), the generated content. + /// + /// One or or more instances. + public CompilationFailure( + string sourceFilePath, + string sourceFileContent, + string compiledContent, + IEnumerable messages) + : this(sourceFilePath, sourceFileContent, compiledContent, messages, failureSummary: null) + { + } + + /// + /// Initializes a new instance of . + /// + /// Path for the file that produced the compilation failure. + /// Contents of the file being compiled. + /// For templated languages (such as Asp.Net Core Razor), the generated content. + /// + /// One or or more instances. + /// Summary message or instructions to fix the failure. + public CompilationFailure( + string sourceFilePath, + string sourceFileContent, + string compiledContent, + IEnumerable messages, + string failureSummary) + { + SourceFilePath = sourceFilePath; + SourceFileContent = sourceFileContent; + CompiledContent = compiledContent; + Messages = messages; + FailureSummary = failureSummary; + } + + /// + /// Path of the file that produced the compilation failure. + /// + public string SourceFilePath { get; } + + /// + /// Contents of the file. + /// + public string SourceFileContent { get; } + + /// + /// Contents being compiled. + /// + /// + /// For templated files, the represents the original content and + /// represents the transformed content. This property can be null if + /// the exception is encountered during transformation. + /// + public string CompiledContent { get; } + + /// + /// Gets a sequence of produced as a result of compilation. + /// + public IEnumerable Messages { get; } + + /// + /// Summary message or instructions to fix the failure. + /// + public string FailureSummary { get; } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.Abstractions/src/DiagnosticMessage.cs b/src/Middleware/Diagnostics.Abstractions/src/DiagnosticMessage.cs new file mode 100644 index 0000000000..dc9f65f507 --- /dev/null +++ b/src/Middleware/Diagnostics.Abstractions/src/DiagnosticMessage.cs @@ -0,0 +1,64 @@ +// 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. + +namespace Microsoft.AspNetCore.Diagnostics +{ + /// + /// A single diagnostic message. + /// + public class DiagnosticMessage + { + public DiagnosticMessage( + string message, + string formattedMessage, + string filePath, + int startLine, + int startColumn, + int endLine, + int endColumn) + { + Message = message; + SourceFilePath = filePath; + StartLine = startLine; + EndLine = endLine; + StartColumn = startColumn; + EndColumn = endColumn; + FormattedMessage = formattedMessage; + } + + /// + /// Path of the file that produced the message. + /// + public string SourceFilePath { get; } + + /// + /// Gets the error message. + /// + public string Message { get; } + + /// + /// Gets the one-based line index for the start of the compilation error. + /// + public int StartLine { get; } + + /// + /// Gets the zero-based column index for the start of the compilation error. + /// + public int StartColumn { get; } + + /// + /// Gets the one-based line index for the end of the compilation error. + /// + public int EndLine { get; } + + /// + /// Gets the zero-based column index for the end of the compilation error. + /// + public int EndColumn { get; } + + /// + /// Gets the formatted error message. + /// + public string FormattedMessage { get; } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.Abstractions/src/ICompilationException.cs b/src/Middleware/Diagnostics.Abstractions/src/ICompilationException.cs new file mode 100644 index 0000000000..480dddf74b --- /dev/null +++ b/src/Middleware/Diagnostics.Abstractions/src/ICompilationException.cs @@ -0,0 +1,22 @@ +// 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.Collections.Generic; + +namespace Microsoft.AspNetCore.Diagnostics +{ + /// + /// Specifies the contract for an exception representing compilation failure. + /// + /// + /// This interface is implemented on exceptions thrown during compilation to enable consumers + /// to read compilation-related data out of the exception + /// + public interface ICompilationException + { + /// + /// Gets a sequence of with compilation failures. + /// + IEnumerable CompilationFailures { get; } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.Abstractions/src/IExceptionHandlerFeature.cs b/src/Middleware/Diagnostics.Abstractions/src/IExceptionHandlerFeature.cs new file mode 100644 index 0000000000..23069e3df4 --- /dev/null +++ b/src/Middleware/Diagnostics.Abstractions/src/IExceptionHandlerFeature.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.AspNetCore.Diagnostics +{ + public interface IExceptionHandlerFeature + { + Exception Error { get; } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.Abstractions/src/IExceptionHandlerPathFeature.cs b/src/Middleware/Diagnostics.Abstractions/src/IExceptionHandlerPathFeature.cs new file mode 100644 index 0000000000..7de318da8c --- /dev/null +++ b/src/Middleware/Diagnostics.Abstractions/src/IExceptionHandlerPathFeature.cs @@ -0,0 +1,17 @@ +// 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. + +namespace Microsoft.AspNetCore.Diagnostics +{ + /// + /// Represents an exception handler with the original path of the request. + /// + public interface IExceptionHandlerPathFeature : IExceptionHandlerFeature + { + /// + /// The portion of the request path that identifies the requested resource. The value + /// is un-escaped. + /// + string Path { get; } + } +} diff --git a/src/Middleware/Diagnostics.Abstractions/src/IStatusCodePagesFeature.cs b/src/Middleware/Diagnostics.Abstractions/src/IStatusCodePagesFeature.cs new file mode 100644 index 0000000000..d5c709e869 --- /dev/null +++ b/src/Middleware/Diagnostics.Abstractions/src/IStatusCodePagesFeature.cs @@ -0,0 +1,16 @@ +// 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. + +namespace Microsoft.AspNetCore.Diagnostics +{ + /// + /// Represents the Status code pages feature. + /// + public interface IStatusCodePagesFeature + { + /// + /// Indicates if the status code middleware will handle responses. + /// + bool Enabled { get; set; } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.Abstractions/src/IStatusCodeReExecuteFeature.cs b/src/Middleware/Diagnostics.Abstractions/src/IStatusCodeReExecuteFeature.cs new file mode 100644 index 0000000000..31451e0bc7 --- /dev/null +++ b/src/Middleware/Diagnostics.Abstractions/src/IStatusCodeReExecuteFeature.cs @@ -0,0 +1,14 @@ +// 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. + +namespace Microsoft.AspNetCore.Diagnostics +{ + public interface IStatusCodeReExecuteFeature + { + string OriginalPathBase { get; set; } + + string OriginalPath { get; set; } + + string OriginalQueryString { get; set; } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.Abstractions/src/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj b/src/Middleware/Diagnostics.Abstractions/src/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj new file mode 100644 index 0000000000..ee9c1f7b85 --- /dev/null +++ b/src/Middleware/Diagnostics.Abstractions/src/Microsoft.AspNetCore.Diagnostics.Abstractions.csproj @@ -0,0 +1,11 @@ + + + + ASP.NET Core diagnostics middleware abstractions and feature interface definitions. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;diagnostics + + + diff --git a/src/Middleware/Diagnostics.Abstractions/src/baseline.netcore.json b/src/Middleware/Diagnostics.Abstractions/src/baseline.netcore.json new file mode 100644 index 0000000000..6cbf6b065c --- /dev/null +++ b/src/Middleware/Diagnostics.Abstractions/src/baseline.netcore.json @@ -0,0 +1,356 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Diagnostics.CompilationFailure", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SourceFilePath", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SourceFileContent", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CompiledContent", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Messages", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FailureSummary", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "sourceFilePath", + "Type": "System.String" + }, + { + "Name": "sourceFileContent", + "Type": "System.String" + }, + { + "Name": "compiledContent", + "Type": "System.String" + }, + { + "Name": "messages", + "Type": "System.Collections.Generic.IEnumerable" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "sourceFilePath", + "Type": "System.String" + }, + { + "Name": "sourceFileContent", + "Type": "System.String" + }, + { + "Name": "compiledContent", + "Type": "System.String" + }, + { + "Name": "messages", + "Type": "System.Collections.Generic.IEnumerable" + }, + { + "Name": "failureSummary", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Diagnostics.DiagnosticMessage", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SourceFilePath", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Message", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StartLine", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_StartColumn", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_EndLine", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_EndColumn", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FormattedMessage", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "message", + "Type": "System.String" + }, + { + "Name": "formattedMessage", + "Type": "System.String" + }, + { + "Name": "filePath", + "Type": "System.String" + }, + { + "Name": "startLine", + "Type": "System.Int32" + }, + { + "Name": "startColumn", + "Type": "System.Int32" + }, + { + "Name": "endLine", + "Type": "System.Int32" + }, + { + "Name": "endColumn", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Diagnostics.ICompilationException", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_CompilationFailures", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IEnumerable", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Error", + "Parameters": [], + "ReturnType": "System.Exception", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Diagnostics.IExceptionHandlerPathFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Diagnostics.IStatusCodePagesFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Enabled", + "Parameters": [], + "ReturnType": "System.Boolean", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Enabled", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Diagnostics.IStatusCodeReExecuteFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OriginalPathBase", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OriginalPathBase", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OriginalPath", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OriginalPath", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_OriginalQueryString", + "Parameters": [], + "ReturnType": "System.String", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_OriginalQueryString", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageExtensions.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageExtensions.cs new file mode 100644 index 0000000000..6184c9f310 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageExtensions.cs @@ -0,0 +1,62 @@ +// 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.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +// ReSharper disable once CheckNamespace +namespace Microsoft.AspNetCore.Builder +{ + /// + /// extension methods for the . + /// + public static class DatabaseErrorPageExtensions + { + /// + /// 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. + /// + /// The to register the middleware with. + /// The same instance so that multiple calls can be chained. + public static IApplicationBuilder UseDatabaseErrorPage(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseDatabaseErrorPage(new DatabaseErrorPageOptions()); + } + + /// + /// 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. + /// + /// The to register the middleware with. + /// A that specifies options for the middleware. + /// The same instance so that multiple calls can be chained. + public static IApplicationBuilder UseDatabaseErrorPage( + this IApplicationBuilder app, DatabaseErrorPageOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + app = app.UseMiddleware(Options.Create(options)); + + app.UseMigrationsEndPoint(new MigrationsEndPointOptions + { + Path = options.MigrationsEndPointPath + }); + + return app; + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageMiddleware.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageMiddleware.cs new file mode 100644 index 0000000000..c7dee3cf1f --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageMiddleware.cs @@ -0,0 +1,253 @@ +// 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.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Internal; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore +{ + /// + /// 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. + /// + public class DatabaseErrorPageMiddleware : IObserver, IObserver> + { + private static readonly AsyncLocal _localDiagnostic = new AsyncLocal(); + + 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; + + /// + /// Initializes a new instance of the class + /// + /// Delegate to execute the next piece of middleware in the request pipeline. + /// + /// The for the application. This middleware both produces logging messages and + /// consumes them to detect database related exception. + /// + /// The options to control what information is displayed on the error page. + public DatabaseErrorPageMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory, + IOptions options) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _next = next; + _options = options.Value; + _logger = loggerFactory.CreateLogger(); + + // Note: this currently leaks if the server hosting this middleware is disposed. + // See aspnet/Home #2825 + DiagnosticListener.AllListeners.Subscribe(this); + } + + /// + /// Process an individual request. + /// + /// The HTTP context for the current request. + /// A task that represents the asynchronous operation. + public virtual async Task Invoke(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(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 context = (DbContext)httpContext.RequestServices.GetService(contextType); + + if (context == null) + { + _logger.ContextNotRegisteredDatabaseErrorPageMiddleware(contextType.FullName); + } + else + { + var relationalDatabaseCreator = context.GetService() as IRelationalDatabaseCreator; + if (relationalDatabaseCreator == null) + { + _logger.NotRelationalDatabase(); + } + else + { + var databaseExists = await relationalDatabaseCreator.ExistsAsync(); + + var migrationsAssembly = context.GetService(); + var modelDiffer = context.GetService(); + + // 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(migrationsAssembly.ModelSnapshot?.Model, context.Model); + + var pendingMigrations + = (databaseExists + ? await context.Database.GetPendingMigrationsAsync() + : context.Database.GetMigrations()) + .ToArray(); + + if (pendingModelChanges || pendingMigrations.Any()) + { + var page = new DatabaseErrorPage + { + Model = new DatabaseErrorPageModel( + contextType, exception, databaseExists, pendingModelChanges, pendingMigrations, _options) + }; + + 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.OnNext(DiagnosticListener diagnosticListener) + { + if (diagnosticListener.Name == DbLoggerCategory.Name) + { + diagnosticListener.Subscribe(this); + } + } + + void IObserver>.OnNext(KeyValuePair 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.OnCompleted() + { + } + + void IObserver.OnError(Exception error) + { + } + + void IObserver>.OnCompleted() + { + } + + void IObserver>.OnError(Exception error) + { + } + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageOptions.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageOptions.cs new file mode 100644 index 0000000000..765467e5f6 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/DatabaseErrorPageOptions.cs @@ -0,0 +1,21 @@ +// 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 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore; +using Microsoft.AspNetCore.Http; + +// ReSharper disable once CheckNamespace +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Options for the . + /// + public class DatabaseErrorPageOptions + { + /// + /// Gets or sets the path that will listen + /// for requests to execute migrations commands. + /// + public virtual PathString MigrationsEndPointPath { get; set; } = MigrationsEndPointOptions.DefaultPath; + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Internal/DiagnosticsEntityFrameworkCoreLoggerExtensions.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Internal/DiagnosticsEntityFrameworkCoreLoggerExtensions.cs new file mode 100644 index 0000000000..e7749f488a --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Internal/DiagnosticsEntityFrameworkCoreLoggerExtensions.cs @@ -0,0 +1,153 @@ +// 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.Extensions.Logging; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Internal +{ + internal static class DiagnosticsEntityFrameworkCoreLoggerExtensions + { + // MigrationsEndPointMiddleware + private static readonly Action _noContextType = LoggerMessage.Define( + LogLevel.Error, + new EventId(1, "NoContextType"), + "No context type was specified. Ensure the form data from the request includes a contextTypeName value, specifying the context 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"), + "The context type '{ContextTypeName}' was not found in services. This usually means the context was not registered in services during startup. You probably want to call AddScoped<>() inside the UseServices(...) call in your application startup code."); + + private static readonly Action _requestPathMatched = LoggerMessage.Define( + LogLevel.Debug, + new EventId(4, "RequestPathMatched"), + "Request path matched the path configured for this migrations endpoint({RequestPath}). Attempting to process the migrations request."); + + private static readonly Action _applyingMigrations = LoggerMessage.Define( + LogLevel.Debug, + new EventId(5, "ApplyingMigrations"), + "Request is valid, applying migrations for context '{ContextTypeName}'"); + + private static readonly Action _migrationsApplied = LoggerMessage.Define( + LogLevel.Debug, + new EventId(6, "MigrationsApplied"), + "Migrations successfully applied for context '{ContextTypeName}'."); + + private static readonly Action _migrationsEndPointMiddlewareException = LoggerMessage.Define( + LogLevel.Error, + new EventId(7, "MigrationsEndPointException"), + "An error occurred while applying the migrations for '{ContextTypeName}'. See InnerException for details:"); + + // DatabaseErrorPageMiddleware + private static readonly Action _attemptingToMatchException = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1, "AttemptingToMatchException"), + "{ExceptionType} occurred, checking if Entity Framework recorded this exception as resulting from a failed database operation."); + + private static readonly Action _noRecordedException = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2, "NoRecordedException"), + "Entity Framework did not record any exceptions due to failed database operations. This means the current exception is not a failed Entity Framework database operation, or the current exception occurred from a DbContext that was not obtained from request services."); + + private static readonly Action _noMatch = LoggerMessage.Define( + LogLevel.Debug, + new EventId(3, "NoMatchFound"), + "The current exception (and its inner exceptions) do not match the last exception Entity Framework recorded due to a failed database operation. This means the database operation exception was handled and another exception occurred later in the request."); + + private static readonly Action _matched = LoggerMessage.Define( + LogLevel.Debug, + new EventId(4, "MatchFound"), + "Entity Framework recorded that the current exception was due to a failed database operation. Attempting to show database error page."); + + private static readonly Action _contextNotRegisteredDatabaseErrorPageMiddleware = LoggerMessage.Define( + LogLevel.Error, + new EventId(5, "ContextNotRegistered"), + "The context type '{ContextTypeName}' was not found in services. This usually means the context was not registered in services during startup. You probably want to call AddScoped<>() inside the UseServices(...) call in your application startup code. Skipping display of the database error page."); + + private static readonly Action _notRelationalDatabase = LoggerMessage.Define( + LogLevel.Debug, + new EventId(6, "NotRelationalDatabase"), + "The target data store is not a relational database. Skipping the database error page."); + + private static readonly Action _databaseErrorPageMiddlewareException = LoggerMessage.Define( + LogLevel.Error, + new EventId(7, "DatabaseErrorPageException"), + "An exception occurred while calculating the database error page content. Skipping display of the database error page."); + + public static void NoContextType(this ILogger logger) + { + _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); + } + + public static void RequestPathMatched(this ILogger logger, string requestPath) + { + _requestPathMatched(logger, requestPath, null); + } + + public static void ApplyingMigrations(this ILogger logger, string contextTypeName) + { + _applyingMigrations(logger, contextTypeName, null); + } + + public static void MigrationsApplied(this ILogger logger, string contextTypeName) + { + _migrationsApplied(logger, contextTypeName, null); + } + + public static void MigrationsEndPointMiddlewareException(this ILogger logger, string context, Exception exception) + { + _migrationsEndPointMiddlewareException(logger, context, exception); + } + + public static void AttemptingToMatchException(this ILogger logger, Type exceptionType) + { + _attemptingToMatchException(logger, exceptionType, null); + } + + public static void NoRecordedException(this ILogger logger) + { + _noRecordedException(logger, null); + } + + public static void NoMatch(this ILogger logger) + { + _noMatch(logger, null); + } + + public static void Matched(this ILogger logger) + { + _matched(logger, null); + } + + public static void NotRelationalDatabase(this ILogger logger) + { + _notRelationalDatabase(logger, null); + } + + public static void ContextNotRegisteredDatabaseErrorPageMiddleware(this ILogger logger, string contextTypeName) + { + _contextNotRegisteredDatabaseErrorPageMiddleware(logger, contextTypeName, null); + } + + public static void DatabaseErrorPageMiddlewareException(this ILogger logger, Exception exception) + { + _databaseErrorPageMiddlewareException(logger, exception); + } + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj new file mode 100644 index 0000000000..f418a50148 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj @@ -0,0 +1,21 @@ + + + + ASP.NET Core middleware for Entity Framework Core error pages. Use this middleware to detect and diagnose errors with Entity Framework Core migrations. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;diagnostics;entityframeworkcore + + + + + + + + + + + + + diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointExtensions.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointExtensions.cs new file mode 100644 index 0000000000..c635bb43fd --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +// ReSharper disable once CheckNamespace +namespace Microsoft.AspNetCore.Builder +{ + /// + /// extension methods for the . + /// + public static class MigrationsEndPointExtensions + { + /// + /// Processes requests to execute migrations operations. The middleware will listen for requests made to . + /// + /// The to register the middleware with. + /// The same instance so that multiple calls can be chained. + public static IApplicationBuilder UseMigrationsEndPoint(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMigrationsEndPoint(new MigrationsEndPointOptions()); + } + + /// + /// Processes requests to execute migrations operations. The middleware will listen for requests to the path configured in . + /// + /// The to register the middleware with. + /// An action to set the options for the middleware. + /// The same instance so that multiple calls can be chained. + public static IApplicationBuilder UseMigrationsEndPoint(this IApplicationBuilder app, MigrationsEndPointOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return app.UseMiddleware(Options.Create(options)); + } + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointMiddleware.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointMiddleware.cs new file mode 100644 index 0000000000..4562bf4218 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointMiddleware.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Internal; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore +{ + /// + /// Processes requests to execute migrations operations. The middleware will listen for requests to the path configured in the supplied options. + /// + public class MigrationsEndPointMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly MigrationsEndPointOptions _options; + + /// + /// Initializes a new instance of the class + /// + /// Delegate to execute the next piece of middleware in the request pipeline. + /// The to write messages to. + /// The options to control the behavior of the middleware. + public MigrationsEndPointMiddleware( + RequestDelegate next, + ILogger logger, + IOptions options) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _next = next; + _logger = logger; + _options = options.Value; + } + + /// + /// Process an individual request. + /// + /// The context for the current request. + /// A task that represents the asynchronous operation. + public virtual async Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Request.Path.Equals(_options.Path)) + { + _logger.RequestPathMatched(context.Request.Path); + + var db = await GetDbContext(context, _logger); + + if (db != null) + { + try + { + _logger.ApplyingMigrations(db.GetType().FullName); + + db.Database.Migrate(); + + context.Response.StatusCode = (int)HttpStatusCode.NoContent; + context.Response.Headers.Add("Pragma", new[] { "no-cache" }); + context.Response.Headers.Add("Cache-Control", new[] { "no-cache" }); + + _logger.MigrationsApplied(db.GetType().FullName); + } + catch (Exception ex) + { + var message = Strings.FormatMigrationsEndPointMiddleware_Exception(db.GetType().FullName) + ex; + + _logger.MigrationsEndPointMiddlewareException(db.GetType().FullName, ex); + + throw new InvalidOperationException(message, ex); + } + } + } + else + { + await _next(context); + } + } + + private static async Task GetDbContext(HttpContext context, ILogger logger) + { + var form = await context.Request.ReadFormAsync(); + var contextTypeName = form["context"]; + + if (string.IsNullOrWhiteSpace(contextTypeName)) + { + logger.NoContextType(); + + await WriteErrorToResponse(context.Response, Strings.MigrationsEndPointMiddleware_NoContextType); + + 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; + } + + private static async Task WriteErrorToResponse(HttpResponse response, string error) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + response.Headers.Add("Pragma", new[] { "no-cache" }); + response.Headers.Add("Cache-Control", new[] { "no-cache" }); + response.ContentType = "text/plain"; + + // Padding to >512 to ensure IE doesn't hide the message + // http://stackoverflow.com/questions/16741062/what-rules-does-ie-use-to-determine-whether-to-show-the-entity-body + await response.WriteAsync(error.PadRight(513)); + } + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointOptions.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointOptions.cs new file mode 100644 index 0000000000..48b053c13f --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointOptions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore; +using Microsoft.AspNetCore.Http; + +// ReSharper disable once CheckNamespace +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Options for the . + /// + public class MigrationsEndPointOptions + { + /// + /// The default value for . + /// + public static PathString DefaultPath = new PathString("/ApplyDatabaseMigrations"); + + /// + /// Gets or sets the path that the will listen + /// for requests to execute migrations commands. + /// + public virtual PathString Path { get; set; } = DefaultPath; + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Properties/AssemblyInfo.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..019e858be0 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// 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.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Properties/Strings.Designer.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Properties/Strings.Designer.cs new file mode 100644 index 0000000000..3266d2f144 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Properties/Strings.Designer.cs @@ -0,0 +1,408 @@ +// +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Strings + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Strings", typeof(Strings).GetTypeInfo().Assembly); + + /// + /// The context type '{0}' was not found in services. This usually means the context was not registered in services during startup. You probably want to call AddScoped<>() inside the UseServices(...) call in your application startup code. Skipping display of the database error page. + /// + internal static string DatabaseErrorPageMiddleware_ContextNotRegistered + { + get => GetString("DatabaseErrorPageMiddleware_ContextNotRegistered"); + } + + /// + /// The context type '{0}' was not found in services. This usually means the context was not registered in services during startup. You probably want to call AddScoped<>() inside the UseServices(...) call in your application startup code. Skipping display of the database error page. + /// + internal static string FormatDatabaseErrorPageMiddleware_ContextNotRegistered(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("DatabaseErrorPageMiddleware_ContextNotRegistered"), p0); + + /// + /// An exception occurred while calculating the database error page content. Skipping display of the database error page. + /// + internal static string DatabaseErrorPageMiddleware_Exception + { + get => GetString("DatabaseErrorPageMiddleware_Exception"); + } + + /// + /// An exception occurred while calculating the database error page content. Skipping display of the database error page. + /// + internal static string FormatDatabaseErrorPageMiddleware_Exception() + => GetString("DatabaseErrorPageMiddleware_Exception"); + + /// + /// > dotnet ef migrations add [migration name] + /// + internal static string DatabaseErrorPage_AddMigrationCommandCLI + { + get => GetString("DatabaseErrorPage_AddMigrationCommandCLI"); + } + + /// + /// > dotnet ef migrations add [migration name] + /// + internal static string FormatDatabaseErrorPage_AddMigrationCommandCLI() + => GetString("DatabaseErrorPage_AddMigrationCommandCLI"); + + /// + /// Apply Migrations + /// + internal static string DatabaseErrorPage_ApplyMigrationsButton + { + get => GetString("DatabaseErrorPage_ApplyMigrationsButton"); + } + + /// + /// Apply Migrations + /// + internal static string FormatDatabaseErrorPage_ApplyMigrationsButton() + => GetString("DatabaseErrorPage_ApplyMigrationsButton"); + + /// + /// Migrations Applied + /// + internal static string DatabaseErrorPage_ApplyMigrationsButtonDone + { + get => GetString("DatabaseErrorPage_ApplyMigrationsButtonDone"); + } + + /// + /// Migrations Applied + /// + internal static string FormatDatabaseErrorPage_ApplyMigrationsButtonDone() + => GetString("DatabaseErrorPage_ApplyMigrationsButtonDone"); + + /// + /// Applying Migrations... + /// + internal static string DatabaseErrorPage_ApplyMigrationsButtonRunning + { + get => GetString("DatabaseErrorPage_ApplyMigrationsButtonRunning"); + } + + /// + /// Applying Migrations... + /// + internal static string FormatDatabaseErrorPage_ApplyMigrationsButtonRunning() + => GetString("DatabaseErrorPage_ApplyMigrationsButtonRunning"); + + /// + /// An error occurred applying migrations, try applying them from the command line + /// + internal static string DatabaseErrorPage_ApplyMigrationsFailed + { + get => GetString("DatabaseErrorPage_ApplyMigrationsFailed"); + } + + /// + /// An error occurred applying migrations, try applying them from the command line + /// + internal static string FormatDatabaseErrorPage_ApplyMigrationsFailed() + => GetString("DatabaseErrorPage_ApplyMigrationsFailed"); + + /// + /// In Visual Studio, you can use the Package Manager Console to apply pending migrations to the database: + /// + internal static string DatabaseErrorPage_HowToApplyFromPMC + { + get => GetString("DatabaseErrorPage_HowToApplyFromPMC"); + } + + /// + /// In Visual Studio, you can use the Package Manager Console to apply pending migrations to the database: + /// + internal static string FormatDatabaseErrorPage_HowToApplyFromPMC() + => GetString("DatabaseErrorPage_HowToApplyFromPMC"); + + /// + /// Try refreshing the page + /// + internal static string DatabaseErrorPage_MigrationsAppliedRefresh + { + get => GetString("DatabaseErrorPage_MigrationsAppliedRefresh"); + } + + /// + /// Try refreshing the page + /// + internal static string FormatDatabaseErrorPage_MigrationsAppliedRefresh() + => GetString("DatabaseErrorPage_MigrationsAppliedRefresh"); + + /// + /// In Visual Studio, use the Package Manager Console to scaffold a new migration and apply it to the database: + /// + internal static string DatabaseErrorPage_NoDbOrMigrationsInfoPMC + { + get => GetString("DatabaseErrorPage_NoDbOrMigrationsInfoPMC"); + } + + /// + /// In Visual Studio, use the Package Manager Console to scaffold a new migration and apply it to the database: + /// + internal static string FormatDatabaseErrorPage_NoDbOrMigrationsInfoPMC() + => GetString("DatabaseErrorPage_NoDbOrMigrationsInfoPMC"); + + /// + /// Use migrations to create the database for {0} + /// + internal static string DatabaseErrorPage_NoDbOrMigrationsTitle + { + get => GetString("DatabaseErrorPage_NoDbOrMigrationsTitle"); + } + + /// + /// Use migrations to create the database for {0} + /// + internal static string FormatDatabaseErrorPage_NoDbOrMigrationsTitle(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("DatabaseErrorPage_NoDbOrMigrationsTitle"), p0); + + /// + /// In Visual Studio, use the Package Manager Console to scaffold a new migration for these changes and apply them to the database: + /// + internal static string DatabaseErrorPage_PendingChangesInfoPMC + { + get => GetString("DatabaseErrorPage_PendingChangesInfoPMC"); + } + + /// + /// In Visual Studio, use the Package Manager Console to scaffold a new migration for these changes and apply them to the database: + /// + internal static string FormatDatabaseErrorPage_PendingChangesInfoPMC() + => GetString("DatabaseErrorPage_PendingChangesInfoPMC"); + + /// + /// There are pending model changes for {0} + /// + internal static string DatabaseErrorPage_PendingChangesTitle + { + get => GetString("DatabaseErrorPage_PendingChangesTitle"); + } + + /// + /// There are pending model changes for {0} + /// + internal static string FormatDatabaseErrorPage_PendingChangesTitle(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("DatabaseErrorPage_PendingChangesTitle"), p0); + + /// + /// There are migrations for {0} that have not been applied to the database + /// + internal static string DatabaseErrorPage_PendingMigrationsInfo + { + get => GetString("DatabaseErrorPage_PendingMigrationsInfo"); + } + + /// + /// There are migrations for {0} that have not been applied to the database + /// + internal static string FormatDatabaseErrorPage_PendingMigrationsInfo(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("DatabaseErrorPage_PendingMigrationsInfo"), p0); + + /// + /// Applying existing migrations for {0} may resolve this issue + /// + internal static string DatabaseErrorPage_PendingMigrationsTitle + { + get => GetString("DatabaseErrorPage_PendingMigrationsTitle"); + } + + /// + /// Applying existing migrations for {0} may resolve this issue + /// + internal static string FormatDatabaseErrorPage_PendingMigrationsTitle(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("DatabaseErrorPage_PendingMigrationsTitle"), p0); + + /// + /// > dotnet ef database update + /// + internal static string DatabaseErrorPage_ApplyMigrationsCommandCLI + { + get => GetString("DatabaseErrorPage_ApplyMigrationsCommandCLI"); + } + + /// + /// > dotnet ef database update + /// + internal static string FormatDatabaseErrorPage_ApplyMigrationsCommandCLI() + => GetString("DatabaseErrorPage_ApplyMigrationsCommandCLI"); + + /// + /// The context type '{0}' was not found in services. This usually means the context was not registered in services during startup. You probably want to call AddScoped<{0}>() inside the UseServices(...) call in your application startup code. + /// + internal static string MigrationsEndPointMiddleware_ContextNotRegistered + { + get => GetString("MigrationsEndPointMiddleware_ContextNotRegistered"); + } + + /// + /// The context type '{0}' was not found in services. This usually means the context was not registered in services during startup. You probably want to call AddScoped<{0}>() inside the UseServices(...) call in your application startup code. + /// + internal static string FormatMigrationsEndPointMiddleware_ContextNotRegistered(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("MigrationsEndPointMiddleware_ContextNotRegistered"), p0); + + /// + /// An error occurred while applying the migrations for '{0}'. See InnerException for details. + /// + internal static string MigrationsEndPointMiddleware_Exception + { + get => GetString("MigrationsEndPointMiddleware_Exception"); + } + + /// + /// An error occurred while applying the migrations for '{0}'. See InnerException for details. + /// + internal static string FormatMigrationsEndPointMiddleware_Exception(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("MigrationsEndPointMiddleware_Exception"), p0); + + /// + /// The context type '{0}' could not be loaded. Ensure this is the correct type name for the context you are trying to apply migrations for. + /// + internal static string MigrationsEndPointMiddleware_InvalidContextType + { + get => GetString("MigrationsEndPointMiddleware_InvalidContextType"); + } + + /// + /// The context type '{0}' could not be loaded. Ensure this is the correct type name for the context you are trying to apply migrations for. + /// + internal static string FormatMigrationsEndPointMiddleware_InvalidContextType(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("MigrationsEndPointMiddleware_InvalidContextType"), p0); + + /// + /// No context type was specified. Ensure the form data from the request includes a contextTypeName value, specifying the context to apply migrations for. + /// + internal static string MigrationsEndPointMiddleware_NoContextType + { + get => GetString("MigrationsEndPointMiddleware_NoContextType"); + } + + /// + /// No context type was specified. Ensure the form data from the request includes a contextTypeName value, specifying the context to apply migrations for. + /// + internal static string FormatMigrationsEndPointMiddleware_NoContextType() + => GetString("MigrationsEndPointMiddleware_NoContextType"); + + /// + /// A database operation failed while processing the request. + /// + internal static string DatabaseErrorPage_Title + { + get => GetString("DatabaseErrorPage_Title"); + } + + /// + /// A database operation failed while processing the request. + /// + internal static string FormatDatabaseErrorPage_Title() + => GetString("DatabaseErrorPage_Title"); + + /// + /// Entity Framework did not record any exceptions due to failed database operations. This means the current exception is not a failed Entity Framework database operation, or the current exception occurred from a DbContext that was not obtained from request services. + /// + internal static string DatabaseErrorPage_NoRecordedException + { + get => GetString("DatabaseErrorPage_NoRecordedException"); + } + + /// + /// Entity Framework did not record any exceptions due to failed database operations. This means the current exception is not a failed Entity Framework database operation, or the current exception occurred from a DbContext that was not obtained from request services. + /// + internal static string FormatDatabaseErrorPage_NoRecordedException() + => GetString("DatabaseErrorPage_NoRecordedException"); + + /// + /// PM> Add-Migration [migration name] + /// + internal static string DatabaseErrorPage_AddMigrationCommandPMC + { + get => GetString("DatabaseErrorPage_AddMigrationCommandPMC"); + } + + /// + /// PM> Add-Migration [migration name] + /// + internal static string FormatDatabaseErrorPage_AddMigrationCommandPMC() + => GetString("DatabaseErrorPage_AddMigrationCommandPMC"); + + /// + /// PM> Update-Database + /// + internal static string DatabaseErrorPage_ApplyMigrationsCommandPMC + { + get => GetString("DatabaseErrorPage_ApplyMigrationsCommandPMC"); + } + + /// + /// PM> Update-Database + /// + internal static string FormatDatabaseErrorPage_ApplyMigrationsCommandPMC() + => GetString("DatabaseErrorPage_ApplyMigrationsCommandPMC"); + + /// + /// Alternatively, you can scaffold a new migration and apply it from a command prompt at your project directory: + /// + internal static string DatabaseErrorPage_NoDbOrMigrationsInfoCLI + { + get => GetString("DatabaseErrorPage_NoDbOrMigrationsInfoCLI"); + } + + /// + /// Alternatively, you can scaffold a new migration and apply it from a command prompt at your project directory: + /// + internal static string FormatDatabaseErrorPage_NoDbOrMigrationsInfoCLI() + => GetString("DatabaseErrorPage_NoDbOrMigrationsInfoCLI"); + + /// + /// Alternatively, you can scaffold a new migration and apply it from a command prompt at your project directory: + /// + internal static string DatabaseErrorPage_PendingChangesInfoCLI + { + get => GetString("DatabaseErrorPage_PendingChangesInfoCLI"); + } + + /// + /// Alternatively, you can scaffold a new migration and apply it from a command prompt at your project directory: + /// + internal static string FormatDatabaseErrorPage_PendingChangesInfoCLI() + => GetString("DatabaseErrorPage_PendingChangesInfoCLI"); + + /// + /// Alternatively, you can apply pending migrations from a command prompt at your project directory: + /// + internal static string DatabaseErrorPage_HowToApplyFromCLI + { + get => GetString("DatabaseErrorPage_HowToApplyFromCLI"); + } + + /// + /// Alternatively, you can apply pending migrations from a command prompt at your project directory: + /// + internal static string FormatDatabaseErrorPage_HowToApplyFromCLI() + => GetString("DatabaseErrorPage_HowToApplyFromCLI"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Strings.resx b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Strings.resx new file mode 100644 index 0000000000..3f987c8716 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Strings.resx @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The context type '{0}' was not found in services. This usually means the context was not registered in services during startup. You probably want to call AddScoped<>() inside the UseServices(...) call in your application startup code. Skipping display of the database error page. + + + An exception occurred while calculating the database error page content. Skipping display of the database error page. + + + > dotnet ef migrations add [migration name] + + + Apply Migrations + + + Migrations Applied + + + Applying Migrations... + + + An error occurred applying migrations, try applying them from the command line + + + In Visual Studio, you can use the Package Manager Console to apply pending migrations to the database: + + + Try refreshing the page + + + In Visual Studio, use the Package Manager Console to scaffold a new migration and apply it to the database: + + + Use migrations to create the database for {0} + + + In Visual Studio, use the Package Manager Console to scaffold a new migration for these changes and apply them to the database: + + + There are pending model changes for {0} + + + There are migrations for {0} that have not been applied to the database + + + Applying existing migrations for {0} may resolve this issue + + + > dotnet ef database update + + + The context type '{0}' was not found in services. This usually means the context was not registered in services during startup. You probably want to call AddScoped<{0}>() inside the UseServices(...) call in your application startup code. + + + An error occurred while applying the migrations for '{0}'. See InnerException for details. + + + The context type '{0}' could not be loaded. Ensure this is the correct type name for the context you are trying to apply migrations for. + + + No context type was specified. Ensure the form data from the request includes a contextTypeName value, specifying the context to apply migrations for. + + + A database operation failed while processing the request. + + + Entity Framework did not record any exceptions due to failed database operations. This means the current exception is not a failed Entity Framework database operation, or the current exception occurred from a DbContext that was not obtained from request services. + + + PM> Add-Migration [migration name] + + + PM> Update-Database + + + Alternatively, you can scaffold a new migration and apply it from a command prompt at your project directory: + + + Alternatively, you can scaffold a new migration and apply it from a command prompt at your project directory: + + + Alternatively, you can apply pending migrations from a command prompt at your project directory: + + \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPage.Designer.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPage.Designer.cs new file mode 100644 index 0000000000..b9a3641004 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPage.Designer.cs @@ -0,0 +1,424 @@ +// +#pragma warning disable 1591 +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views +{ + #line hidden +#line 1 "DatabaseErrorPage.cshtml" +using System; + +#line default +#line hidden + using System.Threading.Tasks; +#line 2 "DatabaseErrorPage.cshtml" +using System.Linq; + +#line default +#line hidden +#line 3 "DatabaseErrorPage.cshtml" +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore; + +#line default +#line hidden +#line 4 "DatabaseErrorPage.cshtml" +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views; + +#line default +#line hidden + internal class DatabaseErrorPage : Microsoft.Extensions.RazorViews.BaseView + { + #pragma warning disable 1998 + public async override global::System.Threading.Tasks.Task ExecuteAsync() + { +#line 5 "DatabaseErrorPage.cshtml" + + Response.StatusCode = 500; + Response.ContentType = "text/html; charset=utf-8"; + Response.ContentLength = null; // Clear any prior Content-Length + +#line default +#line hidden + WriteLiteral(@" + + + + + Internal Server Error + + + +

"); +#line 113 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_Title); + +#line default +#line hidden + WriteLiteral("

\r\n

\r\n"); +#line 115 "DatabaseErrorPage.cshtml" + for (Exception ex = Model.Exception; ex != null; ex = ex.InnerException) + { + +#line default +#line hidden + WriteLiteral(" "); +#line 117 "DatabaseErrorPage.cshtml" + Write(ex.GetType().Name); + +#line default +#line hidden + WriteLiteral(": "); +#line 117 "DatabaseErrorPage.cshtml" + Write(ex.Message); + +#line default +#line hidden + WriteLiteral("\r\n
\r\n"); +#line 119 "DatabaseErrorPage.cshtml" + } + +#line default +#line hidden + WriteLiteral("

\r\n
\r\n\r\n"); +#line 123 "DatabaseErrorPage.cshtml" + if (!Model.DatabaseExists && !Model.PendingMigrations.Any()) + { + +#line default +#line hidden + WriteLiteral("

"); +#line 125 "DatabaseErrorPage.cshtml" + Write(Strings.FormatDatabaseErrorPage_NoDbOrMigrationsTitle(Model.ContextType.Name)); + +#line default +#line hidden + WriteLiteral("

\r\n

"); +#line 126 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_NoDbOrMigrationsInfoPMC); + +#line default +#line hidden + WriteLiteral("

\r\n "); +#line 127 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_AddMigrationCommandPMC); + +#line default +#line hidden + WriteLiteral("\r\n
\r\n "); +#line 129 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC); + +#line default +#line hidden + WriteLiteral("\r\n

"); +#line 130 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_NoDbOrMigrationsInfoCLI); + +#line default +#line hidden + WriteLiteral("

\r\n "); +#line 131 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_AddMigrationCommandCLI); + +#line default +#line hidden + WriteLiteral("\r\n
\r\n "); +#line 133 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI); + +#line default +#line hidden + WriteLiteral("\r\n
\r\n"); +#line 135 "DatabaseErrorPage.cshtml" + } + else if (Model.PendingMigrations.Any()) + { + +#line default +#line hidden + WriteLiteral("
\r\n

"); +#line 139 "DatabaseErrorPage.cshtml" + Write(Strings.FormatDatabaseErrorPage_PendingMigrationsTitle(Model.ContextType.Name)); + +#line default +#line hidden + WriteLiteral("

\r\n

"); +#line 140 "DatabaseErrorPage.cshtml" + Write(Strings.FormatDatabaseErrorPage_PendingMigrationsInfo(Model.ContextType.Name)); + +#line default +#line hidden + WriteLiteral("

\r\n\r\n
    \r\n"); +#line 143 "DatabaseErrorPage.cshtml" + foreach (var migration in Model.PendingMigrations) + { + +#line default +#line hidden + WriteLiteral("
  • "); +#line 145 "DatabaseErrorPage.cshtml" + Write(migration); + +#line default +#line hidden + WriteLiteral("
  • \r\n"); +#line 146 "DatabaseErrorPage.cshtml" + } + +#line default +#line hidden + WriteLiteral("
\r\n\r\n

\r\n + + +

+ \r\n\r\n

"); +#line 190 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_HowToApplyFromPMC); + +#line default +#line hidden + WriteLiteral("

\r\n "); +#line 191 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC); + +#line default +#line hidden + WriteLiteral("\r\n

"); +#line 192 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_HowToApplyFromCLI); + +#line default +#line hidden + WriteLiteral("

\r\n "); +#line 193 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI); + +#line default +#line hidden + WriteLiteral("\r\n
\r\n
\r\n"); +#line 196 "DatabaseErrorPage.cshtml" + } + else if (Model.PendingModelChanges) + { + +#line default +#line hidden + WriteLiteral("
\r\n

"); +#line 200 "DatabaseErrorPage.cshtml" + Write(Strings.FormatDatabaseErrorPage_PendingChangesTitle(Model.ContextType.Name)); + +#line default +#line hidden + WriteLiteral("

\r\n

"); +#line 201 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_PendingChangesInfoPMC); + +#line default +#line hidden + WriteLiteral("

\r\n "); +#line 202 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_AddMigrationCommandPMC); + +#line default +#line hidden + WriteLiteral("\r\n
\r\n "); +#line 204 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC); + +#line default +#line hidden + WriteLiteral("\r\n

"); +#line 205 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_PendingChangesInfoCLI); + +#line default +#line hidden + WriteLiteral("

\r\n "); +#line 206 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_AddMigrationCommandCLI); + +#line default +#line hidden + WriteLiteral("\r\n
\r\n "); +#line 208 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI); + +#line default +#line hidden + WriteLiteral("\r\n
\r\n
\r\n"); +#line 211 "DatabaseErrorPage.cshtml" + } + +#line default +#line hidden + WriteLiteral("\r\n"); + } + #pragma warning restore 1998 +#line 11 "DatabaseErrorPage.cshtml" + + public DatabaseErrorPageModel Model { get; set; } + + public string UrlEncode(string content) + { + return UrlEncoder.Encode(content); + } + + public string JavaScriptEncode(string content) + { + return JavaScriptEncoder.Encode(content); + } + +#line default +#line hidden + } +} +#pragma warning restore 1591 diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPage.cshtml b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPage.cshtml new file mode 100644 index 0000000000..70711befb1 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPage.cshtml @@ -0,0 +1,135 @@ +@using System +@using System.Linq +@using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore +@using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views +@{ + Response.StatusCode = 500; + Response.ContentType = "text/html; charset=utf-8"; + Response.ContentLength = null; // Clear any prior Content-Length +} +@functions +{ + public DatabaseErrorPageModel Model { get; set; } + + public string UrlEncode(string content) + { + return UrlEncoder.Encode(content); + } + + public string JavaScriptEncode(string content) + { + return JavaScriptEncoder.Encode(content); + } +} + + + + + + Internal Server Error + + + +

@Strings.DatabaseErrorPage_Title

+

+ @for (Exception ex = Model.Exception; ex != null; ex = ex.InnerException) + { + @ex.GetType().Name: @ex.Message +
+ } +

+
+ + @if (!Model.DatabaseExists && !Model.PendingMigrations.Any()) + { +

@Strings.FormatDatabaseErrorPage_NoDbOrMigrationsTitle(Model.ContextType.Name)

+

@Strings.DatabaseErrorPage_NoDbOrMigrationsInfoPMC

+ @Strings.DatabaseErrorPage_AddMigrationCommandPMC +
+ @Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC +

@Strings.DatabaseErrorPage_NoDbOrMigrationsInfoCLI

+ @Strings.DatabaseErrorPage_AddMigrationCommandCLI +
+ @Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI +
+ } + else if (Model.PendingMigrations.Any()) + { +
+

@Strings.FormatDatabaseErrorPage_PendingMigrationsTitle(Model.ContextType.Name)

+

@Strings.FormatDatabaseErrorPage_PendingMigrationsInfo(Model.ContextType.Name)

+ +
    + @foreach (var migration in Model.PendingMigrations) + { +
  • @migration
  • + } +
+ +

+ + + +

+ + +

@Strings.DatabaseErrorPage_HowToApplyFromPMC

+ @Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC +

@Strings.DatabaseErrorPage_HowToApplyFromCLI

+ @Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI +
+
+ } + else if (Model.PendingModelChanges) + { +
+

@Strings.FormatDatabaseErrorPage_PendingChangesTitle(Model.ContextType.Name)

+

@Strings.DatabaseErrorPage_PendingChangesInfoPMC

+ @Strings.DatabaseErrorPage_AddMigrationCommandPMC +
+ @Strings.DatabaseErrorPage_ApplyMigrationsCommandPMC +

@Strings.DatabaseErrorPage_PendingChangesInfoCLI

+ @Strings.DatabaseErrorPage_AddMigrationCommandCLI +
+ @Strings.DatabaseErrorPage_ApplyMigrationsCommandCLI +
+
+ } + + \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPageModel.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPageModel.cs new file mode 100644 index 0000000000..d6520c52ac --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/DatabaseErrorPageModel.cs @@ -0,0 +1,35 @@ +// 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 Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views +{ + internal class DatabaseErrorPageModel + { + public DatabaseErrorPageModel( + Type contextType, + Exception exception, + bool databaseExists, + bool pendingModelChanges, + IEnumerable pendingMigrations, + DatabaseErrorPageOptions options) + { + ContextType = contextType; + Exception = exception; + DatabaseExists = databaseExists; + PendingModelChanges = pendingModelChanges; + PendingMigrations = pendingMigrations; + Options = options; + } + + public virtual Type ContextType { get; } + public virtual Exception Exception { get; } + public virtual bool DatabaseExists { get; } + public virtual bool PendingModelChanges { get; } + public virtual IEnumerable PendingMigrations { get; } + public virtual DatabaseErrorPageOptions Options { get; } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/ErrorPage.css b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/ErrorPage.css new file mode 100644 index 0000000000..b875f43069 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/Views/ErrorPage.css @@ -0,0 +1,78 @@ +body { + font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif; + font-size: .813em; + line-height: 1.4em; + color: #222; +} + +h1, h2, h3, h4, h5 { + font-weight: 100; +} + +h1 { + color: #44525e; + margin: 15px 0 15px 0; +} + +h2 { + margin: 10px 5px 0 0; +} + +h3 { + color: #363636; + margin: 5px 5px 0 0; +} + +code { + font-family: Consolas, "Courier New", courier, monospace; +} + +a { + color: #1ba1e2; + text-decoration: none; +} + + a:hover { + color: #13709e; + text-decoration: underline; + } + +hr { + border: 1px #ddd solid; +} + +body .titleerror { + padding: 3px; +} + +#applyMigrations { + font-size: 14px; + background: #44c5f2; + color: #ffffff; + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-weight: normal; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: 1px solid transparent; +} + + #applyMigrations:disabled { + background-color: #a9e4f9; + border-color: #44c5f2; + } + +.error { + color: red; +} + +.expanded { + display: block; +} + +.collapsed { + display: none; +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/src/baseline.netcore.json b/src/Middleware/Diagnostics.EntityFrameworkCore/src/baseline.netcore.json new file mode 100644 index 0000000000..287e144156 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/src/baseline.netcore.json @@ -0,0 +1,795 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.DiagnosticsViewPage.Views.AttributeValue", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Prefix", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Literal", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromTuple", + "Parameters": [ + { + "Name": "value", + "Type": "System.Tuple" + } + ], + "ReturnType": "Microsoft.AspNetCore.DiagnosticsViewPage.Views.AttributeValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "FromTuple", + "Parameters": [ + { + "Name": "value", + "Type": "System.Tuple" + } + ], + "ReturnType": "Microsoft.AspNetCore.DiagnosticsViewPage.Views.AttributeValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "op_Implicit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Tuple" + } + ], + "ReturnType": "Microsoft.AspNetCore.DiagnosticsViewPage.Views.AttributeValue", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "prefix", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + }, + { + "Name": "literal", + "Type": "System.Boolean" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.DiagnosticsViewPage.Views.BaseView", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Context", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpContext", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Request", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpRequest", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Response", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.HttpResponse", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Output", + "Parameters": [], + "ReturnType": "System.IO.StreamWriter", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HtmlEncoder", + "Parameters": [], + "ReturnType": "System.Text.Encodings.Web.HtmlEncoder", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_HtmlEncoder", + "Parameters": [ + { + "Name": "value", + "Type": "System.Text.Encodings.Web.HtmlEncoder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UrlEncoder", + "Parameters": [], + "ReturnType": "System.Text.Encodings.Web.UrlEncoder", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UrlEncoder", + "Parameters": [ + { + "Name": "value", + "Type": "System.Text.Encodings.Web.UrlEncoder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_JavaScriptEncoder", + "Parameters": [], + "ReturnType": "System.Text.Encodings.Web.JavaScriptEncoder", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_JavaScriptEncoder", + "Parameters": [ + { + "Name": "value", + "Type": "System.Text.Encodings.Web.JavaScriptEncoder" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ExecuteAsync", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ExecuteAsync", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteLiteral", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteLiteral", + "Parameters": [ + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAttributeValue", + "Parameters": [ + { + "Name": "thingy", + "Type": "System.String" + }, + { + "Name": "startPostion", + "Type": "System.Int32" + }, + { + "Name": "value", + "Type": "System.Object" + }, + { + "Name": "endValue", + "Type": "System.Int32" + }, + { + "Name": "dealyo", + "Type": "System.Int32" + }, + { + "Name": "yesno", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "BeginWriteAttribute", + "Parameters": [ + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "begining", + "Type": "System.String" + }, + { + "Name": "startPosition", + "Type": "System.Int32" + }, + { + "Name": "ending", + "Type": "System.String" + }, + { + "Name": "endPosition", + "Type": "System.Int32" + }, + { + "Name": "thingy", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "EndWriteAttribute", + "Parameters": [], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteAttributeTo", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.TextWriter" + }, + { + "Name": "name", + "Type": "System.String" + }, + { + "Name": "leader", + "Type": "System.String" + }, + { + "Name": "trailer", + "Type": "System.String" + }, + { + "Name": "values", + "Type": "Microsoft.AspNetCore.DiagnosticsViewPage.Views.AttributeValue[]", + "IsParams": true + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Write", + "Parameters": [ + { + "Name": "result", + "Type": "Microsoft.AspNetCore.DiagnosticsViewPage.Views.HelperResult" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteTo", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.TextWriter" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteTo", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.TextWriter" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteLiteralTo", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.TextWriter" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteLiteralTo", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.TextWriter" + }, + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "HtmlEncodeAndReplaceLineBreaks", + "Parameters": [ + { + "Name": "input", + "Type": "System.String" + } + ], + "ReturnType": "System.String", + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.DiagnosticsViewPage.Views.HelperResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_WriteAction", + "Parameters": [], + "ReturnType": "System.Action", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteTo", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.TextWriter" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "action", + "Type": "System.Action" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.DatabaseErrorPageMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "System.IObserver", + "System.IObserver>" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "loggerFactory", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.MigrationsEndPointMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "logger", + "Type": "Microsoft.Extensions.Logging.ILogger" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.DatabaseErrorPageExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseDatabaseErrorPage", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseDatabaseErrorPage", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Builder.DatabaseErrorPageOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.DatabaseErrorPageOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_MigrationsEndPointPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MigrationsEndPointPath", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.MigrationsEndPointExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseMigrationsEndPoint", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseMigrationsEndPoint", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Builder.MigrationsEndPointOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.MigrationsEndPointOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Path", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Path", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Http.PathString" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DefaultPath", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Http.PathString", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/DatabaseErrorPageMiddlewareTest.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/DatabaseErrorPageMiddlewareTest.cs new file mode 100644 index 0000000000..bc9764c339 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/DatabaseErrorPageMiddlewareTest.cs @@ -0,0 +1,526 @@ +// 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.SqlClient; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.FunctionalTests.Helpers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests +{ + public class DatabaseErrorPageMiddlewareTest + { + [Fact] + public async Task Successful_requests_pass_thru() + { + var builder = new WebHostBuilder().Configure(app => app + .UseDatabaseErrorPage() + .UseMiddleware()); + var server = new TestServer(builder); + + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/"); + + Assert.Equal("Request Handled", await response.Content.ReadAsStringAsync()); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + class SuccessMiddleware + { + public SuccessMiddleware(RequestDelegate next) + { } + + public virtual async Task Invoke(HttpContext context) + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync("Request Handled"); + } + } + + [Fact] + public async Task Non_database_exceptions_pass_thru() + { + var builder = new WebHostBuilder().Configure(app => app + .UseDatabaseErrorPage() + .UseMiddleware()); + var server = new TestServer(builder); + + var ex = await Assert.ThrowsAsync(async () => + await server.CreateClient().GetAsync("http://localhost/")); + + Assert.Equal("Exception requested from TestMiddleware", ex.Message); + } + + class ExceptionMiddleware + { + public ExceptionMiddleware(RequestDelegate next) + { } + + public virtual Task Invoke(HttpContext context) + { + throw new InvalidOperationException("Exception requested from TestMiddleware"); + } + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Existing_database_not_using_migrations_exception_passes_thru() + { + using (var database = SqlServerTestStore.CreateScratch()) + { + TestServer server = SetupTestServer(database); + var ex = await Assert.ThrowsAsync(async () => + await server.CreateClient().GetAsync("http://localhost/")); + + Assert.Equal("Invalid column name 'Name'.", ex.InnerException.Message); + } + } + + class DatabaseErrorButNoMigrationsMiddleware + { + public DatabaseErrorButNoMigrationsMiddleware(RequestDelegate next) + { } + + public virtual Task Invoke(HttpContext context) + { + var db = context.RequestServices.GetService(); + db.Database.EnsureCreated(); + db.Database.ExecuteSqlCommand("ALTER TABLE dbo.Blogs DROP COLUMN Name"); + + db.Blogs.Add(new Blog()); + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); + } + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Error_page_displayed_no_migrations() + { + using (var database = SqlServerTestStore.CreateScratch()) + { + TestServer server = SetupTestServer(database); + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_NoDbOrMigrationsTitle", typeof(BloggingContext).Name), content); + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_AddMigrationCommandPMC").Replace(">", ">"), content); + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_ApplyMigrationsCommandPMC").Replace(">", ">"), content); + } + } + + class NoMigrationsMiddleware + { + public NoMigrationsMiddleware(RequestDelegate next) + { } + + public virtual Task Invoke(HttpContext context) + { + var db = context.RequestServices.GetService(); + db.Blogs.Add(new Blog()); + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); + } + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public void No_exception_on_diagnostic_event_received_when_null_state() + { + using (var database = SqlServerTestStore.CreateScratch()) + { + using (var server = SetupTestServer(database)) + { + using (var db = server.Host.Services.GetService()) + { + db.Blogs.Add(new Blog()); + + try + { + db.SaveChanges(); + } + catch (SqlException) + { + } + } + } + } + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Error_page_displayed_pending_migrations() + { + using (var database = SqlServerTestStore.CreateScratch()) + { + TestServer server = SetupTestServer(database); + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingMigrationsTitle", typeof(BloggingContextWithMigrations).Name), content); + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_ApplyMigrationsCommandPMC").Replace(">", ">"), content); + Assert.Contains("
  • 111111111111111_MigrationOne
  • ", content); + Assert.Contains("
  • 222222222222222_MigrationTwo
  • ", content); + + Assert.DoesNotContain(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_AddMigrationCommandPMC").Replace(">", ">"), content); + } + } + + class PendingMigrationsMiddleware + { + public PendingMigrationsMiddleware(RequestDelegate next) + { } + + public virtual Task Invoke(HttpContext context) + { + var db = context.RequestServices.GetService(); + db.Blogs.Add(new Blog()); + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); + } + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Error_page_displayed_pending_model_changes() + { + using (var database = SqlServerTestStore.CreateScratch()) + { + TestServer server = SetupTestServer(database); + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingChangesTitle", typeof(BloggingContextWithPendingModelChanges).Name), content); + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_AddMigrationCommandCLI").Replace(">", ">"), content); + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_AddMigrationCommandPMC").Replace(">", ">"), content); + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_ApplyMigrationsCommandCLI").Replace(">", ">"), content); + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_ApplyMigrationsCommandPMC").Replace(">", ">"), content); + } + } + + class PendingModelChangesMiddleware + { + public PendingModelChangesMiddleware(RequestDelegate next) + { } + + public virtual Task Invoke(HttpContext context) + { + var db = context.RequestServices.GetService(); + db.Database.Migrate(); + + db.Blogs.Add(new Blog()); + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); + } + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Error_page_then_apply_migrations() + { + using (var database = SqlServerTestStore.CreateScratch()) + { + TestServer server = SetupTestServer(database); + var client = server.CreateClient(); + + var expectedMigrationsEndpoint = "/ApplyDatabaseMigrations"; + var expectedContextType = typeof(BloggingContextWithMigrations).AssemblyQualifiedName; + + // Step One: Initial request with database failure + HttpResponseMessage response = await client.GetAsync("http://localhost/"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + + // Ensure the url we're going to test is what the page is using in it's JavaScript + var javaScriptEncoder = JavaScriptEncoder.Default; + Assert.Contains("req.open(\"POST\", \"" + JavaScriptEncode(expectedMigrationsEndpoint) + "\", true);", content); + Assert.Contains("var formBody = \"context=" + JavaScriptEncode(UrlEncode(expectedContextType)) + "\";", content); + + // Step Two: Request to migrations endpoint + var formData = new FormUrlEncodedContent(new List> + { + new KeyValuePair("context", expectedContextType) + }); + + response = await client.PostAsync("http://localhost" + expectedMigrationsEndpoint, formData); + content = await response.Content.ReadAsStringAsync(); + Console.WriteLine(content); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // Step Three: Successful request after migrations applied + response = await client.GetAsync("http://localhost/"); + content = await response.Content.ReadAsStringAsync(); + Assert.Equal("Saved a Blog", content); + } + } + + class ApplyMigrationsMiddleware + { + public ApplyMigrationsMiddleware(RequestDelegate next) + { } + + public virtual async Task Invoke(HttpContext context) + { + var db = context.RequestServices.GetService(); + db.Blogs.Add(new Blog()); + db.SaveChanges(); + await context.Response.WriteAsync("Saved a Blog"); + } + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Customize_migrations_end_point() + { + var migrationsEndpoint = "/MyCustomEndPoints/ApplyMyMigrationsHere"; + + using (var database = SqlServerTestStore.CreateScratch()) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseDatabaseErrorPage(new DatabaseErrorPageOptions + { + MigrationsEndPointPath = new PathString(migrationsEndpoint) + }); + + app.UseMiddleware(); + }) + .ConfigureServices(services => + { + services.AddDbContext(optionsBuilder => + { + optionsBuilder.UseSqlServer(database.ConnectionString); + }); + }); + var server = new TestServer(builder); + + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("req.open(\"POST\", \"" + JavaScriptEncode(migrationsEndpoint) + "\", true);", content); + } + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Pass_thru_when_context_not_in_services() + { + var logProvider = new TestLoggerProvider(); + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseDatabaseErrorPage(); + app.UseMiddleware(); +#pragma warning disable CS0618 // Type or member is obsolete + app.ApplicationServices.GetService().AddProvider(logProvider); +#pragma warning restore CS0618 // Type or member is obsolete + }); + var server = new TestServer(builder); + + var ex = await Assert.ThrowsAsync(async () => + { + try + { + await server.CreateClient().GetAsync("http://localhost/"); + } + catch (InvalidOperationException exception) when (exception.InnerException != null) + { + throw exception.InnerException; + } + }); + + Assert.Contains(logProvider.Logger.Messages.ToList(), m => + m.StartsWith(StringsHelpers.GetResourceString("FormatDatabaseErrorPageMiddleware_ContextNotRegistered", typeof(BloggingContext)))); + } + + class ContextNotRegisteredInServicesMiddleware + { + public ContextNotRegisteredInServicesMiddleware(RequestDelegate next) + { } + + public virtual Task Invoke(HttpContext context) + { + using (var database = SqlServerTestStore.CreateScratch()) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseLoggerFactory(context.RequestServices.GetService()); + if (!PlatformHelper.IsMono) + { + optionsBuilder.UseSqlServer(database.ConnectionString, b => b.CommandTimeout(600).EnableRetryOnFailure()); + } + else + { + optionsBuilder.UseInMemoryDatabase("Scratch"); + } + + var db = new BloggingContext(optionsBuilder.Options); + db.Blogs.Add(new Blog()); + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); + } + } + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Pass_thru_when_exception_in_logic() + { + using (var database = SqlServerTestStore.CreateScratch()) + { + var logProvider = new TestLoggerProvider(); + + var server = SetupTestServer(database, logProvider); + + var ex = await Assert.ThrowsAsync(async () => + { + try + { + await server.CreateClient().GetAsync("http://localhost/"); + } + catch (InvalidOperationException exception) when (exception.InnerException != null) + { + throw exception.InnerException; + } + }); + + Assert.Contains(logProvider.Logger.Messages.ToList(), m => + m.StartsWith(StringsHelpers.GetResourceString("FormatDatabaseErrorPageMiddleware_Exception"))); + } + } + + class ExceptionInLogicMiddleware + { + public ExceptionInLogicMiddleware(RequestDelegate next) + { } + + public virtual Task Invoke(HttpContext context) + { + var db = context.RequestServices.GetService(); + db.Blogs.Add(new Blog()); + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); + } + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Error_page_displayed_when_exception_wrapped() + { + using (var database = SqlServerTestStore.CreateScratch()) + { + TestServer server = SetupTestServer(database); + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("I wrapped your exception", content); + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_NoDbOrMigrationsTitle", typeof(BloggingContext).Name), content); + } + } + + class WrappedExceptionMiddleware + { + public WrappedExceptionMiddleware(RequestDelegate next) + { } + + public virtual Task Invoke(HttpContext context) + { + var db = context.RequestServices.GetService(); + db.Blogs.Add(new Blog()); + try + { + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); + } + catch (Exception ex) + { + throw new Exception("I wrapped your exception", ex); + } + } + } + + private static TestServer SetupTestServer(SqlServerTestStore database, ILoggerProvider logProvider = null) + where TContext : DbContext + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseDatabaseErrorPage(); + + app.UseMiddleware(); + + if (logProvider != null) + { +#pragma warning disable CS0618 // Type or member is obsolete + app.ApplicationServices.GetService().AddProvider(logProvider); +#pragma warning restore CS0618 // Type or member is obsolete + } + }) + .ConfigureServices(services => + { + services.AddDbContext(optionsBuilder => + { + if (!PlatformHelper.IsMono) + { + optionsBuilder.UseSqlServer( + database.ConnectionString, + b => b.CommandTimeout(600).EnableRetryOnFailure()); + } + else + { + optionsBuilder.UseInMemoryDatabase("Scratch"); + } + }); + }); + return new TestServer(builder); + } + + private static UrlEncoder _urlEncoder = UrlEncoder.Default; + private static string UrlEncode(string content) + { + return _urlEncoder.Encode(content); + } + + private static JavaScriptEncoder _javaScriptEncoder = JavaScriptEncoder.Default; + private static string JavaScriptEncode(string content) + { + return _javaScriptEncoder.Encode(content); + } + } +} diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Diagnostics.EFCore.FunctionalTests.csproj b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Diagnostics.EFCore.FunctionalTests.csproj new file mode 100644 index 0000000000..a82ac7c490 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Diagnostics.EFCore.FunctionalTests.csproj @@ -0,0 +1,16 @@ + + + + $(StandardTestTfms) + + Diagnostics.EFCore.FunctionalTests + + + + + + + + + + diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Helpers/PlatformHelper.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Helpers/PlatformHelper.cs new file mode 100644 index 0000000000..b05aacb836 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Helpers/PlatformHelper.cs @@ -0,0 +1,18 @@ +// 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; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.FunctionalTests.Helpers +{ + public class PlatformHelper + { + public static bool IsMono + { + get + { + return Type.GetType("Mono.Runtime") != null; + } + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Helpers/StringHelpers.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Helpers/StringHelpers.cs new file mode 100644 index 0000000000..6b4263791a --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Helpers/StringHelpers.cs @@ -0,0 +1,19 @@ +// 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.Reflection; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.FunctionalTests.Helpers +{ + public class StringsHelpers + { + public static string GetResourceString(string stringName, params object[] parameters) + { + var strings = typeof(DatabaseErrorPageMiddleware).GetTypeInfo().Assembly.GetType("Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Strings").GetTypeInfo(); + var method = strings.GetDeclaredMethods(stringName).Single(); + return (string)method.Invoke(null, parameters); + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Helpers/TestLoggerProvider.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Helpers/TestLoggerProvider.cs new file mode 100644 index 0000000000..901557715a --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/Helpers/TestLoggerProvider.cs @@ -0,0 +1,61 @@ +// 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.Extensions.Logging; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.FunctionalTests.Helpers +{ + public class TestLoggerProvider : ILoggerProvider + { + private readonly TestLogger _logger = new TestLogger(); + + public TestLogger Logger + { + get { return _logger; } + } + + public ILogger CreateLogger(string name) + { + return _logger; + } + + public void Dispose() + { + } + + public class TestLogger : ILogger + { + private List _messages = new List(); + + public IEnumerable Messages + { + get { return _messages; } + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _messages.Add(formatter(state, exception)); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public IDisposable BeginScope(TState state) + { + return NullScope.Instance; + } + + public class NullScope : IDisposable + { + public static NullScope Instance = new NullScope(); + + public void Dispose() + { } + } + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/MigrationsEndPointMiddlewareTest.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/MigrationsEndPointMiddlewareTest.cs new file mode 100644 index 0000000000..d02f80a93b --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/MigrationsEndPointMiddlewareTest.cs @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.FunctionalTests.Helpers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests +{ + public class MigrationsEndPointMiddlewareTest + { + [Fact] + public async Task Non_migration_requests_pass_thru() + { + var builder = new WebHostBuilder().Configure(app => app + .UseMigrationsEndPoint() + .UseMiddleware()); + var server = new TestServer(builder); + + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/"); + + Assert.Equal("Request Handled", await response.Content.ReadAsStringAsync()); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + class SuccessMiddleware + { + public SuccessMiddleware(RequestDelegate next) + { } + + public virtual async Task Invoke(HttpContext context) + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync("Request Handled"); + } + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Migration_request_default_path() + { + await Migration_request(useCustomPath: false); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Migration_request_custom_path() + { + await Migration_request(useCustomPath: true); + } + + private async Task Migration_request(bool useCustomPath) + { + using (var database = SqlServerTestStore.CreateScratch()) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer(database.ConnectionString); + + var path = useCustomPath ? new PathString("/EndPoints/ApplyMyMigrations") : MigrationsEndPointOptions.DefaultPath; + + var builder = new WebHostBuilder() + .Configure(app => + { + if (useCustomPath) + { + app.UseMigrationsEndPoint(new MigrationsEndPointOptions + { + Path = path + }); + } + else + { + app.UseMigrationsEndPoint(); + } + }) + .ConfigureServices(services => + { + services.AddDbContext(options => + { + options.UseSqlServer(database.ConnectionString); + }); + }); + var server = new TestServer(builder); + + using (var db = BloggingContextWithMigrations.CreateWithoutExternalServiceProvider(optionsBuilder.Options)) + { + var databaseCreator = db.GetService(); + Assert.False(databaseCreator.Exists()); + + var formData = new FormUrlEncodedContent(new List> + { + new KeyValuePair("context", typeof(BloggingContextWithMigrations).AssemblyQualifiedName) + }); + + HttpResponseMessage response = await server.CreateClient() + .PostAsync("http://localhost" + path, formData); + + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + Assert.True(databaseCreator.Exists()); + + var historyRepository = db.GetService(); + var appliedMigrations = historyRepository.GetAppliedMigrations(); + Assert.Equal(2, appliedMigrations.Count); + Assert.Equal("111111111111111_MigrationOne", appliedMigrations.ElementAt(0).MigrationId); + Assert.Equal("222222222222222_MigrationTwo", appliedMigrations.ElementAt(1).MigrationId); + } + } + } + + [Fact] + public async Task Context_type_not_specified() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseMigrationsEndPoint(); + }); + var server = new TestServer(builder); + + var formData = new FormUrlEncodedContent(new List>()); + + var response = await server.CreateClient().PostAsync("http://localhost" + MigrationsEndPointOptions.DefaultPath, formData); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.StartsWith(StringsHelpers.GetResourceString("FormatMigrationsEndPointMiddleware_NoContextType"), content); + Assert.True(content.Length > 512); + } + + [Fact] + public async Task Invalid_context_type_specified() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseMigrationsEndPoint(); + }); + var server = new TestServer(builder); + + var typeName = "You won't find this type ;)"; + var formData = new FormUrlEncodedContent(new List> + { + new KeyValuePair("context", typeName) + }); + + var response = await server.CreateClient().PostAsync("http://localhost" + MigrationsEndPointOptions.DefaultPath, formData); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.StartsWith(StringsHelpers.GetResourceString("FormatMigrationsEndPointMiddleware_InvalidContextType", typeName), content); + Assert.True(content.Length > 512); + } + + [Fact] + public async Task Context_not_registered_in_services() + { + var builder = new WebHostBuilder() + .Configure(app => app.UseMigrationsEndPoint()) + .ConfigureServices(services => services.AddEntityFrameworkSqlServer()); + var server = new TestServer(builder); + + var formData = new FormUrlEncodedContent(new List> + { + new KeyValuePair("context", typeof(BloggingContext).AssemblyQualifiedName) + }); + + var response = await server.CreateClient().PostAsync("http://localhost" + MigrationsEndPointOptions.DefaultPath, formData); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.StartsWith(StringsHelpers.GetResourceString("FormatMigrationsEndPointMiddleware_ContextNotRegistered", typeof(BloggingContext)), content); + Assert.True(content.Length > 512); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task Exception_while_applying_migrations() + { + using (var database = SqlServerTestStore.CreateScratch()) + { + var builder = new WebHostBuilder() + .Configure(app => app.UseMigrationsEndPoint()) + .ConfigureServices(services => + { + services.AddDbContext(optionsBuilder => + { + optionsBuilder.UseSqlServer(database.ConnectionString); + }); + }); + var server = new TestServer(builder); + + var formData = new FormUrlEncodedContent(new List> + { + new KeyValuePair("context", typeof(BloggingContextWithSnapshotThatThrows).AssemblyQualifiedName) + }); + + var ex = await Assert.ThrowsAsync(async () => + await server.CreateClient().PostAsync("http://localhost" + MigrationsEndPointOptions.DefaultPath, formData)); + + Assert.StartsWith(StringsHelpers.GetResourceString("FormatMigrationsEndPointMiddleware_Exception", typeof(BloggingContextWithSnapshotThatThrows)), ex.Message); + Assert.Equal("Welcome to the invalid migration!", ex.InnerException.Message); + } + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/SqlServerTestStore.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/SqlServerTestStore.cs new file mode 100644 index 0000000000..cfdfcf8d07 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/SqlServerTestStore.cs @@ -0,0 +1,60 @@ +// 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.Data.SqlClient; +using System.Threading; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests +{ + public class SqlServerTestStore : IDisposable + { + private static int _scratchCount; + + public static SqlServerTestStore CreateScratch() + { + var name = "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.FunctionalTests.Scratch_" + Interlocked.Increment(ref _scratchCount); + var db = new SqlServerTestStore(name); + return db; + } + + private readonly string _connectionString; + + private SqlServerTestStore(string name) + { + _connectionString = new SqlConnectionStringBuilder + { + DataSource = @"(localdb)\MSSQLLocalDB", + InitialCatalog = name, + IntegratedSecurity = true, + ConnectTimeout = 600 + }.ConnectionString; + } + + public string ConnectionString + { + get { return _connectionString; } + } + + private void EnsureDeleted() + { + if (!PlatformHelper.IsMono) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer(_connectionString, b => b.CommandTimeout(600).EnableRetryOnFailure()); + + using (var db = new DbContext(optionsBuilder.Options)) + { + db.Database.EnsureDeleted(); + } + } + } + + public virtual void Dispose() + { + EnsureDeleted(); + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/Blog.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/Blog.cs new file mode 100644 index 0000000000..3861146450 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/Blog.cs @@ -0,0 +1,13 @@ +// 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; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests +{ + public class Blog + { + public int BlogId { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContext.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContext.cs new file mode 100644 index 0000000000..cab6af6b93 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContext.cs @@ -0,0 +1,18 @@ +// 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.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests +{ + public class BloggingContext : DbContext + { + public BloggingContext(DbContextOptions options) + : base(options) + { } + + public DbSet Blogs { get; set; } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContextWithMigrations.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContextWithMigrations.cs new file mode 100644 index 0000000000..90e38ed1c0 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContextWithMigrations.cs @@ -0,0 +1,71 @@ +// 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.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests +{ + public class BloggingContextWithMigrations : BloggingContext + { + public BloggingContextWithMigrations(DbContextOptions options) + : base(options) + { } + + // Providing a factory method so that the ctor is hidden from DI + public static BloggingContextWithMigrations CreateWithoutExternalServiceProvider(DbContextOptions options) + { + return new BloggingContextWithMigrations(options); + } + + [DbContext(typeof(BloggingContextWithMigrations))] + public class BloggingContextWithMigrationsModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder builder) + { + builder.Entity("Blogging.Models.Blog", b => + { + b.Property("BlogId"); + b.Property("Name"); + b.HasKey("BlogId"); + }); + } + } + + [DbContext(typeof(BloggingContextWithMigrations))] + [Migration("111111111111111_MigrationOne")] + public class MigrationOne : Migration + { + public override IModel TargetModel => new BloggingContextWithMigrationsModelSnapshot().Model; + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable("Blogs", + c => new + { + BlogId = c.Column().Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Name = c.Column(nullable: true), + }) + .PrimaryKey("PK_Blog", t => t.BlogId); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable("Blogs"); + } + } + + [DbContext(typeof(BloggingContextWithMigrations))] + [Migration("222222222222222_MigrationTwo")] + public class MigrationTwo : Migration + { + public override IModel TargetModel => new BloggingContextWithMigrationsModelSnapshot().Model; + + protected override void Up(MigrationBuilder migrationBuilder) + { } + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContextWithPendingModelChanges.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContextWithPendingModelChanges.cs new file mode 100644 index 0000000000..b2cc986039 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContextWithPendingModelChanges.cs @@ -0,0 +1,33 @@ +// 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.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests +{ + public class BloggingContextWithPendingModelChanges : BloggingContext + { + public BloggingContextWithPendingModelChanges(DbContextOptions options) + : base(options) + { } + + [DbContext(typeof(BloggingContextWithPendingModelChanges))] + public class BloggingModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + } + } + + [DbContext(typeof(BloggingContextWithPendingModelChanges))] + [Migration("111111111111111_MigrationOne")] + public partial class MigrationOne : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { } + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContextWithSnapshotThatThrows.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContextWithSnapshotThatThrows.cs new file mode 100644 index 0000000000..fb96dfa523 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/FunctionalTests/TestModels/BloggingContextWithSnapshotThatThrows.cs @@ -0,0 +1,39 @@ +// 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.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests +{ + public class BloggingContextWithSnapshotThatThrows : BloggingContext + { + public BloggingContextWithSnapshotThatThrows(DbContextOptions options) + : base(options) + { } + + [DbContext(typeof(BloggingContextWithSnapshotThatThrows))] + public class BloggingContextWithSnapshotThatThrowsModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + throw new Exception("Welcome to the invalid snapshot!"); + } + } + + [DbContext(typeof(BloggingContextWithSnapshotThatThrows))] + [Migration("111111111111111_MigrationOne")] + public class MigrationOne : Migration + { + public override IModel TargetModel => new BloggingContextWithSnapshotThatThrowsModelSnapshot().Model; + + protected override void Up(MigrationBuilder migrationBuilder) + { + throw new Exception("Welcome to the invalid migration!"); + } + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseErrorPageOptionsTest.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseErrorPageOptionsTest.cs new file mode 100644 index 0000000000..842c5f14a7 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseErrorPageOptionsTest.cs @@ -0,0 +1,28 @@ +// 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 Microsoft.AspNetCore.Builder; +using Xunit; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests +{ + public class DatabaseErrorPageOptionsTest + { + [Fact] + public void Empty_MigrationsEndPointPath_by_default() + { + var options = new DatabaseErrorPageOptions(); + + Assert.Equal(MigrationsEndPointOptions.DefaultPath, options.MigrationsEndPointPath); + } + + [Fact] + public void MigrationsEndPointPath_is_respected() + { + var options = new DatabaseErrorPageOptions(); + options.MigrationsEndPointPath = "/test"; + + Assert.Equal("/test", options.MigrationsEndPointPath); + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseErrorPageTest.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseErrorPageTest.cs new file mode 100644 index 0000000000..46ca2f2a96 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/DatabaseErrorPageTest.cs @@ -0,0 +1,199 @@ +// 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 Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.Helpers; +using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Views; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Moq; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests +{ + public class DatabaseErrorPageTest + { + [Fact] + public async Task No_database_or_migrations_only_displays_scaffold_first_migration() + { + var options = new DatabaseErrorPageOptions(); + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception(), + databaseExists: false, + pendingModelChanges: false, + pendingMigrations: new string[] { }, + options: options); + + var content = await ExecutePage(options, model); + + AssertHelpers.DisplaysScaffoldFirstMigration(typeof(BloggingContext), content); + AssertHelpers.NotDisplaysApplyMigrations(typeof(BloggingContext), content); + AssertHelpers.NotDisplaysScaffoldNextMigraion(typeof(BloggingContext), content); + } + + [Fact] + public async Task No_database_with_migrations_only_displays_apply_migrations() + { + var options = new DatabaseErrorPageOptions(); + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception(), + databaseExists: false, + pendingModelChanges: false, + pendingMigrations: new[] { "111_MigrationOne" }, + options: options); + + var content = await ExecutePage(options, model); + + AssertHelpers.NotDisplaysScaffoldFirstMigration(typeof(BloggingContext), content); + AssertHelpers.DisplaysApplyMigrations(typeof(BloggingContext), content); + AssertHelpers.NotDisplaysScaffoldNextMigraion(typeof(BloggingContext), content); + } + + [Fact] + public async Task Existing_database_with_migrations_only_displays_apply_migrations() + { + var options = new DatabaseErrorPageOptions(); + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception(), + databaseExists: true, + pendingModelChanges: false, + pendingMigrations: new[] { "111_MigrationOne" }, + options: options); + + var content = await ExecutePage(options, model); + + AssertHelpers.NotDisplaysScaffoldFirstMigration(typeof(BloggingContext), content); + AssertHelpers.DisplaysApplyMigrations(typeof(BloggingContext), content); + AssertHelpers.NotDisplaysScaffoldNextMigraion(typeof(BloggingContext), content); + } + + [Fact] + public async Task Existing_database_with_migrations_and_pending_model_changes_only_displays_apply_migrations() + { + var options = new DatabaseErrorPageOptions(); + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception(), + databaseExists: true, + pendingModelChanges: true, + pendingMigrations: new[] { "111_MigrationOne" }, + options: options); + + var content = await ExecutePage(options, model); + + AssertHelpers.NotDisplaysScaffoldFirstMigration(typeof(BloggingContext), content); + AssertHelpers.DisplaysApplyMigrations(typeof(BloggingContext), content); + AssertHelpers.NotDisplaysScaffoldNextMigraion(typeof(BloggingContext), content); + } + + [Fact] + public async Task Pending_model_changes_only_displays_scaffold_next_migration() + { + var options = new DatabaseErrorPageOptions(); + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception(), + databaseExists: true, + pendingModelChanges: true, + pendingMigrations: new string[] { }, + options: options); + + var content = await ExecutePage(options, model); + + AssertHelpers.NotDisplaysScaffoldFirstMigration(typeof(BloggingContext), content); + AssertHelpers.NotDisplaysApplyMigrations(typeof(BloggingContext), content); + AssertHelpers.DisplaysScaffoldNextMigraion(typeof(BloggingContext), content); + } + + [Fact] + public async Task Exception_details_are_displayed() + { + var options = new DatabaseErrorPageOptions(); + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception("Something bad happened"), + databaseExists: false, + pendingModelChanges: false, + pendingMigrations: new string[] { }, + options: options); + + var content = await ExecutePage(options, model); + + Assert.Contains("Something bad happened", content); + } + + [Fact] + public async Task Inner_exception_details_are_displayed() + { + var options = new DatabaseErrorPageOptions(); + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception("Something bad happened", new Exception("Because something more badder happened")), + databaseExists: false, + pendingModelChanges: false, + pendingMigrations: new string[] { }, + options: options); + + var content = await ExecutePage(options, model); + + Assert.Contains("Something bad happened", content); + Assert.Contains("Because something more badder happened", content); + } + + [Fact] + public async Task MigrationsEndPointPath_is_respected() + { + var options = new DatabaseErrorPageOptions(); + options.MigrationsEndPointPath = "/HitThisEndPoint"; + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception(), + databaseExists: true, + pendingModelChanges: false, + pendingMigrations: new[] { "111_MigrationOne" }, + options: options); + + var content = await ExecutePage(options, model); + + Assert.Contains(options.MigrationsEndPointPath.Value, content); + } + + + private static async Task ExecutePage(DatabaseErrorPageOptions options, DatabaseErrorPageModel model) + { + var page = new DatabaseErrorPage(); + var context = new Mock(); + var response = new Mock(); + var stream = new MemoryStream(); + + response.Setup(r => r.Body).Returns(stream); + context.Setup(c => c.Response).Returns(response.Object); + + page.Model = model; + + await page.ExecuteAsync(context.Object); + var content = Encoding.ASCII.GetString(stream.ToArray()); + return content; + } + + private class BloggingContext : DbContext + { + + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Helpers/AssertHelpers.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Helpers/AssertHelpers.cs new file mode 100644 index 0000000000..80d8b45e18 --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Helpers/AssertHelpers.cs @@ -0,0 +1,41 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.Helpers +{ + public static class AssertHelpers + { + public static void DisplaysScaffoldFirstMigration(Type contextType, string content) + { + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_NoDbOrMigrationsTitle", contextType.Name), content); + } + + public static void NotDisplaysScaffoldFirstMigration(Type contextType, string content) + { + Assert.DoesNotContain(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_NoDbOrMigrationsTitle", contextType.Name), content); + } + + public static void DisplaysApplyMigrations(Type contextType, string content) + { + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingMigrationsTitle", contextType.Name), content); + } + + public static void NotDisplaysApplyMigrations(Type contextType, string content) + { + Assert.DoesNotContain(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingMigrationsTitle", contextType.Name), content); + } + + public static void DisplaysScaffoldNextMigraion(Type contextType, string content) + { + Assert.Contains(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingChangesTitle", contextType.Name), content); + } + + public static void NotDisplaysScaffoldNextMigraion(Type contextType, string content) + { + Assert.DoesNotContain(StringsHelpers.GetResourceString("FormatDatabaseErrorPage_PendingChangesTitle", contextType.Name), content); + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Helpers/StringHelpers.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Helpers/StringHelpers.cs new file mode 100644 index 0000000000..68d7d3392e --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Helpers/StringHelpers.cs @@ -0,0 +1,19 @@ +// 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.Reflection; + +namespace Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.Helpers +{ + public class StringsHelpers + { + public static string GetResourceString(string stringName, params object[] parameters) + { + var strings = typeof(DatabaseErrorPageMiddleware).GetTypeInfo().Assembly.GetType("Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Strings").GetTypeInfo(); + var method = strings.GetDeclaredMethods(stringName).Single(); + return (string)method.Invoke(null, parameters); + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.csproj b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.csproj new file mode 100644 index 0000000000..fc5a2b350f --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.Tests.csproj @@ -0,0 +1,13 @@ + + + + $(StandardTestTfms) + + + + + + + + + diff --git a/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/TestHelperExtensions.cs b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/TestHelperExtensions.cs new file mode 100644 index 0000000000..e87b4ef99d --- /dev/null +++ b/src/Middleware/Diagnostics.EntityFrameworkCore/test/UnitTests/TestHelperExtensions.cs @@ -0,0 +1,21 @@ +// 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 Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore.Tests +{ + public static class TestHelperExtensions + { + public static IServiceCollection AddProviderServices(this IServiceCollection serviceCollection) + { + return serviceCollection.AddEntityFrameworkInMemoryDatabase(); + } + + public static DbContextOptions UseProviderOptions(this DbContextOptions options) + { + return options; + } + } +} diff --git a/src/Middleware/Diagnostics/src/.csslintrc b/src/Middleware/Diagnostics/src/.csslintrc new file mode 100644 index 0000000000..fda9775958 --- /dev/null +++ b/src/Middleware/Diagnostics/src/.csslintrc @@ -0,0 +1,7 @@ +{ + "unique-headings": false, + "ids": false, + "box-sizing": false, + "qualified-headings": false, + "display-property-grouping": false +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics/src/.jshintrc b/src/Middleware/Diagnostics/src/.jshintrc new file mode 100644 index 0000000000..077404aaa4 --- /dev/null +++ b/src/Middleware/Diagnostics/src/.jshintrc @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs new file mode 100644 index 0000000000..6ef0f6dfe1 --- /dev/null +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs @@ -0,0 +1,53 @@ +// 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.Extensions.Options; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// extension methods for the . + /// + public static class DeveloperExceptionPageExtensions + { + /// + /// Captures synchronous and asynchronous instances from the pipeline and generates HTML error responses. + /// + /// The . + /// A reference to the after the operation has completed. + public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + + /// + /// Captures synchronous and asynchronous instances from the pipeline and generates HTML error responses. + /// + /// The . + /// A that specifies options for the middleware. + /// A reference to the after the operation has completed. + public static IApplicationBuilder UseDeveloperExceptionPage( + this IApplicationBuilder app, + DeveloperExceptionPageOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return app.UseMiddleware(Options.Create(options)); + } + } +} diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs new file mode 100644 index 0000000000..c58c53ce8b --- /dev/null +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -0,0 +1,210 @@ +// 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.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.Internal; +using Microsoft.AspNetCore.Diagnostics.RazorViews; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.StackTrace.Sources; + +namespace Microsoft.AspNetCore.Diagnostics +{ + /// + /// Captures synchronous and asynchronous exceptions from the pipeline and generates HTML error responses. + /// + public class DeveloperExceptionPageMiddleware + { + private readonly RequestDelegate _next; + private readonly DeveloperExceptionPageOptions _options; + private readonly ILogger _logger; + private readonly IFileProvider _fileProvider; + private readonly DiagnosticSource _diagnosticSource; + private readonly ExceptionDetailsProvider _exceptionDetailsProvider; + + /// + /// Initializes a new instance of the class + /// + /// + /// + /// + /// + /// + public DeveloperExceptionPageMiddleware( + RequestDelegate next, + IOptions options, + ILoggerFactory loggerFactory, + IHostingEnvironment hostingEnvironment, + DiagnosticSource diagnosticSource) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _next = next; + _options = options.Value; + _logger = loggerFactory.CreateLogger(); + _fileProvider = _options.FileProvider ?? hostingEnvironment.ContentRootFileProvider; + _diagnosticSource = diagnosticSource; + _exceptionDetailsProvider = new ExceptionDetailsProvider(_fileProvider, _options.SourceCodeLineCount); + } + + /// + /// Process an individual request. + /// + /// + /// + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + _logger.UnhandledException(ex); + + if (context.Response.HasStarted) + { + _logger.ResponseStartedErrorPageMiddleware(); + throw; + } + + try + { + context.Response.Clear(); + context.Response.StatusCode = 500; + + await DisplayException(context, ex); + + if (_diagnosticSource.IsEnabled("Microsoft.AspNetCore.Diagnostics.UnhandledException")) + { + _diagnosticSource.Write("Microsoft.AspNetCore.Diagnostics.UnhandledException", new { httpContext = context, exception = ex }); + } + + return; + } + catch (Exception ex2) + { + // If there's a Exception while generating the error page, re-throw the original exception. + _logger.DisplayErrorPageException(ex2); + } + throw; + } + } + + // Assumes the response headers have not been sent. If they have, still attempt to write to the body. + private Task DisplayException(HttpContext context, Exception ex) + { + var compilationException = ex as ICompilationException; + if (compilationException != null) + { + return DisplayCompilationException(context, compilationException); + } + + return DisplayRuntimeException(context, ex); + } + + private Task DisplayCompilationException( + HttpContext context, + ICompilationException compilationException) + { + var model = new CompilationErrorPageModel + { + Options = _options, + }; + + var errorPage = new CompilationErrorPage + { + Model = model + }; + + if (compilationException.CompilationFailures == null) + { + return errorPage.ExecuteAsync(context); + } + + foreach (var compilationFailure in compilationException.CompilationFailures) + { + if (compilationFailure == null) + { + continue; + } + + var stackFrames = new List(); + var exceptionDetails = new ExceptionDetails + { + StackFrames = stackFrames, + ErrorMessage = compilationFailure.FailureSummary, + }; + model.ErrorDetails.Add(exceptionDetails); + model.CompiledContent.Add(compilationFailure.CompiledContent); + + if (compilationFailure.Messages == null) + { + continue; + } + + var sourceLines = compilationFailure + .SourceFileContent? + .Split(new[] { Environment.NewLine }, StringSplitOptions.None); + + foreach (var item in compilationFailure.Messages) + { + if (item == null) + { + continue; + } + + var frame = new StackFrameSourceCodeInfo + { + File = compilationFailure.SourceFilePath, + Line = item.StartLine, + Function = string.Empty + }; + + if (sourceLines != null) + { + _exceptionDetailsProvider.ReadFrameContent(frame, sourceLines, item.StartLine, item.EndLine); + } + + frame.ErrorDetails = item.Message; + + stackFrames.Add(frame); + } + } + + return errorPage.ExecuteAsync(context); + } + + private Task DisplayRuntimeException(HttpContext context, Exception ex) + { + var request = context.Request; + + var model = new ErrorPageModel + { + Options = _options, + ErrorDetails = _exceptionDetailsProvider.GetDetails(ex), + Query = request.Query, + Cookies = request.Cookies, + Headers = request.Headers + }; + + var errorPage = new ErrorPage(model); + return errorPage.ExecuteAsync(context); + } + } +} diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageOptions.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageOptions.cs new file mode 100644 index 0000000000..2562dbc3b8 --- /dev/null +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageOptions.cs @@ -0,0 +1,37 @@ +// 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 Microsoft.AspNetCore.Diagnostics; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Options for the . + /// + public class DeveloperExceptionPageOptions + { + /// + /// Create an instance with the default options settings. + /// + public DeveloperExceptionPageOptions() + { + SourceCodeLineCount = 6; + } + + /// + /// Determines how many lines of code to include before and after the line of code + /// present in an exception's stack frame. Only applies when symbols are available and + /// source code referenced by the exception stack trace is present on the server. + /// + public int SourceCodeLineCount { get; set; } + + /// + /// Provides files containing source code used to display contextual information of an exception. + /// + /// + /// If null will use a . + /// + public IFileProvider FileProvider { get; set; } + } +} diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorModel.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorModel.cs new file mode 100644 index 0000000000..f84bb4f19d --- /dev/null +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorModel.cs @@ -0,0 +1,30 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.StackTrace.Sources; + +namespace Microsoft.AspNetCore.Diagnostics.RazorViews +{ + /// + /// Holds data to be displayed on the compilation error page. + /// + internal class CompilationErrorPageModel + { + /// + /// Options for what output to display. + /// + public DeveloperExceptionPageOptions Options { get; set; } + + /// + /// Detailed information about each parse or compilation error. + /// + public IList ErrorDetails { get; } = new List(); + + /// + /// Gets the generated content that produced the corresponding . + /// + public IList CompiledContent { get; } = new List(); + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.Designer.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.Designer.cs new file mode 100644 index 0000000000..d5342bf79a --- /dev/null +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.Designer.cs @@ -0,0 +1,751 @@ +// +#pragma warning disable 1591 +namespace Microsoft.AspNetCore.Diagnostics.RazorViews +{ + #line hidden +#line 1 "CompilationErrorPage.cshtml" +using System; + +#line default +#line hidden + using System.Threading.Tasks; +#line 2 "CompilationErrorPage.cshtml" +using System.Globalization; + +#line default +#line hidden +#line 3 "CompilationErrorPage.cshtml" +using System.Linq; + +#line default +#line hidden +#line 4 "CompilationErrorPage.cshtml" +using System.Net; + +#line default +#line hidden +#line 5 "CompilationErrorPage.cshtml" +using Microsoft.AspNetCore.Diagnostics; + +#line default +#line hidden +#line 6 "CompilationErrorPage.cshtml" +using Microsoft.AspNetCore.Diagnostics.RazorViews; + +#line default +#line hidden + internal class CompilationErrorPage : Microsoft.Extensions.RazorViews.BaseView + { + #pragma warning disable 1998 + public async override global::System.Threading.Tasks.Task ExecuteAsync() + { +#line 11 "CompilationErrorPage.cshtml" + + Response.StatusCode = 500; + Response.ContentType = "text/html; charset=utf-8"; + Response.ContentLength = null; // Clear any prior Content-Length + +#line default +#line hidden + WriteLiteral("\r\n\r\n \r\n \r\n "); +#line 20 "CompilationErrorPage.cshtml" + Write(Resources.ErrorPageHtml_Title); + +#line default +#line hidden + WriteLiteral(@" + + + +

    "); +#line 222 "CompilationErrorPage.cshtml" + Write(Resources.ErrorPageHtml_CompilationException); + +#line default +#line hidden + WriteLiteral("

    \r\n"); +#line 223 "CompilationErrorPage.cshtml" + + var exceptionDetailId = ""; + + +#line default +#line hidden + WriteLiteral(" "); +#line 226 "CompilationErrorPage.cshtml" + for (var i = 0; i < Model.ErrorDetails.Count; i++) + { + var errorDetail = Model.ErrorDetails[i]; + exceptionDetailId = "exceptionDetail" + i; + + +#line default +#line hidden + WriteLiteral("
    \r\n"); +#line 232 "CompilationErrorPage.cshtml" + + var stackFrameCount = 0; + var frameId = ""; + var fileName = errorDetail.StackFrames.FirstOrDefault()?.File; + if (!string.IsNullOrEmpty(fileName)) + { + +#line default +#line hidden + WriteLiteral("
    "); +#line 238 "CompilationErrorPage.cshtml" + Write(fileName); + +#line default +#line hidden + WriteLiteral("
    \r\n"); +#line 239 "CompilationErrorPage.cshtml" + } + + +#line default +#line hidden + WriteLiteral(" "); +#line 241 "CompilationErrorPage.cshtml" + if (!string.IsNullOrEmpty(errorDetail.ErrorMessage)) + { + +#line default +#line hidden + WriteLiteral("
    "); +#line 243 "CompilationErrorPage.cshtml" + Write(errorDetail.ErrorMessage); + +#line default +#line hidden + WriteLiteral("
    \r\n"); +#line 244 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden + WriteLiteral("
    \r\n
      \r\n"); +#line 247 "CompilationErrorPage.cshtml" + foreach (var frame in errorDetail.StackFrames) + { + stackFrameCount++; + frameId = "frame" + stackFrameCount; + + +#line default +#line hidden + WriteLiteral("
    • "); +#line 267 "CompilationErrorPage.cshtml" + Write(line); + +#line default +#line hidden + WriteLiteral("
    • \r\n"); +#line 268 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden + WriteLiteral(" \r\n"); +#line 270 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden + WriteLiteral("
    \r\n"); +#line 287 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden + WriteLiteral(" \r\n"); +#line 289 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden + WriteLiteral(" \r\n
    \r\n \r\n"); +#line 293 "CompilationErrorPage.cshtml" + if (!string.IsNullOrEmpty(Model.CompiledContent[i])) + { + +#line default +#line hidden + WriteLiteral("
    \r\n
    \r\n \r\n
    \r\n
    \r\n \r\n"); +#line 303 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden +#line 303 "CompilationErrorPage.cshtml" + + } + +#line default +#line hidden + WriteLiteral(@" + + + +"); + } + #pragma warning restore 1998 +#line 8 "CompilationErrorPage.cshtml" + + public CompilationErrorPageModel Model { get; set; } + +#line default +#line hidden + } +} +#pragma warning restore 1591 diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.cshtml b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.cshtml new file mode 100644 index 0000000000..961c6bedbb --- /dev/null +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.cshtml @@ -0,0 +1,116 @@ +@using System +@using System.Globalization +@using System.Linq +@using System.Net +@using Microsoft.AspNetCore.Diagnostics +@using Microsoft.AspNetCore.Diagnostics.RazorViews +@functions +{ + public CompilationErrorPageModel Model { get; set; } +} +@{ + Response.StatusCode = 500; + Response.ContentType = "text/html; charset=utf-8"; + Response.ContentLength = null; // Clear any prior Content-Length +} + + + + + @Resources.ErrorPageHtml_Title + + + +

    @Resources.ErrorPageHtml_CompilationException

    + @{ + var exceptionDetailId = ""; + } + @for (var i = 0; i < Model.ErrorDetails.Count; i++) + { + var errorDetail = Model.ErrorDetails[i]; + exceptionDetailId = "exceptionDetail" + i; + +
    + @{ + var stackFrameCount = 0; + var frameId = ""; + var fileName = errorDetail.StackFrames.FirstOrDefault()?.File; + if (!string.IsNullOrEmpty(fileName)) + { +
    @fileName
    + } + } + @if (!string.IsNullOrEmpty(errorDetail.ErrorMessage)) + { +
    @errorDetail.ErrorMessage
    + } +
    +
      + @foreach (var frame in errorDetail.StackFrames) + { + stackFrameCount++; + frameId = "frame" + stackFrameCount; + +
    • + @if (!string.IsNullOrEmpty(frame.ErrorDetails)) + { +

      @frame.ErrorDetails

      + } + + @if (frame.Line != 0 && frame.ContextCode.Any()) + { + +
      + @if (frame.PreContextCode.Any()) + { +
        + @foreach (var line in frame.PreContextCode) + { +
      1. @line
      2. + } +
      + } +
        + @foreach (var line in frame.ContextCode) + { +
      1. @line
      2. + } +
      + @if (frame.PostContextCode.Any()) + { +
        + @foreach (var line in frame.PostContextCode) + { +
      1. @line
      2. + } +
      + } +
      + } +
    • + } +
    +
    +
    + @if (!string.IsNullOrEmpty(Model.CompiledContent[i])) + { +
    +
    + +
    +
    +
    @Model.CompiledContent[i]
    +
    +
    + } + } + + + + diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/ErrorPage.Designer.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/ErrorPage.Designer.cs new file mode 100644 index 0000000000..ea4969b0ed --- /dev/null +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/ErrorPage.Designer.cs @@ -0,0 +1,1109 @@ +// +#pragma warning disable 1591 +namespace Microsoft.AspNetCore.Diagnostics.RazorViews +{ + #line hidden +#line 1 "ErrorPage.cshtml" +using System; + +#line default +#line hidden + using System.Threading.Tasks; +#line 2 "ErrorPage.cshtml" +using System.Globalization; + +#line default +#line hidden +#line 3 "ErrorPage.cshtml" +using System.Linq; + +#line default +#line hidden +#line 4 "ErrorPage.cshtml" +using System.Net; + +#line default +#line hidden +#line 5 "ErrorPage.cshtml" +using System.Reflection; + +#line default +#line hidden +#line 6 "ErrorPage.cshtml" +using Microsoft.AspNetCore.Diagnostics.RazorViews; + +#line default +#line hidden +#line 7 "ErrorPage.cshtml" +using Microsoft.AspNetCore.Diagnostics; + +#line default +#line hidden + internal class ErrorPage : Microsoft.Extensions.RazorViews.BaseView + { + #pragma warning disable 1998 + public async override global::System.Threading.Tasks.Task ExecuteAsync() + { +#line 17 "ErrorPage.cshtml" + + // TODO: Response.ReasonPhrase = "Internal Server Error"; + Response.ContentType = "text/html; charset=utf-8"; + string location = string.Empty; + +#line default +#line hidden + WriteLiteral("\r\n