diff --git a/DiagnosticsPages.sln b/DiagnosticsPages.sln index 4cdd5428..03831ee2 100644 --- a/DiagnosticsPages.sln +++ b/DiagnosticsPages.sln @@ -42,6 +42,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Diagnostic EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ElmPageSample", "samples\ElmPageSample\ElmPageSample.xproj", "{FFD28DCF-C24F-4C59-9B6B-F3B74CE13129}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "DatabaseErrorPageSample", "samples\DatabaseErrorPageSample\DatabaseErrorPageSample.xproj", "{FF7F11A1-14E7-4948-A853-2487D99DE0C6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -206,6 +208,18 @@ Global {FFD28DCF-C24F-4C59-9B6B-F3B74CE13129}.Release|Mixed Platforms.Build.0 = Release|Any CPU {FFD28DCF-C24F-4C59-9B6B-F3B74CE13129}.Release|x86.ActiveCfg = Release|Any CPU {FFD28DCF-C24F-4C59-9B6B-F3B74CE13129}.Release|x86.Build.0 = Release|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Debug|x86.ActiveCfg = Debug|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Debug|x86.Build.0 = Debug|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Release|Any CPU.Build.0 = Release|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Release|x86.ActiveCfg = Release|Any CPU + {FF7F11A1-14E7-4948-A853-2487D99DE0C6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -225,5 +239,6 @@ Global {CC1F5841-FE10-4DDB-8477-C4DE92BA759F} = {ACAA0157-A8C4-4152-93DE-90CCDF304087} {83FFB65A-97B1-45AA-BCB8-3F43966BC8A3} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D} {FFD28DCF-C24F-4C59-9B6B-F3B74CE13129} = {ACAA0157-A8C4-4152-93DE-90CCDF304087} + {FF7F11A1-14E7-4948-A853-2487D99DE0C6} = {ACAA0157-A8C4-4152-93DE-90CCDF304087} EndGlobalSection EndGlobal diff --git a/samples/DatabaseErrorPageSample/DatabaseErrorPageSample.xproj b/samples/DatabaseErrorPageSample/DatabaseErrorPageSample.xproj new file mode 100644 index 00000000..e3e9eb4c --- /dev/null +++ b/samples/DatabaseErrorPageSample/DatabaseErrorPageSample.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + ff7f11a1-14e7-4948-a853-2487d99de0c6 + DatabaseErrorPageSample + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 10233 + + + \ No newline at end of file diff --git a/samples/DatabaseErrorPageSample/Properties/launchSettings.json b/samples/DatabaseErrorPageSample/Properties/launchSettings.json new file mode 100644 index 00000000..794f3146 --- /dev/null +++ b/samples/DatabaseErrorPageSample/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNET_ENV": "Development" + } + }, + "web": { + "commandName": "web", + "commandLineArgs": " " + } + } +} \ No newline at end of file diff --git a/samples/DatabaseErrorPageSample/Startup.cs b/samples/DatabaseErrorPageSample/Startup.cs new file mode 100644 index 00000000..cc59dfbc --- /dev/null +++ b/samples/DatabaseErrorPageSample/Startup.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.AspNet.Builder; +using Microsoft.Data.Entity; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; + +namespace DatabaseErrorPageSample +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddEntityFramework() + .AddSqlServer() + .AddDbContext(options => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=DatabaseErrorPageSample;Trusted_Connection=True;")); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + app.UseDatabaseErrorPage(); + app.Run(context => + { + context.ApplicationServices.GetService().Blog.FirstOrDefault(); + return Task.FromResult(0); + }); + } + } + + public class MyContext : DbContext + { + public DbSet Blog { get; set; } + } + + public class Blog + { + public int BlogId { get; set; } + public string Url { get; set; } + } +} diff --git a/samples/DatabaseErrorPageSample/project.json b/samples/DatabaseErrorPageSample/project.json new file mode 100644 index 00000000..1c7bbee7 --- /dev/null +++ b/samples/DatabaseErrorPageSample/project.json @@ -0,0 +1,18 @@ +{ + "webroot": "wwwroot", + "dependencies": { + "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-*", + "EntityFramework.MicrosoftSqlServer": "7.0.0-*", + "EntityFramework.Commands": "7.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.Server.Kestrel": "1.0.0-*" + }, + "commands": { + "ef": "EntityFramework.Commands", + "web": "Microsoft.AspNet.Server.Kestrel" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + } + } diff --git a/samples/DatabaseErrorPageSample/wwwroot/Readme.md b/samples/DatabaseErrorPageSample/wwwroot/Readme.md new file mode 100644 index 00000000..9686df1c --- /dev/null +++ b/samples/DatabaseErrorPageSample/wwwroot/Readme.md @@ -0,0 +1 @@ +Sample demonstrating ErrorPage middleware. \ No newline at end of file diff --git a/samples/DatabaseErrorPageSample/wwwroot/web.config b/samples/DatabaseErrorPageSample/wwwroot/web.config new file mode 100644 index 00000000..9a0d90ab --- /dev/null +++ b/samples/DatabaseErrorPageSample/wwwroot/web.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageExtensions.cs b/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageExtensions.cs index 5dc3f550..a650eb09 100644 --- a/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageExtensions.cs +++ b/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageExtensions.cs @@ -4,6 +4,7 @@ using JetBrains.Annotations; using Microsoft.AspNet.Diagnostics.Entity; using Microsoft.AspNet.Diagnostics.Entity.Utilities; +using System; namespace Microsoft.AspNet.Builder { @@ -13,15 +14,35 @@ public static IApplicationBuilder UseDatabaseErrorPage([NotNull] this IApplicati { Check.NotNull(builder, nameof(builder)); - return builder.UseDatabaseErrorPage(DatabaseErrorPageOptions.ShowAll); + return builder.UseDatabaseErrorPage(options => options.EnableAll()); } - public static IApplicationBuilder UseDatabaseErrorPage([NotNull] this IApplicationBuilder builder, [NotNull] DatabaseErrorPageOptions options) + public static IApplicationBuilder UseDatabaseErrorPage([NotNull] this IApplicationBuilder builder, [NotNull] Action optionsAction) { Check.NotNull(builder, nameof(builder)); + Check.NotNull(optionsAction, nameof(optionsAction)); + + var options = new DatabaseErrorPageOptions(); + optionsAction(options); + + builder = builder.UseMiddleware(options); + + if(options.EnableMigrationCommands) + { + builder.UseMigrationsEndPoint(o => o.Path = options.MigrationsEndPointPath); + } + + return builder; + } + + public static void EnableAll([NotNull] this DatabaseErrorPageOptions options) + { Check.NotNull(options, nameof(options)); - return builder.UseMiddleware(options); + options.ShowExceptionDetails = true; + options.ListMigrations = true; + options.EnableMigrationCommands = true; + options.MigrationsEndPointPath = MigrationsEndPointOptions.DefaultPath; } } } diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageOptions.cs b/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageOptions.cs index c093d23b..4cacd57f 100644 --- a/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageOptions.cs +++ b/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageOptions.cs @@ -1,18 +1,15 @@ // 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.AspNet.Http; + namespace Microsoft.AspNet.Diagnostics.Entity { public class DatabaseErrorPageOptions { - public static DatabaseErrorPageOptions ShowAll => new DatabaseErrorPageOptions - { - ShowExceptionDetails = true, - ListMigrations = true - }; - public virtual bool ShowExceptionDetails { get; set; } - public virtual bool ListMigrations { get; set; } + public virtual bool EnableMigrationCommands { get; set; } + public virtual PathString MigrationsEndPointPath { get; set; } } } diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointExtensions.cs b/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointExtensions.cs new file mode 100644 index 00000000..0c40f827 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointExtensions.cs @@ -0,0 +1,31 @@ +// 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 JetBrains.Annotations; +using Microsoft.AspNet.Diagnostics.Entity; +using Microsoft.AspNet.Diagnostics.Entity.Utilities; + +namespace Microsoft.AspNet.Builder +{ + public static class MigrationsEndPointExtensions + { + public static IApplicationBuilder UseMigrationsEndPoint([NotNull] this IApplicationBuilder builder) + { + Check.NotNull(builder, "builder"); + + return builder.UseMigrationsEndPoint(options => { }); + } + + public static IApplicationBuilder UseMigrationsEndPoint([NotNull] this IApplicationBuilder builder, [NotNull] Action optionsAction) + { + Check.NotNull(builder, "builder"); + Check.NotNull(optionsAction, "optionsAction"); + + var options = new MigrationsEndPointOptions(); + optionsAction(options); + + return builder.UseMiddleware(options); + } + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointMiddleware.cs b/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointMiddleware.cs new file mode 100644 index 00000000..eefc1370 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointMiddleware.cs @@ -0,0 +1,121 @@ +// 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 JetBrains.Annotations; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Diagnostics.Entity.Utilities; +using Microsoft.AspNet.Http; +using Microsoft.Data.Entity; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNet.Diagnostics.Entity +{ + public class MigrationsEndPointMiddleware + { + private readonly RequestDelegate _next; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly MigrationsEndPointOptions _options; + + public MigrationsEndPointMiddleware( + [NotNull] RequestDelegate next, + [NotNull] IServiceProvider serviceProvider, + [NotNull] ILogger logger, + [NotNull] MigrationsEndPointOptions options) + { + Check.NotNull(next, "next"); + Check.NotNull(serviceProvider, "serviceProvider"); + Check.NotNull(logger, "logger"); + Check.NotNull(options, "options"); + + _next = next; + _serviceProvider = serviceProvider; + _logger = logger; + _options = options; + } + + public virtual async Task Invoke([NotNull] HttpContext context) + { + Check.NotNull(context, "context"); + + if (context.Request.Path.Equals(_options.Path)) + { + _logger.LogVerbose(Strings.FormatMigrationsEndPointMiddleware_RequestPathMatched(context.Request.Path)); + + var db = await GetDbContext(context, _logger); + if (db != null) + { + try + { + _logger.LogVerbose(Strings.FormatMigrationsEndPointMiddleware_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.LogVerbose(Strings.FormatMigrationsEndPointMiddleware_Applied(db.GetType().FullName)); + } + catch (Exception ex) + { + var message = Strings.FormatMigrationsEndPointMiddleware_Exception(db.GetType().FullName) + ex.ToString(); + _logger.LogError(message); + 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.LogError(Strings.MigrationsEndPointMiddleware_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.LogError(message); + 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.LogError(message); + 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/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointOptions.cs b/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointOptions.cs new file mode 100644 index 00000000..a2cf6b20 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointOptions.cs @@ -0,0 +1,14 @@ +// 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.AspNet.Http; + +namespace Microsoft.AspNet.Diagnostics.Entity +{ + public class MigrationsEndPointOptions + { + public static PathString DefaultPath = new PathString("/ApplyDatabaseMigrations"); + + public virtual PathString Path { get; set; } = DefaultPath; + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Properties/Strings.Designer.cs b/src/Microsoft.AspNet.Diagnostics.Entity/Properties/Strings.Designer.cs index 0d2a924f..7e45413e 100644 --- a/src/Microsoft.AspNet.Diagnostics.Entity/Properties/Strings.Designer.cs +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Properties/Strings.Designer.cs @@ -90,6 +90,70 @@ internal static string FormatDatabaseErrorPage_AddMigrationCommand() return GetString("DatabaseErrorPage_AddMigrationCommand"); } + /// + /// Apply Migrations + /// + internal static string DatabaseErrorPage_ApplyMigrationsButton + { + get { return GetString("DatabaseErrorPage_ApplyMigrationsButton"); } + } + + /// + /// Apply Migrations + /// + internal static string FormatDatabaseErrorPage_ApplyMigrationsButton() + { + return GetString("DatabaseErrorPage_ApplyMigrationsButton"); + } + + /// + /// Migrations Applied + /// + internal static string DatabaseErrorPage_ApplyMigrationsButtonDone + { + get { return GetString("DatabaseErrorPage_ApplyMigrationsButtonDone"); } + } + + /// + /// Migrations Applied + /// + internal static string FormatDatabaseErrorPage_ApplyMigrationsButtonDone() + { + return GetString("DatabaseErrorPage_ApplyMigrationsButtonDone"); + } + + /// + /// Applying Migrations... + /// + internal static string DatabaseErrorPage_ApplyMigrationsButtonRunning + { + get { return GetString("DatabaseErrorPage_ApplyMigrationsButtonRunning"); } + } + + /// + /// Applying Migrations... + /// + internal static string FormatDatabaseErrorPage_ApplyMigrationsButtonRunning() + { + return GetString("DatabaseErrorPage_ApplyMigrationsButtonRunning"); + } + + /// + /// An error occurred applying migrations, try applying them from the command line + /// + internal static string DatabaseErrorPage_ApplyMigrationsFailed + { + get { return GetString("DatabaseErrorPage_ApplyMigrationsFailed"); } + } + + /// + /// An error occurred applying migrations, try applying them from the command line + /// + internal static string FormatDatabaseErrorPage_ApplyMigrationsFailed() + { + return GetString("DatabaseErrorPage_ApplyMigrationsFailed"); + } + /// /// You can also apply migrations from the command line: /// @@ -106,6 +170,22 @@ internal static string FormatDatabaseErrorPage_HowToApplyFromCmd() return GetString("DatabaseErrorPage_HowToApplyFromCmd"); } + /// + /// Try refreshing the page + /// + internal static string DatabaseErrorPage_MigrationsAppliedRefresh + { + get { return GetString("DatabaseErrorPage_MigrationsAppliedRefresh"); } + } + + /// + /// Try refreshing the page + /// + internal static string FormatDatabaseErrorPage_MigrationsAppliedRefresh() + { + return GetString("DatabaseErrorPage_MigrationsAppliedRefresh"); + } + /// /// From the command line, scaffold a new migration and apply it to the database: /// @@ -234,6 +314,118 @@ internal static string FormatInvalidEnumValue(object argumentName, object enumTy return string.Format(CultureInfo.CurrentCulture, GetString("InvalidEnumValue", "argumentName", "enumType"), argumentName, enumType); } + /// + /// Migrations successfully applied for context '{0}'. + /// + internal static string MigrationsEndPointMiddleware_Applied + { + get { return GetString("MigrationsEndPointMiddleware_Applied"); } + } + + /// + /// Migrations successfully applied for context '{0}'. + /// + internal static string FormatMigrationsEndPointMiddleware_Applied(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MigrationsEndPointMiddleware_Applied"), p0); + } + + /// + /// Request is valid, applying migrations for context '{0}'. + /// + internal static string MigrationsEndPointMiddleware_ApplyingMigrations + { + get { return GetString("MigrationsEndPointMiddleware_ApplyingMigrations"); } + } + + /// + /// Request is valid, applying migrations for context '{0}'. + /// + internal static string FormatMigrationsEndPointMiddleware_ApplyingMigrations(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MigrationsEndPointMiddleware_ApplyingMigrations"), p0); + } + + /// + /// 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 { return 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) + { + return 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 { return GetString("MigrationsEndPointMiddleware_Exception"); } + } + + /// + /// An error occurred while applying the migrations for '{0}'. See InnerException for details. + /// + internal static string FormatMigrationsEndPointMiddleware_Exception(object p0) + { + return 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 { return 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) + { + return 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 { return 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() + { + return GetString("MigrationsEndPointMiddleware_NoContextType"); + } + + /// + /// Request path matched the path configured for this migrations endpoint ({0}). Attempting to process the migrations request. + /// + internal static string MigrationsEndPointMiddleware_RequestPathMatched + { + get { return GetString("MigrationsEndPointMiddleware_RequestPathMatched"); } + } + + /// + /// Request path matched the path configured for this migrations endpoint ({0}). Attempting to process the migrations request. + /// + internal static string FormatMigrationsEndPointMiddleware_RequestPathMatched(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MigrationsEndPointMiddleware_RequestPathMatched"), p0); + } + /// /// A database operation failed while processing the request. /// diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Strings.resx b/src/Microsoft.AspNet.Diagnostics.Entity/Strings.resx index 1965877e..36f32ac9 100644 --- a/src/Microsoft.AspNet.Diagnostics.Entity/Strings.resx +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Strings.resx @@ -132,9 +132,24 @@ > dnx ef migrations add [migration name] + + Apply Migrations + + + Migrations Applied + + + Applying Migrations... + + + An error occurred applying migrations, try applying them from the command line + You can also apply migrations from the command line: + + Try refreshing the page + From the command line, scaffold a new migration and apply it to the database: @@ -159,6 +174,27 @@ The value provided for argument '{argumentName}' must be a valid value of enum type '{enumType}'. + + Migrations successfully applied for context '{0}'. + + + Request is valid, applying migrations for context '{0}'. + + + 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. + + + Request path matched the path configured for this migrations endpoint ({0}). Attempting to process the migrations request. + A database operation failed while processing the request. diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cs b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cs index e9c5b18d..a38a6435 100644 --- a/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cs +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cs @@ -13,22 +13,12 @@ namespace Microsoft.AspNet.Diagnostics.Entity.Views #line hidden ; #line 3 "DatabaseErrorPage.cshtml" -using JetBrains.Annotations; - -#line default -#line hidden -#line 4 "DatabaseErrorPage.cshtml" using Microsoft.AspNet.Diagnostics.Entity #line default #line hidden ; -#line 5 "DatabaseErrorPage.cshtml" -using Microsoft.AspNet.Diagnostics.Entity.Utilities; - -#line default -#line hidden -#line 6 "DatabaseErrorPage.cshtml" +#line 4 "DatabaseErrorPage.cshtml" using Microsoft.AspNet.Diagnostics.Entity.Views #line default @@ -38,20 +28,18 @@ namespace Microsoft.AspNet.Diagnostics.Entity.Views public class DatabaseErrorPage : Microsoft.AspNet.Diagnostics.Views.BaseView { -#line 14 "DatabaseErrorPage.cshtml" +#line 11 "DatabaseErrorPage.cshtml" - private DatabaseErrorPageModel _model; + public DatabaseErrorPageModel Model { get; set; } - public virtual DatabaseErrorPageModel Model + public string UrlEncode(string content) { - get { return _model; } - [param: NotNull] - set - { - Check.NotNull(value, "value"); + return UrlEncoder.UrlEncode(content); + } - _model = value; - } + public string JavaScriptEncode(string content) + { + return JavaScriptStringEncoder.JavaScriptStringEncode(content); } #line default @@ -64,10 +52,9 @@ public DatabaseErrorPage() #pragma warning disable 1998 public override async Task ExecuteAsync() { -#line 7 "DatabaseErrorPage.cshtml" +#line 5 "DatabaseErrorPage.cshtml" Response.StatusCode = 500; - // TODO: Response.ReasonPhrase = "Internal Server Error"; Response.ContentType = "text/html"; Response.ContentLength = null; // Clear any prior Content-Length @@ -76,26 +63,21 @@ public override async Task ExecuteAsync() WriteLiteral("\r\n\r\n\r\n\r" + "\n \r\n Internal Server Error\r\n \r\n body {\r\n font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif;\r\n font-size: .813em;\r\n line-height: 1.4em;\r\n color: #222;\r\n}\r\n\r\nh1, h2, h3, h4, h5 {\r\n font-weight: 100;\r\n}\r\n\r\nh1 {\r\n color: #44525e;\r\n margin: 15px 0 15px 0;\r\n}\r\n\r\nh2 {\r\n margin: 10px 5px 0 0;\r\n}\r\n\r\nh3 {\r\n color: #363636;\r\n margin: 5px 5px 0 0;\r\n}\r\n\r\ncode {\r\n font-family: Consolas, \"Courier New\", courier, monospace;\r\n}\r\n\r\na {\r\n color: #1ba1e2;\r\n text-decoration: none;\r\n}\r\n\r\n a:hover {\r\n color: #13709e;\r\n text-decoration: underline;\r\n }\r\n\r\nhr {\r\n border: 1px #ddd solid;\r\n}\r\n\r\nbody .titleerror {\r\n padding: 3px;\r\n}\r\n\r\n#applyMigrations {\r\n font-size: 14px;\r\n background: #44c5f2;\r\n color: #ffffff;\r\n display: inline-block;\r\n padding: 6px 12px;\r\n margin-bottom: 0;\r\n font-weight: normal;\r\n text-align: center;\r\n white-space: nowrap;\r\n vertical-align: middle;\r\n cursor: pointer;\r\n border: 1px solid transparent;\r\n}\r\n\r\n #applyMigrations:disabled {\r\n background-color: #a9e4f9;\r\n border-color: #44c5f2;\r\n }\r\n\r\n.error {\r\n color: red;\r\n}\r\n\r\n.expanded {\r\n display: block;\r\n}\r\n\r\n.collapsed {\r\n display: none;\r\n}\r\n\r\n "); -#line 37 "DatabaseErrorPage.cshtml" - Write(string.Empty); - -#line default -#line hidden - WriteLiteral("\r\n \r\n\r\n\r\n

"); -#line 41 "DatabaseErrorPage.cshtml" +"yle>\r\n body {\r\n font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif;\r\n font-size: .813em;\r\n line-height: 1.4em;\r\n color: #222;\r\n}\r\n\r\nh1, h2, h3, h4, h5 {\r\n font-weight: 100;\r\n}\r\n\r\nh1 {\r\n color: #44525e;\r\n margin: 15px 0 15px 0;\r\n}\r\n\r\nh2 {\r\n margin: 10px 5px 0 0;\r\n}\r\n\r\nh3 {\r\n color: #363636;\r\n margin: 5px 5px 0 0;\r\n}\r\n\r\ncode {\r\n font-family: Consolas, \"Courier New\", courier, monospace;\r\n}\r\n\r\na {\r\n color: #1ba1e2;\r\n text-decoration: none;\r\n}\r\n\r\n a:hover {\r\n color: #13709e;\r\n text-decoration: underline;\r\n }\r\n\r\nhr {\r\n border: 1px #ddd solid;\r\n}\r\n\r\nbody .titleerror {\r\n padding: 3px;\r\n}\r\n\r\n#applyMigrations {\r\n font-size: 14px;\r\n background: #44c5f2;\r\n color: #ffffff;\r\n display: inline-block;\r\n padding: 6px 12px;\r\n margin-bottom: 0;\r\n font-weight: normal;\r\n text-align: center;\r\n white-space: nowrap;\r\n vertical-align: middle;\r\n cursor: pointer;\r\n border: 1px solid transparent;\r\n}\r\n\r\n #applyMigrations:disabled {\r\n background-color: #a9e4f9;\r\n border-color: #44c5f2;\r\n }\r\n\r\n.error {\r\n color: red;\r\n}\r\n\r\n.expanded {\r\n display: block;\r\n}\r\n\r\n.collapsed {\r\n display: none;\r\n}\r\n\r\n \r\n\r\n\r\n" + +"

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

\r\n"); -#line 42 "DatabaseErrorPage.cshtml" +#line 36 "DatabaseErrorPage.cshtml" #line default #line hidden -#line 42 "DatabaseErrorPage.cshtml" +#line 36 "DatabaseErrorPage.cshtml" if (Model.Options.ShowExceptionDetails) { @@ -103,13 +85,13 @@ public override async Task ExecuteAsync() #line hidden WriteLiteral("

\r\n"); -#line 45 "DatabaseErrorPage.cshtml" +#line 39 "DatabaseErrorPage.cshtml" #line default #line hidden -#line 45 "DatabaseErrorPage.cshtml" +#line 39 "DatabaseErrorPage.cshtml" for (Exception ex = Model.Exception; ex != null; ex = ex.InnerException) { @@ -117,39 +99,39 @@ public override async Task ExecuteAsync() #line hidden WriteLiteral(" "); -#line 47 "DatabaseErrorPage.cshtml" +#line 41 "DatabaseErrorPage.cshtml" Write(ex.GetType().Name); #line default #line hidden WriteLiteral(": "); -#line 47 "DatabaseErrorPage.cshtml" +#line 41 "DatabaseErrorPage.cshtml" Write(ex.Message); #line default #line hidden WriteLiteral("\r\n
\r\n"); -#line 49 "DatabaseErrorPage.cshtml" +#line 43 "DatabaseErrorPage.cshtml" } #line default #line hidden WriteLiteral("

\r\n
\r\n"); -#line 52 "DatabaseErrorPage.cshtml" +#line 46 "DatabaseErrorPage.cshtml" } #line default #line hidden WriteLiteral("\r\n"); -#line 54 "DatabaseErrorPage.cshtml" +#line 48 "DatabaseErrorPage.cshtml" #line default #line hidden -#line 54 "DatabaseErrorPage.cshtml" +#line 48 "DatabaseErrorPage.cshtml" if (!Model.DatabaseExists && !Model.PendingMigrations.Any()) { @@ -157,31 +139,31 @@ public override async Task ExecuteAsync() #line hidden WriteLiteral("

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

\r\n

"); -#line 57 "DatabaseErrorPage.cshtml" +#line 51 "DatabaseErrorPage.cshtml" Write(Strings.DatabaseErrorPage_NoDbOrMigrationsInfo); #line default #line hidden WriteLiteral("

\r\n "); -#line 58 "DatabaseErrorPage.cshtml" +#line 52 "DatabaseErrorPage.cshtml" Write(Strings.DatabaseErrorPage_AddMigrationCommand); #line default #line hidden WriteLiteral(" \r\n
\r\n "); -#line 60 "DatabaseErrorPage.cshtml" +#line 54 "DatabaseErrorPage.cshtml" Write(Strings.DatabaseErrorPage_ApplyMigrationsCommand); #line default #line hidden WriteLiteral(" \r\n
\r\n"); -#line 62 "DatabaseErrorPage.cshtml" +#line 56 "DatabaseErrorPage.cshtml" } else if (Model.PendingMigrations.Any()) { @@ -190,25 +172,25 @@ public override async Task ExecuteAsync() #line hidden WriteLiteral("
\r\n

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

\r\n

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

\r\n\r\n"); -#line 69 "DatabaseErrorPage.cshtml" +#line 63 "DatabaseErrorPage.cshtml" #line default #line hidden -#line 69 "DatabaseErrorPage.cshtml" +#line 63 "DatabaseErrorPage.cshtml" if (Model.Options.ListMigrations) { @@ -216,13 +198,13 @@ public override async Task ExecuteAsync() #line hidden WriteLiteral("
    \r\n"); -#line 72 "DatabaseErrorPage.cshtml" +#line 66 "DatabaseErrorPage.cshtml" #line default #line hidden -#line 72 "DatabaseErrorPage.cshtml" +#line 66 "DatabaseErrorPage.cshtml" foreach (var migration in Model.PendingMigrations) { @@ -230,39 +212,138 @@ public override async Task ExecuteAsync() #line hidden WriteLiteral("
  • "); -#line 74 "DatabaseErrorPage.cshtml" +#line 68 "DatabaseErrorPage.cshtml" Write(migration); #line default #line hidden WriteLiteral("
  • \r\n"); -#line 75 "DatabaseErrorPage.cshtml" +#line 69 "DatabaseErrorPage.cshtml" } #line default #line hidden WriteLiteral("
\r\n"); -#line 77 "DatabaseErrorPage.cshtml" +#line 71 "DatabaseErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral("\r\n"); +#line 73 "DatabaseErrorPage.cshtml" + + +#line default +#line hidden + +#line 73 "DatabaseErrorPage.cshtml" + if (Model.Options.EnableMigrationCommands) + { + +#line default +#line hidden + + WriteLiteral("

\r\n + + +

+ \r\n"); +#line 115 "DatabaseErrorPage.cshtml" } #line default #line hidden WriteLiteral("\r\n

"); -#line 79 "DatabaseErrorPage.cshtml" +#line 117 "DatabaseErrorPage.cshtml" Write(Strings.DatabaseErrorPage_HowToApplyFromCmd); #line default #line hidden WriteLiteral("

\r\n "); -#line 80 "DatabaseErrorPage.cshtml" +#line 118 "DatabaseErrorPage.cshtml" Write(Strings.DatabaseErrorPage_ApplyMigrationsCommand); #line default #line hidden WriteLiteral("\r\n
\r\n
\r\n"); -#line 83 "DatabaseErrorPage.cshtml" +#line 121 "DatabaseErrorPage.cshtml" } else if (Model.PendingModelChanges) { @@ -271,31 +352,31 @@ public override async Task ExecuteAsync() #line hidden WriteLiteral("
\r\n

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

\r\n

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

\r\n "); -#line 89 "DatabaseErrorPage.cshtml" +#line 127 "DatabaseErrorPage.cshtml" Write(Strings.DatabaseErrorPage_AddMigrationCommand); #line default #line hidden WriteLiteral("\r\n
\r\n "); -#line 91 "DatabaseErrorPage.cshtml" +#line 129 "DatabaseErrorPage.cshtml" Write(Strings.DatabaseErrorPage_ApplyMigrationsCommand); #line default #line hidden WriteLiteral("\r\n
\r\n
\r\n"); -#line 94 "DatabaseErrorPage.cshtml" +#line 132 "DatabaseErrorPage.cshtml" } #line default diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cshtml b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cshtml index 31774799..ccfe5927 100644 --- a/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cshtml +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cshtml @@ -1,29 +1,24 @@ @using System @using System.Linq -@using JetBrains.Annotations; @using Microsoft.AspNet.Diagnostics.Entity -@using Microsoft.AspNet.Diagnostics.Entity.Utilities; @using Microsoft.AspNet.Diagnostics.Entity.Views @{ Response.StatusCode = 500; - // TODO: Response.ReasonPhrase = "Internal Server Error"; Response.ContentType = "text/html"; Response.ContentLength = null; // Clear any prior Content-Length } @functions { - private DatabaseErrorPageModel _model; + public DatabaseErrorPageModel Model { get; set; } - public virtual DatabaseErrorPageModel Model + public string UrlEncode(string content) { - get { return _model; } - [param: NotNull] - set - { - Check.NotNull(value, "value"); + return UrlEncoder.UrlEncode(content); + } - _model = value; - } + public string JavaScriptEncode(string content) + { + return JavaScriptStringEncoder.JavaScriptStringEncode(content); } } @@ -34,7 +29,6 @@ Internal Server Error @@ -76,6 +70,50 @@ } + @if (Model.Options.EnableMigrationCommands) + { +

+ + + +

+ + } +

@Strings.DatabaseErrorPage_HowToApplyFromCmd

@Strings.DatabaseErrorPage_ApplyMigrationsCommand
diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPageModel.cs b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPageModel.cs index 6836fc2a..983ccda7 100644 --- a/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPageModel.cs +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPageModel.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using JetBrains.Annotations; +using Microsoft.AspNet.Builder; using Microsoft.AspNet.Diagnostics.Entity.Utilities; using System; using System.Collections.Generic; diff --git a/src/Microsoft.AspNet.Diagnostics/Views/BaseView.cs b/src/Microsoft.AspNet.Diagnostics/Views/BaseView.cs index cba88ca0..1d8010aa 100644 --- a/src/Microsoft.AspNet.Diagnostics/Views/BaseView.cs +++ b/src/Microsoft.AspNet.Diagnostics/Views/BaseView.cs @@ -45,6 +45,16 @@ public abstract class BaseView /// protected IHtmlEncoder HtmlEncoder { get; set; } + /// + /// Url encoder used to encode content. + /// + protected IUrlEncoder UrlEncoder { get; set; } + + /// + /// JavaScript encoder used to encode content. + /// + protected IJavaScriptStringEncoder JavaScriptStringEncoder { get; set; } + /// /// Execute an individual request /// @@ -56,6 +66,8 @@ public async Task ExecuteAsync(HttpContext context) Response = Context.Response; Output = new StreamWriter(Response.Body, Encoding.UTF8, 4096, leaveOpen: true); HtmlEncoder = context.ApplicationServices.GetHtmlEncoder(); + UrlEncoder = context.ApplicationServices.GetUrlEncoder(); + JavaScriptStringEncoder = context.ApplicationServices.GetJavaScriptStringEncoder(); await ExecuteAsync(); Output.Dispose(); } diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/DatabaseErrorPageMiddlewareTest.cs b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/DatabaseErrorPageMiddlewareTest.cs index 1caded83..2cd385ee 100644 --- a/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/DatabaseErrorPageMiddlewareTest.cs +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/DatabaseErrorPageMiddlewareTest.cs @@ -2,6 +2,7 @@ // 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.Linq; using System.Net; @@ -17,6 +18,7 @@ using Microsoft.Data.Entity.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.WebEncoders; using Xunit; namespace Microsoft.AspNet.Diagnostics.Entity.Tests @@ -121,12 +123,10 @@ public NoMigrationsMiddleware(RequestDelegate next) public virtual Task Invoke(HttpContext context) { - using (var db = context.ApplicationServices.GetService()) - { - db.Blogs.Add(new Blog()); - db.SaveChanges(); - throw new Exception("SaveChanges should have thrown"); - } + var db = context.ApplicationServices.GetService(); + db.Blogs.Add(new Blog()); + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); } } @@ -155,12 +155,10 @@ public PendingMigrationsMiddleware(RequestDelegate next) public virtual Task Invoke(HttpContext context) { - using (var db = context.ApplicationServices.GetService()) - { - db.Blogs.Add(new Blog()); - db.SaveChanges(); - throw new Exception("SaveChanges should have thrown"); - } + var db = context.ApplicationServices.GetService(); + db.Blogs.Add(new Blog()); + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); } } @@ -186,19 +184,104 @@ public PendingModelChangesMiddleware(RequestDelegate next) public virtual Task Invoke(HttpContext context) { - using (var db = context.ApplicationServices.GetService()) - { + var db = context.ApplicationServices.GetService(); db.Database.Migrate(); db.Blogs.Add(new Blog()); db.SaveChanges(); throw new Exception("SaveChanges should have thrown"); - } } } [ConditionalTheory] [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + public async Task Error_page_then_apply_migrations() + { + TestServer server = SetupTestServer(); + 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 = new JavaScriptStringEncoder(); + 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.ApplicationServices.GetService(); + db.Blogs.Add(new Blog()); + db.SaveChanges(); + await context.Response.WriteAsync("Saved a Blog"); + } + } + + [ConditionalTheory] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + public async Task Customize_migrations_end_point() + { + var migrationsEndpoint = "/MyCustomEndPoints/ApplyMyMigrationsHere"; + + using (var database = SqlServerTestStore.CreateScratch()) + { + var server = TestServer.Create(app => + { + app.UseDatabaseErrorPage(options => + { + options.EnableAll(); + options.MigrationsEndPointPath = new PathString(migrationsEndpoint); + }); + + app.UseMiddleware(); + }, + services => + { + services.AddEntityFramework().AddSqlServer(); + services.AddScoped(); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer(database.ConnectionString); + services.AddInstance(optionsBuilder.Options); + }); + + 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); + } + } + + [Fact] public async Task Pass_thru_when_context_not_in_services() { using (var database = SqlServerTestStore.CreateScratch()) @@ -242,12 +325,10 @@ public ContextNotRegisteredInServicesMiddleware(RequestDelegate next) public virtual Task Invoke(HttpContext context) { var options = context.ApplicationServices.GetService(); - using (var db = new BloggingContext(context.ApplicationServices, options)) - { - db.Blogs.Add(new Blog()); - db.SaveChanges(); - throw new Exception("SaveChanges should have thrown"); - } + var db = new BloggingContext(context.ApplicationServices, options); + db.Blogs.Add(new Blog()); + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); } } @@ -276,12 +357,10 @@ public ExceptionInLogicMiddleware(RequestDelegate next) public virtual Task Invoke(HttpContext context) { - using (var db = context.ApplicationServices.GetService()) - { - db.Blogs.Add(new Blog()); - db.SaveChanges(); - throw new Exception("SaveChanges should have thrown"); - } + var db = context.ApplicationServices.GetService(); + db.Blogs.Add(new Blog()); + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); } } @@ -305,18 +384,16 @@ public WrappedExceptionMiddleware(RequestDelegate next) public virtual Task Invoke(HttpContext context) { - using (var db = context.ApplicationServices.GetService()) + var db = context.ApplicationServices.GetService(); + db.Blogs.Add(new Blog()); + try { - 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); - } + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); + } + catch (Exception ex) + { + throw new Exception("I wrapped your exception", ex); } } } @@ -357,5 +434,17 @@ private static TestServer SetupTestServer(ILoggerProvider }); } } + + private static UrlEncoder _urlEncoder = new UrlEncoder(); + private static string UrlEncode(string content) + { + return _urlEncoder.UrlEncode(content); + } + + private static JavaScriptStringEncoder _javaScriptEncoder = new JavaScriptStringEncoder(); + private static string JavaScriptEncode(string content) + { + return _javaScriptEncoder.JavaScriptStringEncode(content); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/MigrationsEndPointMiddlewareTest.cs b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/MigrationsEndPointMiddlewareTest.cs new file mode 100644 index 00000000..b0fe5884 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/MigrationsEndPointMiddlewareTest.cs @@ -0,0 +1,209 @@ +// 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.AspNet.Builder; +using Microsoft.AspNet.Diagnostics.Entity.Tests.Helpers; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.TestHost; +using Microsoft.AspNet.Testing.xunit; +using Microsoft.Data.Entity; +using Microsoft.Data.Entity.Infrastructure; +using Microsoft.Data.Entity.Migrations; +using Microsoft.Data.Entity.Storage; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNet.Diagnostics.Entity.Tests +{ + public class MigrationsEndPointMiddlewareTest + { + [Fact] + public async Task Non_migration_requests_pass_thru() + { + TestServer server = TestServer.Create(app => app + .UseMigrationsEndPoint() + .UseMiddleware()); + + 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) + { + await context.Response.WriteAsync("Request Handled"); + context.Response.StatusCode = (int)HttpStatusCode.OK; + } + } + + [ConditionalTheory] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + public async Task Migration_request_default_path() + { + await Migration_request(useCustomPath: false); + } + + [ConditionalTheory] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + 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; + + TestServer server = TestServer.Create(app => + { + if (useCustomPath) + { + app.UseMigrationsEndPoint(o => o.Path = path); + } + else + { + app.UseMigrationsEndPoint(); + } + }, + services => + { + services.AddEntityFramework().AddSqlServer(); + services.AddScoped(); + services.AddInstance(optionsBuilder.Options); + }); + + using (var db = BloggingContextWithMigrations.CreateWithoutExternalServiceProvider(optionsBuilder.Options)) + { + var databaseCreator = ((IAccessor)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 = ((IAccessor)db).Service.GetRequiredService(); + 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 server = TestServer.Create(app => + { + app.UseMigrationsEndPoint(); + }); + + 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 server = TestServer.Create(app => + { + app.UseMigrationsEndPoint(); + }); + + 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 server = TestServer.Create( + app => app.UseMigrationsEndPoint(), + services => services.AddEntityFramework().AddSqlServer()); + + 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); + } + + [ConditionalTheory] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + public async Task Exception_while_applying_migrations() + { + using (var database = SqlServerTestStore.CreateScratch()) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlServer(database.ConnectionString); + + TestServer server = TestServer.Create( + app => app.UseMigrationsEndPoint(), + services => + { + services.AddEntityFramework().AddSqlServer(); + services.AddScoped(); + services.AddInstance(optionsBuilder.Options); + }); + + 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/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageOptionsTest.cs b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageOptionsTest.cs index fcb371c3..e604a141 100644 --- a/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageOptionsTest.cs +++ b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageOptionsTest.cs @@ -1,6 +1,7 @@ // 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.AspNet.Builder; using Xunit; namespace Microsoft.AspNet.Diagnostics.Entity.Tests @@ -8,39 +9,78 @@ namespace Microsoft.AspNet.Diagnostics.Entity.Tests public class DatabaseErrorPageOptionsTest { [Fact] - public void Default_visibility_is_false() + public void Everything_disabled_by_default() { var options = new DatabaseErrorPageOptions(); Assert.False(options.ShowExceptionDetails); Assert.False(options.ListMigrations); + Assert.False(options.EnableMigrationCommands); + Assert.Equal(string.Empty, options.MigrationsEndPointPath); } [Fact] - public void ShowAll_shows_all_errors() + public void EnableAll_enables_everything() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); Assert.True(options.ShowExceptionDetails); Assert.True(options.ListMigrations); + Assert.True(options.EnableMigrationCommands); + Assert.Equal(MigrationsEndPointOptions.DefaultPath, options.MigrationsEndPointPath); } [Fact] public void ShowExceptionDetails_is_respected() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); options.ShowExceptionDetails = false; Assert.False(options.ShowExceptionDetails); + Assert.True(options.ListMigrations); + Assert.True(options.EnableMigrationCommands); + Assert.Equal(MigrationsEndPointOptions.DefaultPath, options.MigrationsEndPointPath); } [Fact] public void ListMigrations_is_respected() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); options.ListMigrations = false; + Assert.True(options.ShowExceptionDetails); Assert.False(options.ListMigrations); + Assert.True(options.EnableMigrationCommands); + Assert.Equal(MigrationsEndPointOptions.DefaultPath, options.MigrationsEndPointPath); + } + + [Fact] + public void EnableMigrationCommands_is_respected() + { + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); + options.EnableMigrationCommands = false; + + Assert.True(options.ShowExceptionDetails); + Assert.True(options.ListMigrations); + Assert.False(options.EnableMigrationCommands); + Assert.Equal(MigrationsEndPointOptions.DefaultPath, options.MigrationsEndPointPath); + } + + [Fact] + public void MigrationsEndPointPath_is_respected() + { + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); + options.MigrationsEndPointPath = "/test"; + + Assert.True(options.ShowExceptionDetails); + Assert.True(options.ListMigrations); + Assert.True(options.EnableMigrationCommands); + Assert.Equal("/test", options.MigrationsEndPointPath); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageTest.cs b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageTest.cs index e2a454a8..52fde88d 100644 --- a/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageTest.cs +++ b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageTest.cs @@ -1,6 +1,7 @@ // 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.AspNet.Builder; using Microsoft.AspNet.Diagnostics.Entity.Tests.Helpers; using Microsoft.AspNet.Diagnostics.Entity.Views; using Microsoft.AspNet.Http; @@ -19,7 +20,8 @@ public class DatabaseErrorPageTest [Fact] public async Task No_database_or_migrations_only_displays_scaffold_first_migration() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); var model = new DatabaseErrorPageModel( contextType: typeof(BloggingContext), @@ -39,7 +41,8 @@ public async Task No_database_or_migrations_only_displays_scaffold_first_migrati [Fact] public async Task No_database_with_migrations_only_displays_apply_migrations() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); var model = new DatabaseErrorPageModel( contextType: typeof(BloggingContext), @@ -59,7 +62,8 @@ public async Task No_database_with_migrations_only_displays_apply_migrations() [Fact] public async Task Existing_database_with_migrations_only_displays_apply_migrations() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); var model = new DatabaseErrorPageModel( contextType: typeof(BloggingContext), @@ -79,7 +83,8 @@ public async Task Existing_database_with_migrations_only_displays_apply_migratio [Fact] public async Task Existing_database_with_migrations_and_pending_model_changes_only_displays_apply_migrations() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); var model = new DatabaseErrorPageModel( contextType: typeof(BloggingContext), @@ -99,7 +104,8 @@ public async Task Existing_database_with_migrations_and_pending_model_changes_on [Fact] public async Task Pending_model_changes_only_displays_scaffold_next_migration() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); var model = new DatabaseErrorPageModel( contextType: typeof(BloggingContext), @@ -119,7 +125,8 @@ public async Task Pending_model_changes_only_displays_scaffold_next_migration() [Fact] public async Task Exception_details_are_displayed() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); var model = new DatabaseErrorPageModel( contextType: typeof(BloggingContext), @@ -137,7 +144,8 @@ public async Task Exception_details_are_displayed() [Fact] public async Task Inner_exception_details_are_displayed() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); var model = new DatabaseErrorPageModel( contextType: typeof(BloggingContext), @@ -156,7 +164,8 @@ public async Task Inner_exception_details_are_displayed() [Fact] public async Task ShowExceptionDetails_is_respected() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); options.ShowExceptionDetails = false; var model = new DatabaseErrorPageModel( @@ -175,7 +184,8 @@ public async Task ShowExceptionDetails_is_respected() [Fact] public async Task ListMigrations_is_respected() { - var options = DatabaseErrorPageOptions.ShowAll; + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); options.ListMigrations = false; var model = new DatabaseErrorPageModel( @@ -191,6 +201,47 @@ public async Task ListMigrations_is_respected() Assert.DoesNotContain("111_MigrationOne", content); } + [Fact] + public async Task EnableMigrationCommands_is_respected() + { + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); + options.EnableMigrationCommands = false; + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception(), + databaseExists: true, + pendingModelChanges: false, + pendingMigrations: new string[] { "111_MigrationOne" }, + options: options); + + var content = await ExecutePage(options, model); + + Assert.DoesNotContain(options.MigrationsEndPointPath.Value, content); + } + + [Fact] + public async Task MigrationsEndPointPath_is_respected() + { + var options = new DatabaseErrorPageOptions(); + options.EnableAll(); + options.MigrationsEndPointPath = "/HitThisEndPoint"; + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception(), + databaseExists: true, + pendingModelChanges: false, + pendingMigrations: new string[] { "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();