diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/DataStoreErrorLogger.cs b/src/Microsoft.AspNet.Diagnostics.Entity/DataStoreErrorLogger.cs new file mode 100644 index 0000000000..32d710770a --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/DataStoreErrorLogger.cs @@ -0,0 +1,108 @@ +// 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 JetBrains.Annotations; +using Microsoft.AspNet.Diagnostics.Entity.Utilities; +using Microsoft.Data.Entity.Storage; +using Microsoft.Framework.Logging; +using System; +#if ASPNETCORE50 +using System.Threading; +#else +using System.Runtime.Remoting.Messaging; +#endif + +namespace Microsoft.AspNet.Diagnostics.Entity +{ + public class DataStoreErrorLogger : ILogger + { +#if ASPNETCORE50 + private readonly AsyncLocal _log = new AsyncLocal(); +#else + private const string ContextName = "__DataStoreErrorLog"; +#endif + + public virtual DataStoreErrorLog LastError + { + get + { +#if ASPNETCORE50 + return _log.Value; +#else + return (DataStoreErrorLog)CallContext.LogicalGetData(ContextName); +#endif + } + } + + public virtual void StartLoggingForCurrentCallContext() + { + // 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. +#if ASPNETCORE50 + _log.Value = new DataStoreErrorLog(); +#else + CallContext.LogicalSetData(ContextName, new DataStoreErrorLog()); +#endif + } + + public virtual void Write(LogLevel logLevel, int eventId, [CanBeNull] object state, [CanBeNull] Exception exception, [CanBeNull] Func formatter) + { + var errorState = state as DataStoreErrorLogState; + if (errorState != null && exception != null && LastError != null) + { + LastError.SetError(errorState.ContextType, exception); + } + } + + public virtual bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public virtual IDisposable BeginScope(object state) + { + return NullScope.Instance; + } + + private class NullScope : IDisposable + { + public static NullScope Instance = new NullScope(); + + public void Dispose() + { } + } + + public class DataStoreErrorLog + { + private Type _contextType; + private Exception _exception; + + public virtual void SetError([NotNull] Type contextType, [NotNull] Exception exception) + { + Check.NotNull(contextType, "contextType"); + Check.NotNull(exception, "exception"); + + _contextType = contextType; + _exception = exception; + } + + public virtual bool IsErrorLogged + { + get { return _exception != null; } + } + + public virtual Type ContextType + { + get { return _contextType; } + } + + public virtual Exception Exception + { + get { return _exception; } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/DataStoreErrorLoggerProvider.cs b/src/Microsoft.AspNet.Diagnostics.Entity/DataStoreErrorLoggerProvider.cs new file mode 100644 index 0000000000..d36f4a878c --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/DataStoreErrorLoggerProvider.cs @@ -0,0 +1,22 @@ +// 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.Framework.Logging; + +namespace Microsoft.AspNet.Diagnostics.Entity +{ + public class DataStoreErrorLoggerProvider : ILoggerProvider + { + private readonly DataStoreErrorLogger _logger = new DataStoreErrorLogger(); + + public virtual ILogger Create(string name) + { + return _logger; + } + + public virtual DataStoreErrorLogger Logger + { + get { return _logger; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageExtensions.cs b/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageExtensions.cs new file mode 100644 index 0000000000..3eac097cc8 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageExtensions.cs @@ -0,0 +1,34 @@ +// 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 JetBrains.Annotations; +using Microsoft.AspNet.Diagnostics.Entity; +using Microsoft.AspNet.Diagnostics.Entity.Utilities; + +namespace Microsoft.AspNet.Builder +{ + public static class DatabaseErrorPageExtensions + { + public static IApplicationBuilder UseDatabaseErrorPage([NotNull] this IApplicationBuilder builder) + { + Check.NotNull(builder, "builder"); + + return builder.UseDatabaseErrorPage(DatabaseErrorPageOptions.ShowAll); + } + + public static IApplicationBuilder UseDatabaseErrorPage([NotNull] this IApplicationBuilder builder, [NotNull] DatabaseErrorPageOptions options) + { + Check.NotNull(builder, "builder"); + Check.NotNull(options, "options"); + + builder = builder.UseMiddleware(options); + + if(options.EnableMigrationCommands) + { + builder.UseMigrationsEndPoint(new MigrationsEndPointOptions { Path = options.MigrationsEndPointPath }); + } + + return builder; + } + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageMiddleware.cs b/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageMiddleware.cs new file mode 100644 index 0000000000..bd4ddb6d09 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageMiddleware.cs @@ -0,0 +1,116 @@ +// 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 JetBrains.Annotations; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Diagnostics.Entity.Utilities; +using Microsoft.AspNet.Diagnostics.Entity.Views; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.RequestContainer; +using Microsoft.Data.Entity; +using Microsoft.Data.Entity.Migrations; +using Microsoft.Data.Entity.Migrations.Infrastructure; +using Microsoft.Data.Entity.Migrations.Utilities; +using Microsoft.Data.Entity.Relational; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Diagnostics.Entity +{ + public class DatabaseErrorPageMiddleware + { + private readonly RequestDelegate _next; + private readonly DatabaseErrorPageOptions _options; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly DataStoreErrorLoggerProvider _loggerProvider; + + public DatabaseErrorPageMiddleware([NotNull] RequestDelegate next, [NotNull] IServiceProvider serviceProvider, [NotNull] ILoggerFactory loggerFactory, [NotNull] DatabaseErrorPageOptions options) + { + Check.NotNull(next, "next"); + Check.NotNull(serviceProvider, "serviceProvider"); + Check.NotNull(loggerFactory, "loggerFactory"); + Check.NotNull(options, "options"); + + _next = next; + _serviceProvider = serviceProvider; + _options = options; + _logger = loggerFactory.Create(); + + _loggerProvider = new DataStoreErrorLoggerProvider(); + loggerFactory.AddProvider(_loggerProvider); + } + + public virtual async Task Invoke([NotNull] HttpContext context) + { + Check.NotNull(context, "context"); + + try + { +#if !ASPNETCORE50 + // TODO This probably isn't the correct place for this workaround, it + // needs to be called before anything is written to CallContext + // http://msdn.microsoft.com/en-us/library/dn458353(v=vs.110).aspx + System.Configuration.ConfigurationManager.GetSection("system.xml/xmlReader"); +#endif + _loggerProvider.Logger.StartLoggingForCurrentCallContext(); + + await _next(context).WithCurrentCulture(); + } + catch (Exception ex) + { + try + { + if (_loggerProvider.Logger.LastError.IsErrorLogged + && _loggerProvider.Logger.LastError.Exception == ex) + { + using (RequestServicesContainer.EnsureRequestServices(context, _serviceProvider)) + { + var dbContextType = _loggerProvider.Logger.LastError.ContextType; + var dbContext = (DbContext)context.RequestServices.GetService(dbContextType); + if (dbContext == null) + { + _logger.WriteError(Strings.DatabaseErrorPageMiddleware_ContextNotRegistered(dbContextType.FullName)); + } + else + { + if (dbContext.Database is RelationalDatabase) + { + var databaseExists = dbContext.Database.AsRelational().Exists(); + + var services = (MigrationsDataStoreServices)dbContext.Configuration.DataStoreServices; + + var pendingMigrations = services.Migrator.GetPendingMigrations().Select(m => m.GetMigrationId()); + + var pendingModelChanges = true; + var snapshot = services.Migrator.MigrationAssembly.Model; + if (snapshot != null) + { + pendingModelChanges = services.Migrator.ModelDiffer.Diff(snapshot, dbContext.Model).Any(); + } + + if ((!databaseExists && pendingMigrations.Any()) || pendingMigrations.Any() || pendingModelChanges) + { + var page = new DatabaseErrorPage(); + page.Model = new DatabaseErrorPageModel(dbContextType, ex, databaseExists, pendingModelChanges, pendingMigrations, _options); + await page.ExecuteAsync(context).WithCurrentCulture(); + return; + } + } + } + } + } + } + catch (Exception e) + { + _logger.WriteError(Strings.DatabaseErrorPageMiddleware_Exception, e); + } + + throw; + } + } + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageOptions.cs b/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageOptions.cs new file mode 100644 index 0000000000..72b785e297 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/DatabaseErrorPageOptions.cs @@ -0,0 +1,64 @@ +// 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.Diagnostics.Entity.Utilities; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Diagnostics.Entity +{ + public class DatabaseErrorPageOptions + { + private PathString _migrationsEndPointPath = MigrationsEndPointOptions.DefaultPath; + private bool _defaultVisibility; + private bool? _showExceptionDetails; + private bool? _listMigrations; + private bool? _enableMigrationCommands; + + public static DatabaseErrorPageOptions ShowAll + { + get + { + // We don't use a static instance because it's mutable. + return new DatabaseErrorPageOptions() + { + ShowExceptionDetails = true, + ListMigrations = true, + EnableMigrationCommands = true + }; + } + } + + public virtual PathString MigrationsEndPointPath + { + get { return _migrationsEndPointPath; } + set + { + Check.NotNull(value, "value"); + _migrationsEndPointPath = value; + } + } + + public virtual bool ShowExceptionDetails + { + get { return _showExceptionDetails ?? _defaultVisibility; } + set { _showExceptionDetails = value; } + } + + public virtual bool ListMigrations + { + get { return _listMigrations ?? _defaultVisibility; } + set { _listMigrations = value; } + } + + public virtual bool EnableMigrationCommands + { + get { return _enableMigrationCommands ?? _defaultVisibility; } + set { _enableMigrationCommands = value; } + } + + public virtual void SetDefaultVisibility(bool isVisible) + { + _defaultVisibility = isVisible; + } + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Microsoft.AspNet.Diagnostics.Entity.kproj b/src/Microsoft.AspNet.Diagnostics.Entity/Microsoft.AspNet.Diagnostics.Entity.kproj new file mode 100644 index 0000000000..f096c3b557 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Microsoft.AspNet.Diagnostics.Entity.kproj @@ -0,0 +1,14 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 4f5a6a72-ffe4-49c4-b4c6-58132cfcb9fe + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointExtensions.cs b/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointExtensions.cs new file mode 100644 index 0000000000..27629808fc --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointExtensions.cs @@ -0,0 +1,27 @@ +// 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 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(new MigrationsEndPointOptions()); + } + + public static IApplicationBuilder UseMigrationsEndPoint([NotNull] this IApplicationBuilder builder, [NotNull] MigrationsEndPointOptions options) + { + Check.NotNull(builder, "builder"); + Check.NotNull(options, "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 0000000000..475174ce9a --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointMiddleware.cs @@ -0,0 +1,123 @@ +// 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.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.Data.Entity.Migrations.Infrastructure; +using Microsoft.Framework.DependencyInjection; +using System.Net; +using Microsoft.Framework.Logging; +using Microsoft.AspNet.RequestContainer; + +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] ILoggerFactory loggerFactory, [NotNull] MigrationsEndPointOptions options) + { + Check.NotNull(next, "next"); + Check.NotNull(serviceProvider, "serviceProvider"); + Check.NotNull(loggerFactory, "loggerFactory"); + Check.NotNull(options, "options"); + + _next = next; + _serviceProvider = serviceProvider; + _logger = loggerFactory.Create(); + _options = options; + } + + public virtual async Task Invoke([NotNull] HttpContext context) + { + Check.NotNull(context, "context"); + + if (context.Request.Path.Equals(_options.Path)) + { + _logger.WriteVerbose(Strings.MigrationsEndPointMiddleware_RequestPathMatched(context.Request.Path)); + + using (RequestServicesContainer.EnsureRequestServices(context, _serviceProvider)) + { + var db = await GetDbContext(context, _logger).WithCurrentCulture(); + if (db != null) + { + try + { + _logger.WriteVerbose(Strings.MigrationsEndPointMiddleware_ApplyingMigrations(db.GetType().FullName)); + + db.Database.AsMigrationsEnabled().ApplyMigrations(); + + context.Response.StatusCode = (int)HttpStatusCode.NoContent; + context.Response.Headers.Add("Pragma", new[] { "no-cache" }); + context.Response.Headers.Add("Cache-Control", new[] { "no-cache" }); + + _logger.WriteVerbose(Strings.MigrationsEndPointMiddleware_Applied(db.GetType().FullName)); + } + catch (Exception ex) + { + var message = Strings.MigrationsEndPointMiddleware_Exception(db.GetType().FullName); + _logger.WriteError(message); + throw new InvalidOperationException(message, ex); + } + } + } + } + else + { + await _next(context).WithCurrentCulture(); + } + } + + private static async Task GetDbContext(HttpContext context, ILogger logger) + { + var form = await context.Request.GetFormAsync().WithCurrentCulture(); + var contextTypeName = form["context"]; + if (string.IsNullOrWhiteSpace(contextTypeName)) + { + logger.WriteError(Strings.MigrationsEndPointMiddleware_NoContextType); + await WriteErrorToResponse(context.Response, Strings.MigrationsEndPointMiddleware_NoContextType).WithCurrentCulture(); + return null; + } + + var contextType = Type.GetType(contextTypeName); + if (contextType == null) + { + var message = Strings.MigrationsEndPointMiddleware_InvalidContextType(contextTypeName); + logger.WriteError(message); + await WriteErrorToResponse(context.Response, message).WithCurrentCulture(); + return null; + } + + var db = (DbContext)context.RequestServices.GetService(contextType); + if (db == null) + { + var message = Strings.MigrationsEndPointMiddleware_ContextNotRegistered(contextType.FullName); + logger.WriteError(message); + await WriteErrorToResponse(context.Response, message).WithCurrentCulture(); + 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)).WithCurrentCulture(); + } + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointOptions.cs b/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointOptions.cs new file mode 100644 index 0000000000..01b8852119 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/MigrationsEndPointOptions.cs @@ -0,0 +1,24 @@ +// 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.Diagnostics.Entity.Utilities; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Diagnostics.Entity +{ + public class MigrationsEndPointOptions + { + public static PathString DefaultPath = new PathString("/ApplyDatabaseMigrations"); + private PathString _path = DefaultPath; + + public virtual PathString Path + { + get { return _path; } + set + { + Check.NotNull(value, "value"); + _path = value; + } + } + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Properties/Strings.Designer.cs b/src/Microsoft.AspNet.Diagnostics.Entity/Properties/Strings.Designer.cs new file mode 100644 index 0000000000..28d70ca160 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Properties/Strings.Designer.cs @@ -0,0 +1,256 @@ +// +namespace Microsoft.AspNet.Diagnostics.Entity +{ + using System.Diagnostics; + using System.Globalization; + using System.Reflection; + using System.Resources; + using JetBrains.Annotations; + + public static class Strings + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Diagnostics.Entity.Strings", typeof(Strings).GetTypeInfo().Assembly); + + /// + /// The string argument '{argumentName}' cannot be empty. + /// + public static string ArgumentIsEmpty([CanBeNull] object argumentName) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ArgumentIsEmpty", "argumentName"), argumentName); + } + + /// + /// The collection argument '{argumentName}' must contain at least one element. + /// + public static string CollectionArgumentIsEmpty([CanBeNull] object argumentName) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CollectionArgumentIsEmpty", "argumentName"), argumentName); + } + + /// + /// 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. Skipping display of the database error page. + /// + public static string DatabaseErrorPageMiddleware_ContextNotRegistered([CanBeNull] object p0) + { + return 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. + /// + public static string DatabaseErrorPageMiddleware_Exception + { + get { return GetString("DatabaseErrorPageMiddleware_Exception"); } + } + + /// + /// > k ef migration add [migration name] + /// + public static string DatabaseErrorPage_AddMigrationCommand + { + get { return GetString("DatabaseErrorPage_AddMigrationCommand"); } + } + + /// + /// Apply Migrations + /// + public static string DatabaseErrorPage_ApplyMigrationsButton + { + get { return GetString("DatabaseErrorPage_ApplyMigrationsButton"); } + } + + /// + /// Migrations Applied + /// + public static string DatabaseErrorPage_ApplyMigrationsButtonDone + { + get { return GetString("DatabaseErrorPage_ApplyMigrationsButtonDone"); } + } + + /// + /// Applying Migrations... + /// + public static string DatabaseErrorPage_ApplyMigrationsButtonRunning + { + get { return GetString("DatabaseErrorPage_ApplyMigrationsButtonRunning"); } + } + + /// + /// An error occurred applying migrations, try applying them from the command line + /// + public static string DatabaseErrorPage_ApplyMigrationsFailed + { + get { return GetString("DatabaseErrorPage_ApplyMigrationsFailed"); } + } + + /// + /// You can also apply migrations from the command line: + /// + public static string DatabaseErrorPage_HowToApplyFromCmd + { + get { return GetString("DatabaseErrorPage_HowToApplyFromCmd"); } + } + + /// + /// Try refreshing the page + /// + public static string DatabaseErrorPage_MigrationsAppliedRefresh + { + get { return GetString("DatabaseErrorPage_MigrationsAppliedRefresh"); } + } + + /// + /// From the command line, scaffold a new migration and apply it to the database: + /// + public static string DatabaseErrorPage_NoDbOrMigrationsInfo + { + get { return GetString("DatabaseErrorPage_NoDbOrMigrationsInfo"); } + } + + /// + /// Use migrations to create the database for {0} + /// + public static string DatabaseErrorPage_NoDbOrMigrationsTitle([CanBeNull] object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DatabaseErrorPage_NoDbOrMigrationsTitle"), p0); + } + + /// + /// Scaffold a new migration for these changes and apply them to the database from the command line: + /// + public static string DatabaseErrorPage_PendingChangesInfo + { + get { return GetString("DatabaseErrorPage_PendingChangesInfo"); } + } + + /// + /// There are pending model changes for {0} + /// + public static string DatabaseErrorPage_PendingChangesTitle([CanBeNull] object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DatabaseErrorPage_PendingChangesTitle"), p0); + } + + /// + /// There are migrations for {0} that have not been applied to the database + /// + public static string DatabaseErrorPage_PendingMigrationsInfo([CanBeNull] object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DatabaseErrorPage_PendingMigrationsInfo"), p0); + } + + /// + /// Applying existing migrations for {0} may resolve this issue + /// + public static string DatabaseErrorPage_PendingMigrationsTitle([CanBeNull] object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DatabaseErrorPage_PendingMigrationsTitle"), p0); + } + + /// + /// > k ef migration apply + /// + public static string DatabaseErrorPage_ApplyMigrationsCommand + { + get { return GetString("DatabaseErrorPage_ApplyMigrationsCommand"); } + } + + /// + /// The value provided for argument '{argumentName}' must be a valid value of enum type '{enumType}'. + /// + public static string InvalidEnumValue([CanBeNull] object argumentName, [CanBeNull] object enumType) + { + return string.Format(CultureInfo.CurrentCulture, GetString("InvalidEnumValue", "argumentName", "enumType"), argumentName, enumType); + } + + /// + /// Migrations successfully applied for context '{0}'. + /// + public static string MigrationsEndPointMiddleware_Applied([CanBeNull] object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MigrationsEndPointMiddleware_Applied"), p0); + } + + /// + /// Request is valid, applying migrations for context '{0}'. + /// + public static string MigrationsEndPointMiddleware_ApplyingMigrations([CanBeNull] 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. + /// + public static string MigrationsEndPointMiddleware_ContextNotRegistered([CanBeNull] object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MigrationsEndPointMiddleware_ContextNotRegistered"), p0); + } + + /// + /// An error occurred while applying the migrations for '{0}'. See InnerException for details. + /// + public static string MigrationsEndPointMiddleware_Exception([CanBeNull] 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. + /// + public static string MigrationsEndPointMiddleware_InvalidContextType([CanBeNull] 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. + /// + public static string MigrationsEndPointMiddleware_NoContextType + { + get { return GetString("MigrationsEndPointMiddleware_NoContextType"); } + } + + /// + /// Request path matched the path configured for this migrations endpoint ({0}). Attempting to process the migrations request. + /// + public static string MigrationsEndPointMiddleware_RequestPathMatched([CanBeNull] object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MigrationsEndPointMiddleware_RequestPathMatched"), p0); + } + + /// + /// A database operation failed while processing the request. + /// + public static string DatabaseErrorPage_Title + { + get { return GetString("DatabaseErrorPage_Title"); } + } + + /// + /// To use migrations from a command prompt you will need to <a href='http://go.microsoft.com/fwlink/?LinkId=518242'>install K Version Manager (KVM)</a>. Once installed, you can run migration commands from a standard command prompt in the project directory. + /// + public static string DatabaseErrorPage_EnableMigrationsCommandsInfo + { + get { return GetString("DatabaseErrorPage_EnableMigrationsCommandsInfo"); } + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + 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/Microsoft.AspNet.Diagnostics.Entity/Properties/Strings.resx b/src/Microsoft.AspNet.Diagnostics.Entity/Properties/Strings.resx new file mode 100644 index 0000000000..12b4868cf5 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Properties/Strings.resx @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 string argument '{argumentName}' cannot be empty. + + + The collection argument '{argumentName}' must contain at least one element. + + + 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. Skipping display of the database error page. + + + An exception occurred while calculating the database error page content. Skipping display of the database error page. + + + > k ef migration 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: + + + Use migrations to create the database for {0} + + + Scaffold a new migration for these changes and apply them to the database from the command line: + + + 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 + + + > k ef migration apply + + + 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. + + + To use migrations from a command prompt you will need to <a href='http://go.microsoft.com/fwlink/?LinkId=518242'>install K Version Manager (KVM)</a>. Once installed, you can run migration commands from a standard command prompt in the project directory. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Utilities/Check.cs b/src/Microsoft.AspNet.Diagnostics.Entity/Utilities/Check.cs new file mode 100644 index 0000000000..b0fc36be5c --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Utilities/Check.cs @@ -0,0 +1,80 @@ +// 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.Diagnostics; +using JetBrains.Annotations; + +namespace Microsoft.AspNet.Diagnostics.Entity.Utilities +{ + [DebuggerStepThrough] + internal static class Check + { + [ContractAnnotation("value:null => halt")] + public static T NotNull([NoEnumeration] T value, [InvokerParameterName] [NotNull] string parameterName) + { + NotEmpty(parameterName, "parameterName"); + + if (ReferenceEquals(value, null)) + { + throw new ArgumentNullException(parameterName); + } + + return value; + } + + [ContractAnnotation("value:null => halt")] + public static IReadOnlyList NotEmpty(IReadOnlyList value, [InvokerParameterName] [NotNull] string parameterName) + { + NotEmpty(parameterName, "parameterName"); + NotNull(value, parameterName); + + if (value.Count == 0) + { + throw new ArgumentException(Strings.CollectionArgumentIsEmpty(parameterName)); + } + + return value; + } + + [ContractAnnotation("value:null => halt")] + public static string NotEmpty(string value, [InvokerParameterName] [NotNull] string parameterName) + { + if (ReferenceEquals(parameterName, null)) + { + throw new ArgumentNullException("parameterName"); + } + + if (parameterName.Length == 0) + { + throw new ArgumentException(Strings.ArgumentIsEmpty("parameterName")); + } + + if (ReferenceEquals(value, null)) + { + throw new ArgumentNullException(parameterName); + } + + if (value.Length == 0) + { + throw new ArgumentException(Strings.ArgumentIsEmpty(parameterName)); + } + + return value; + } + + public static T IsDefined(T value, [InvokerParameterName] [NotNull] string parameterName) + where T : struct + { + NotEmpty(parameterName, "parameterName"); + + if (!Enum.IsDefined(typeof(T), value)) + { + throw new ArgumentException(Strings.InvalidEnumValue(parameterName, typeof(T))); + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cs b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cs new file mode 100644 index 0000000000..d2cbf854f5 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cs @@ -0,0 +1,429 @@ +namespace Microsoft.AspNet.Diagnostics.Entity.Views +{ +#line 1 "DatabaseErrorPage.cshtml" +using System + +#line default +#line hidden + ; +#line 2 "DatabaseErrorPage.cshtml" +using System.Linq + +#line default +#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" +using Microsoft.AspNet.Diagnostics.Entity.Views + +#line default +#line hidden + ; + using System.Threading.Tasks; + + public class DatabaseErrorPage : Microsoft.AspNet.Diagnostics.Views.BaseView + { +#line 14 "DatabaseErrorPage.cshtml" + + private DatabaseErrorPageModel _model; + + public virtual DatabaseErrorPageModel Model + { + get { return _model; } + [param: NotNull] + set + { + Check.NotNull(value, "value"); + + _model = value; + } + } + +#line default +#line hidden + #line hidden + public DatabaseErrorPage() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { +#line 7 "DatabaseErrorPage.cshtml" + + Response.StatusCode = 500; + // TODO: Response.ReasonPhrase = "Internal Server Error"; + Response.ContentType = "text/html"; + Response.ContentLength = null; // Clear any prior Content-Length + +#line default +#line hidden + + WriteLiteral("\r\n"); + 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" + Write(Strings.DatabaseErrorPage_Title); + +#line default +#line hidden + WriteLiteral("

\r\n"); +#line 42 "DatabaseErrorPage.cshtml" + + +#line default +#line hidden + +#line 42 "DatabaseErrorPage.cshtml" + if (Model.Options.ShowExceptionDetails) + { + +#line default +#line hidden + + WriteLiteral("

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

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

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

\r\n

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

\r\n "); +#line 58 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_AddMigrationCommand); + +#line default +#line hidden + WriteLiteral(" \r\n
\r\n "); +#line 60 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_ApplyMigrationsCommand); + +#line default +#line hidden + WriteLiteral(" \r\n

"); +#line 61 "DatabaseErrorPage.cshtml" + WriteLiteral(Strings.DatabaseErrorPage_EnableMigrationsCommandsInfo); + +#line default +#line hidden + WriteLiteral("

\r\n
\r\n"); +#line 63 "DatabaseErrorPage.cshtml" + } + else if (Model.PendingMigrations.Any()) + { + +#line default +#line hidden + + WriteLiteral("
\r\n

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

\r\n

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

\r\n\r\n"); +#line 70 "DatabaseErrorPage.cshtml" + + +#line default +#line hidden + +#line 70 "DatabaseErrorPage.cshtml" + if (Model.Options.ListMigrations) + { + +#line default +#line hidden + + WriteLiteral("
    \r\n"); +#line 73 "DatabaseErrorPage.cshtml" + + +#line default +#line hidden + +#line 73 "DatabaseErrorPage.cshtml" + foreach (var migration in Model.PendingMigrations) + { + +#line default +#line hidden + + WriteLiteral("
  • "); +#line 75 "DatabaseErrorPage.cshtml" + Write(migration); + +#line default +#line hidden + WriteLiteral("
  • \r\n"); +#line 76 "DatabaseErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral("
\r\n"); +#line 78 "DatabaseErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral("\r\n"); +#line 80 "DatabaseErrorPage.cshtml" + + +#line default +#line hidden + +#line 80 "DatabaseErrorPage.cshtml" + if (Model.Options.EnableMigrationCommands) + { + +#line default +#line hidden + + WriteLiteral("

\r\n + + +

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

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

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

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

\r\n
\r\n
\r\n"); +#line 129 "DatabaseErrorPage.cshtml" + } + else if (Model.PendingModelChanges) + { + +#line default +#line hidden + + WriteLiteral("
\r\n

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

\r\n

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

\r\n "); +#line 135 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_AddMigrationCommand); + +#line default +#line hidden + WriteLiteral("\r\n
\r\n "); +#line 137 "DatabaseErrorPage.cshtml" + Write(Strings.DatabaseErrorPage_ApplyMigrationsCommand); + +#line default +#line hidden + WriteLiteral("\r\n

"); +#line 138 "DatabaseErrorPage.cshtml" + WriteLiteral(Strings.DatabaseErrorPage_EnableMigrationsCommandsInfo); + +#line default +#line hidden + WriteLiteral("

\r\n
\r\n
\r\n"); +#line 141 "DatabaseErrorPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral("\r\n"); + } + #pragma warning restore 1998 + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cshtml b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cshtml new file mode 100644 index 0000000000..d559023e5d --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPage.cshtml @@ -0,0 +1,143 @@ +@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 virtual DatabaseErrorPageModel Model + { + get { return _model; } + [param: NotNull] + set + { + Check.NotNull(value, "value"); + + _model = value; + } + } +} + + + + + + Internal Server Error + + + +

@Strings.DatabaseErrorPage_Title

+ @if (Model.Options.ShowExceptionDetails) + { +

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

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

@Strings.DatabaseErrorPage_NoDbOrMigrationsTitle(Model.ContextType.Name)

+

@Strings.DatabaseErrorPage_NoDbOrMigrationsInfo

+ @Strings.DatabaseErrorPage_AddMigrationCommand +
+ @Strings.DatabaseErrorPage_ApplyMigrationsCommand +

@Strings.DatabaseErrorPage_EnableMigrationsCommandsInfo

+
+ } + else if (Model.PendingMigrations.Any()) + { +
+

@Strings.DatabaseErrorPage_PendingMigrationsTitle(Model.ContextType.Name)

+

@Strings.DatabaseErrorPage_PendingMigrationsInfo(Model.ContextType.Name)

+ + @if (Model.Options.ListMigrations) + { +
    + @foreach (var migration in Model.PendingMigrations) + { +
  • @migration
  • + } +
+ } + + @if (Model.Options.EnableMigrationCommands) + { +

+ + + +

+ + } + +

@Strings.DatabaseErrorPage_HowToApplyFromCmd

+ @Strings.DatabaseErrorPage_ApplyMigrationsCommand +

@Strings.DatabaseErrorPage_EnableMigrationsCommandsInfo

+
+
+ } + else if (Model.PendingModelChanges) + { +
+

@Strings.DatabaseErrorPage_PendingChangesTitle(Model.ContextType.Name)

+

@Strings.DatabaseErrorPage_PendingChangesInfo

+ @Strings.DatabaseErrorPage_AddMigrationCommand +
+ @Strings.DatabaseErrorPage_ApplyMigrationsCommand +

@Strings.DatabaseErrorPage_EnableMigrationsCommandsInfo

+
+
+ } + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPageModel.cs b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPageModel.cs new file mode 100644 index 0000000000..1343a4cdfe --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/Views/DatabaseErrorPageModel.cs @@ -0,0 +1,71 @@ +// 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 JetBrains.Annotations; +using Microsoft.AspNet.Diagnostics.Entity.Utilities; +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Diagnostics.Entity.Views +{ + public class DatabaseErrorPageModel + { + private readonly Type _contextType; + private readonly Exception _exception; + private readonly bool _databaseExists; + private readonly bool _pendingModelChanges; + private readonly IEnumerable _pendingMigrations; + private readonly DatabaseErrorPageOptions _options; + + public DatabaseErrorPageModel( + [NotNull] Type contextType, + [NotNull] Exception exception, + bool databaseExists, + bool pendingModelChanges, + [NotNull] IEnumerable pendingMigrations, + [NotNull] DatabaseErrorPageOptions options) + { + Check.NotNull(contextType, "contextType"); + Check.NotNull(exception, "exception"); + Check.NotNull(pendingMigrations, "pendingMigrations"); + Check.NotNull(options, "options"); + + _contextType = contextType; + _exception = exception; + _databaseExists = databaseExists; + _pendingModelChanges = pendingModelChanges; + _pendingMigrations = pendingMigrations; + _options = options; + } + + public virtual Type ContextType + { + get { return _contextType; } + } + + public virtual Exception Exception + { + get { return _exception; } + } + + public virtual bool DatabaseExists + { + get { return _databaseExists; } + } + + public virtual bool PendingModelChanges + { + get { return _pendingModelChanges; } + } + + public virtual IEnumerable PendingMigrations + { + get { return _pendingMigrations; } + } + + public virtual DatabaseErrorPageOptions Options + { + get { return _options; } + } + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Entity/Views/ErrorPage.css b/src/Microsoft.AspNet.Diagnostics.Entity/Views/ErrorPage.css new file mode 100644 index 0000000000..b875f43069 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/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/Microsoft.AspNet.Diagnostics.Entity/project.json b/src/Microsoft.AspNet.Diagnostics.Entity/project.json new file mode 100644 index 0000000000..9d4cb8e028 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Entity/project.json @@ -0,0 +1,24 @@ +{ + "version": "7.0.0-*", + "description": "ASP.NET 5 Middleware for Entity Framework error pages.", + "compilationOptions": { + "warningsAsErrors": true + }, + "dependencies": { + "Microsoft.AspNet.Diagnostics": "1.0.0-*", + "EntityFramework.Migrations": "7.0.0-*" + }, + "code": [ "**\\*.cs", "..\\Shared\\*.cs" ], + "frameworks": { + "aspnet50": { + "frameworkAssemblies": { + "System.Configuration": "" + } + }, + "aspnetcore50": { + "dependencies": { + "System.Threading.ExecutionContext": "4.0.0-beta-*" + } + } + } +} diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/DatabaseErrorPageMiddlewareTest.cs b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/DatabaseErrorPageMiddlewareTest.cs new file mode 100644 index 0000000000..23c0cc1ac4 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/DatabaseErrorPageMiddlewareTest.cs @@ -0,0 +1,367 @@ +// 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.Data.SqlClient; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Diagnostics.Entity.FunctionalTests.Helpers; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.TestHost; +using Microsoft.Data.Entity; +using Microsoft.Data.Entity.Migrations.Infrastructure; +using Microsoft.Data.Entity.SqlServer.FunctionalTests; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; +using Xunit; + +namespace Microsoft.AspNet.Diagnostics.Entity.Tests +{ + public class DatabaseErrorPageMiddlewareTest + { + [Fact] + public async Task Successful_requests_pass_thru() + { + TestServer server = TestServer.Create(app => app + .UseDatabaseErrorPage() + .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; + } + } + + [Fact] + public async Task Non_database_exceptions_pass_thru() + { + TestServer server = TestServer.Create(app => app + .UseDatabaseErrorPage() + .UseMiddleware()); + + 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"); + } + } + + [Fact] + public async Task Error_page_displayed_no_migrations() + { + TestServer server = await SetupTestServer(); + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(Strings.DatabaseErrorPage_NoDbOrMigrationsTitle(typeof(BloggingContext).Name), content); + Assert.Contains(Strings.DatabaseErrorPage_AddMigrationCommand.Replace(">", ">"), content); + Assert.Contains(Strings.DatabaseErrorPage_ApplyMigrationsCommand.Replace(">", ">"), content); + } + + class NoMigrationsMiddleware + { + 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"); + } + } + } + + [Fact] + public async Task Error_page_displayed_pending_migrations() + { + TestServer server = await SetupTestServer(); + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(Strings.DatabaseErrorPage_PendingMigrationsTitle(typeof(BloggingContextWithMigrations).Name), content); + Assert.Contains(Strings.DatabaseErrorPage_ApplyMigrationsCommand.Replace(">", ">"), content); + Assert.Contains("
  • 111111111111111_MigrationOne
  • ", content); + Assert.Contains("
  • 222222222222222_MigrationTwo
  • ", content); + + Assert.DoesNotContain(Strings.DatabaseErrorPage_AddMigrationCommand.Replace(">", ">"), content); + } + + class PendingMigrationsMiddleware + { + 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"); + } + } + } + + [Fact] + public async Task Error_page_displayed_pending_model_changes() + { + TestServer server = await SetupTestServer(); + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/"); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(Strings.DatabaseErrorPage_PendingChangesTitle(typeof(BloggingContextWithPendingModelChanges).Name), content); + Assert.Contains(Strings.DatabaseErrorPage_AddMigrationCommand.Replace(">", ">"), content); + Assert.Contains(Strings.DatabaseErrorPage_ApplyMigrationsCommand.Replace(">", ">"), content); + } + + class PendingModelChangesMiddleware + { + public PendingModelChangesMiddleware(RequestDelegate next) + { } + + public virtual Task Invoke(HttpContext context) + { + using (var db = context.ApplicationServices.GetService()) + { + var services = (MigrationsDataStoreServices)db.Configuration.DataStoreServices; + services.Migrator.ApplyMigrations(); + + db.Blogs.Add(new Blog()); + db.SaveChanges(); + throw new Exception("SaveChanges should have thrown"); + } + } + } + + [Fact] + public async Task Error_page_then_apply_migrations() + { + TestServer server = await 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 + Assert.Contains("req.open(\"POST\", \"" + expectedMigrationsEndpoint + "\", true);", content); + Assert.Contains("var params = \"context=\" + encodeURIComponent(\"" + 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) + { + using (var db = context.ApplicationServices.GetService()) + { + db.Blogs.Add(new Blog()); + db.SaveChanges(); + await context.Response.WriteAsync("Saved a Blog"); + } + } + } + + [Fact] + public async Task Customize_migrations_end_point() + { + var migrationsEndpoint = "/MyCustomEndPoints/ApplyMyMigrationsHere"; + + using (var database = await SqlServerTestStore.CreateScratchAsync(createDatabase: false)) + { + var server = TestServer.Create(app => + { + app.UseServices(services => + { + services.AddEntityFramework().AddSqlServer(); + services.AddScoped(); + services.AddInstance(new DbContextOptions().UseSqlServer(database.Connection.ConnectionString)); + }); + + var options = DatabaseErrorPageOptions.ShowAll; + options.MigrationsEndPointPath = new PathString(migrationsEndpoint); + app.UseDatabaseErrorPage(options); + + app.UseMiddleware(); + }); + + 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\", \"" + migrationsEndpoint + "\", true);", content); + } + } + + [Fact] + public async Task Pass_thru_when_context_not_in_services() + { + using (var database = await SqlServerTestStore.CreateScratchAsync(createDatabase: false)) + { + var logProvider = new TestLoggerProvider(); + + var server = TestServer.Create(app => + { + app.UseServices(services => + { + services.AddEntityFramework() + .AddSqlServer(); + + services.AddInstance( + new DbContextOptions() + .UseSqlServer(database.Connection.ConnectionString)); + }); + + app.UseDatabaseErrorPage(); + + app.UseMiddleware(); + + app.ApplicationServices.GetService().AddProvider(logProvider); + }); + + var ex = await Assert.ThrowsAsync(async () => + await server.CreateClient().GetAsync("http://localhost/")); + + Assert.True(logProvider.Logger.Messages.Any(m => + m.StartsWith(Strings.DatabaseErrorPageMiddleware_ContextNotRegistered(typeof(BloggingContext))))); + } + } + + class ContextNotRegisteredInServicesMiddleware + { + 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"); + } + } + } + + [Fact] + public async Task Pass_thru_when_exception_in_logic() + { + using (var database = await SqlServerTestStore.CreateScratchAsync(createDatabase: false)) + { + var logProvider = new TestLoggerProvider(); + + var server = await SetupTestServer(logProvider); + + var ex = await Assert.ThrowsAsync(async () => + await server.CreateClient().GetAsync("http://localhost/")); + + Assert.True(logProvider.Logger.Messages.Any(m => + m.StartsWith(Strings.DatabaseErrorPageMiddleware_Exception))); + } + } + + class ExceptionInLogicMiddleware + { + 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"); + } + } + } + + private static async Task SetupTestServer(ILoggerProvider logProvider = null) + where TContext : DbContext + { + using (var database = await SqlServerTestStore.CreateScratchAsync(createDatabase: false)) + { + return TestServer.Create(app => + { + app.UseServices(services => + { + services.AddEntityFramework() + .AddSqlServer(); + + services.AddScoped(); + services.AddInstance( + new DbContextOptions() + .UseSqlServer(database.Connection.ConnectionString)); + }); + + app.UseDatabaseErrorPage(); + + app.UseMiddleware(); + + if (logProvider != null) + { + app.ApplicationServices.GetService().AddProvider(logProvider); + } + }); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/Helpers/TestLoggerProvider.cs b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/Helpers/TestLoggerProvider.cs new file mode 100644 index 0000000000..9dfe936776 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/Helpers/TestLoggerProvider.cs @@ -0,0 +1,57 @@ +// 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.Framework.Logging; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Diagnostics.Entity.FunctionalTests.Helpers +{ + public class TestLoggerProvider : ILoggerProvider + { + private readonly TestLogger _logger = new TestLogger(); + + public TestLogger Logger + { + get { return _logger; } + } + + public ILogger Create(string name) + { + return _logger; + } + + public class TestLogger : ILogger + { + private List _messages = new List(); + + public IEnumerable Messages + { + get { return _messages; } + } + + public void Write(LogLevel logLevel, int eventId, object state, Exception exception, Func formatter) + { + _messages.Add(formatter(state, exception)); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public IDisposable BeginScope(object 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/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests.kproj b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests.kproj new file mode 100644 index 0000000000..dde5f1afd7 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests.kproj @@ -0,0 +1,14 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2f9b479d-8247-4210-804b-78e6dd5c3e98 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 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 0000000000..8c857030ef --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/MigrationsEndPointMiddlewareTest.cs @@ -0,0 +1,208 @@ +// 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.Http; +using Microsoft.AspNet.TestHost; +using Microsoft.Data.Entity; +using Microsoft.Data.Entity.Migrations.Infrastructure; +using Microsoft.Data.Entity.Migrations.Utilities; +using Microsoft.Data.Entity.SqlServer.FunctionalTests; +using Microsoft.Framework.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; + } + } + + [Fact] + public async Task Migration_request_default_path() + { + await Migration_request(useCustomPath: false); + } + + [Fact] + public async Task Migration_request_custom_path() + { + await Migration_request(useCustomPath: true); + } + + private async Task Migration_request(bool useCustomPath) + { + using (var database = await SqlServerTestStore.CreateScratchAsync(createDatabase: false)) + { + var options = new DbContextOptions().UseSqlServer(database.Connection.ConnectionString); + var path = useCustomPath ? new PathString("/EndPoints/ApplyMyMigrations") : MigrationsEndPointOptions.DefaultPath; + + TestServer server = TestServer.Create(app => + { + app.UseServices(services => + { + services.AddEntityFramework().AddSqlServer(); + services.AddScoped(); + services.AddInstance(options); + }); + + if (useCustomPath) + { + app.UseMigrationsEndPoint(new MigrationsEndPointOptions { Path = path }); + } + else + { + app.UseMigrationsEndPoint(); + } + }); + + using (var db = BloggingContextWithMigrations.CreateWithoutExternalServiceProvider(options)) + { + Assert.False(db.Database.AsRelational().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(db.Database.AsRelational().Exists()); + var services = (MigrationsDataStoreServices)db.Configuration.DataStoreServices; + var appliedMigrations = services.Migrator.GetDatabaseMigrations(); + Assert.Equal(2, appliedMigrations.Count); + Assert.Equal("111111111111111_MigrationOne", appliedMigrations.ElementAt(0).GetMigrationId()); + Assert.Equal("222222222222222_MigrationTwo", appliedMigrations.ElementAt(1).GetMigrationId()); + } + } + } + + [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(Strings.MigrationsEndPointMiddleware_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(Strings.MigrationsEndPointMiddleware_InvalidContextType(typeName), content); + Assert.True(content.Length > 512); + } + + [Fact] + public async Task Context_not_registered_in_services() + { + var server = TestServer.Create(app => + { + app.UseServices(services => + { + services.AddEntityFramework().AddSqlServer(); + }); + app.UseMigrationsEndPoint(); + }); + + 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(Strings.MigrationsEndPointMiddleware_ContextNotRegistered(typeof(BloggingContext)), content); + Assert.True(content.Length > 512); + } + + [Fact] + public async Task Exception_while_applying_migrations() + { + using (var database = await SqlServerTestStore.CreateScratchAsync(createDatabase: false)) + { + var options = new DbContextOptions().UseSqlServer(database.Connection.ConnectionString); + + TestServer server = TestServer.Create(app => + { + app.UseServices(services => + { + services.AddEntityFramework().AddSqlServer(); + services.AddScoped(); + services.AddInstance(options); + }); + + app.UseMigrationsEndPoint(); + }); + + 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.Equal(Strings.MigrationsEndPointMiddleware_Exception(typeof(BloggingContextWithSnapshotThatThrows)), ex.Message); + Assert.Equal("Welcome to the invalid snapshot!", ex.InnerException.Message); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/Blog.cs b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/Blog.cs new file mode 100644 index 0000000000..d6ef5d5a1c --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/Blog.cs @@ -0,0 +1,13 @@ +// 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; + +namespace Microsoft.AspNet.Diagnostics.Entity.Tests +{ + public class Blog + { + public int BlogId { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContext.cs b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContext.cs new file mode 100644 index 0000000000..7d13248bac --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContext.cs @@ -0,0 +1,32 @@ +// 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.Data.Entity; +using Microsoft.Data.Entity.Migrations.Infrastructure; +using Microsoft.Data.Entity.SqlServer.FunctionalTests; +using System; +using System.Linq; +using System.Reflection; + +namespace Microsoft.AspNet.Diagnostics.Entity.Tests +{ + public class BloggingContext : DbContext + { + protected static readonly string CurrentProductVersion = typeof(HistoryRepository) + .GetTypeInfo() + .Assembly + .GetCustomAttributes() + .Single() + .InformationalVersion; + + protected BloggingContext(DbContextOptions options) + : base(options) + { } + + public BloggingContext(IServiceProvider provider, DbContextOptions options) + : base(provider, options) + { } + + public DbSet Blogs { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContextWithMigrations.cs b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContextWithMigrations.cs new file mode 100644 index 0000000000..5d59d57820 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContextWithMigrations.cs @@ -0,0 +1,113 @@ +// 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.Data.Entity; +using Microsoft.Data.Entity.Metadata; +using Microsoft.Data.Entity.Migrations; +using Microsoft.Data.Entity.Migrations.Builders; +using Microsoft.Data.Entity.Migrations.Infrastructure; +using System; +using System.Linq; +using System.Reflection; + +namespace Microsoft.AspNet.Diagnostics.Entity.Tests +{ + public class BloggingContextWithMigrations : BloggingContext + { + protected BloggingContextWithMigrations(DbContextOptions options) + : base(options) + { } + + public BloggingContextWithMigrations(IServiceProvider provider, DbContextOptions options) + : base(provider, options) + { } + + // Providing a factory method so that the ctor is hidden from DI + public static BloggingContextWithMigrations CreateWithoutExternalServiceProvider(DbContextOptions options) + { + return new BloggingContextWithMigrations(options); + } + + [ContextType(typeof(BloggingContextWithMigrations))] + public class BloggingContextWithMigrationsModelSnapshot : ModelSnapshot + { + public override IModel Model + { + get + { + var builder = new BasicModelBuilder(); + + builder.Entity("Blogging.Models.Blog", b => + { + b.Property("BlogId"); + b.Property("BlogId").GenerateValueOnAdd(); + b.Property("Name"); + b.Key("BlogId"); + }); + + return builder.Model; + } + } + } + + [ContextType(typeof(BloggingContextWithMigrations))] + public class MigrationOne : Migration, IMigrationMetadata + { + string IMigrationMetadata.MigrationId + { + get { return "111111111111111_MigrationOne"; } + } + + string IMigrationMetadata.ProductVersion + { + get { return CurrentProductVersion; } + } + + IModel IMigrationMetadata.TargetModel + { + get { return new BloggingContextWithMigrationsModelSnapshot().Model; } + } + + public override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable("Blog", + c => new + { + BlogId = c.Int(nullable: false, identity: true), + Name = c.String(), + }) + .PrimaryKey("PK_Blog", t => t.BlogId); + } + + public override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable("Blog"); + } + } + + [ContextType(typeof(BloggingContextWithMigrations))] + public class MigrationTwo : Migration, IMigrationMetadata + { + string IMigrationMetadata.MigrationId + { + get { return "222222222222222_MigrationTwo"; } + } + + string IMigrationMetadata.ProductVersion + { + get { return CurrentProductVersion; } + } + + IModel IMigrationMetadata.TargetModel + { + get { return new BloggingContextWithMigrationsModelSnapshot().Model; } + } + + public override void Up(MigrationBuilder migrationBuilder) + { } + + public override void Down(MigrationBuilder migrationBuilder) + { } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContextWithPendingModelChanges.cs b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContextWithPendingModelChanges.cs new file mode 100644 index 0000000000..cb98b4bcbf --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContextWithPendingModelChanges.cs @@ -0,0 +1,53 @@ +// 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.Data.Entity; +using Microsoft.Data.Entity.Metadata; +using Microsoft.Data.Entity.Migrations; +using Microsoft.Data.Entity.Migrations.Builders; +using Microsoft.Data.Entity.Migrations.Infrastructure; +using System; + +namespace Microsoft.AspNet.Diagnostics.Entity.Tests +{ + public class BloggingContextWithPendingModelChanges : BloggingContext + { + public BloggingContextWithPendingModelChanges(IServiceProvider provider, DbContextOptions options) + : base(provider, options) + { } + + [ContextType(typeof(BloggingContextWithPendingModelChanges))] + public class BloggingModelSnapshot : ModelSnapshot + { + public override IModel Model + { + get { return new BasicModelBuilder().Model; } + } + } + + [ContextType(typeof(BloggingContextWithPendingModelChanges))] + public partial class MigrationOne : Migration, IMigrationMetadata + { + string IMigrationMetadata.MigrationId + { + get { return "111111111111111_MigrationOne"; } + } + + string IMigrationMetadata.ProductVersion + { + get { return CurrentProductVersion; } + } + + IModel IMigrationMetadata.TargetModel + { + get { return new BasicModelBuilder().Model; } + } + + public override void Up(MigrationBuilder migrationBuilder) + { } + + public override void Down(MigrationBuilder migrationBuilder) + { } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContextWithSnapshotThatThrows.cs b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContextWithSnapshotThatThrows.cs new file mode 100644 index 0000000000..f1c1fd1973 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/TestModels/BloggingContextWithSnapshotThatThrows.cs @@ -0,0 +1,60 @@ +// 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.Data.Entity; +using Microsoft.Data.Entity.Metadata; +using Microsoft.Data.Entity.Migrations; +using Microsoft.Data.Entity.Migrations.Builders; +using Microsoft.Data.Entity.Migrations.Infrastructure; +using System; +using System.Linq; +using System.Reflection; + +namespace Microsoft.AspNet.Diagnostics.Entity.Tests +{ + public class BloggingContextWithSnapshotThatThrows : BloggingContext + { + public BloggingContextWithSnapshotThatThrows(IServiceProvider provider, DbContextOptions options) + : base(provider, options) + { } + + [ContextType(typeof(BloggingContextWithSnapshotThatThrows))] + public class BloggingContextWithSnapshotThatThrowsModelSnapshot : ModelSnapshot + { + public override IModel Model + { + get + { + throw new Exception("Welcome to the invalid snapshot!"); + } + } + } + + [ContextType(typeof(BloggingContextWithSnapshotThatThrows))] + public class MigrationOne : Migration, IMigrationMetadata + { + string IMigrationMetadata.MigrationId + { + get { return "111111111111111_MigrationOne"; } + } + + string IMigrationMetadata.ProductVersion + { + get { return CurrentProductVersion; } + } + + IModel IMigrationMetadata.TargetModel + { + get { return new BloggingContextWithSnapshotThatThrowsModelSnapshot().Model; } + } + + public override void Up(MigrationBuilder migrationBuilder) + { + throw new Exception("Welcome to the invalid migration!"); + } + + public override void Down(MigrationBuilder migrationBuilder) + { } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/project.json b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/project.json new file mode 100644 index 0000000000..649018fad3 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.FunctionalTests/project.json @@ -0,0 +1,16 @@ +{ + "dependencies": { + "EntityFramework.SqlServer": "7.0.0-*", + "EntityFramework.SqlServer.FunctionalTests": "1.0.0", + "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-*", + "Microsoft.AspNet.Diagnostics.Entity.Tests": "1.0.0", + "Microsoft.AspNet.TestHost": "1.0.0-*", + "Xunit.KRunner": "1.0.0-*" + }, + "commands": { + "test": "Xunit.KRunner" + }, + "frameworks": { + "aspnet50": { } + } +} diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.Tests/ApiConsistencyTest.cs b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/ApiConsistencyTest.cs new file mode 100644 index 0000000000..d378bc8d47 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/ApiConsistencyTest.cs @@ -0,0 +1,38 @@ +// 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.Diagnostics.Entity; +using Microsoft.Data.Entity; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.Diagnostics.EntityTests +{ + public class ApiConsistencyTest : ApiConsistencyTestBase + { + protected override Assembly TargetAssembly + { + get { return typeof(DatabaseErrorPageMiddleware).Assembly; } + } + + protected override IEnumerable GetCancellationTokenExceptions() + { + return new string[] + { + "DatabaseErrorPageMiddleware.Invoke", + "MigrationsEndPointMiddleware.Invoke", + "DatabaseErrorPage.ExecuteAsync", + "BaseView.ExecuteAsync" + }; + } + + protected override IEnumerable GetAsyncSuffixExceptions() + { + return new string[] + { + "DatabaseErrorPageMiddleware.Invoke", + "MigrationsEndPointMiddleware.Invoke" + }; + } + } +} diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageOptionsTest.cs b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageOptionsTest.cs new file mode 100644 index 0000000000..d3c7d98109 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageOptionsTest.cs @@ -0,0 +1,64 @@ +// 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 Xunit; + +namespace Microsoft.AspNet.Diagnostics.Entity.Tests +{ + public class DatabaseErrorPageOptionsTest + { + [Fact] + public void Default_visibility_is_false() + { + var options = new DatabaseErrorPageOptions(); + + Assert.False(options.ShowExceptionDetails); + Assert.False(options.ListMigrations); + Assert.False(options.EnableMigrationCommands); + } + + [Fact] + public void Default_visibility_can_be_changed() + { + var options = new DatabaseErrorPageOptions(); + options.SetDefaultVisibility(true); + + Assert.True(options.ShowExceptionDetails); + Assert.True(options.ListMigrations); + Assert.True(options.EnableMigrationCommands); + } + + [Fact] + public void ShowExceptionDetails_overides_default_visibility() + { + var options = new DatabaseErrorPageOptions { ShowExceptionDetails = false }; + options.SetDefaultVisibility(true); + + Assert.False(options.ShowExceptionDetails); + Assert.True(options.ListMigrations); + Assert.True(options.EnableMigrationCommands); + } + + [Fact] + public void ListMigrations_overides_default_visibility() + { + var options = new DatabaseErrorPageOptions { ListMigrations = false }; + options.SetDefaultVisibility(true); + + Assert.True(options.ShowExceptionDetails); + Assert.False(options.ListMigrations); + Assert.True(options.EnableMigrationCommands); + } + + [Fact] + public void EnableMigrationCommands_overides_default_visibility() + { + var options = new DatabaseErrorPageOptions { EnableMigrationCommands = false }; + options.SetDefaultVisibility(true); + + Assert.True(options.ShowExceptionDetails); + Assert.True(options.ListMigrations); + Assert.False(options.EnableMigrationCommands); + } + } +} \ 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 new file mode 100644 index 0000000000..ad4364f224 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/DatabaseErrorPageTest.cs @@ -0,0 +1,262 @@ +// 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.Diagnostics.Entity.Tests.Helpers; +using Microsoft.AspNet.Diagnostics.Entity.Views; +using Microsoft.AspNet.Http; +using Microsoft.Data.Entity; +using Moq; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNet.Diagnostics.Entity.Tests +{ + public class DatabaseErrorPageTest + { + [Fact] + public async Task No_database_or_migrations_only_displays_scaffold_first_migration() + { + var options = new DatabaseErrorPageOptions(); + options.SetDefaultVisibility(true); + + 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(); + options.SetDefaultVisibility(true); + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception(), + databaseExists: false, + pendingModelChanges: false, + pendingMigrations: new string[] { "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(); + options.SetDefaultVisibility(true); + + 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); + + 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(); + options.SetDefaultVisibility(true); + + var model = new DatabaseErrorPageModel( + contextType: typeof(BloggingContext), + exception: new Exception(), + databaseExists: true, + pendingModelChanges: true, + pendingMigrations: new string[] { "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(); + options.SetDefaultVisibility(true); + + 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(); + options.SetDefaultVisibility(true); + + 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(); + options.SetDefaultVisibility(true); + + 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 ShowExceptionDetails_is_respected() + { + var options = new DatabaseErrorPageOptions { ShowExceptionDetails = false }; + options.SetDefaultVisibility(true); + + 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.DoesNotContain("Something bad happened", content); + } + + [Fact] + public async Task ListMigrations_is_respected() + { + var options = new DatabaseErrorPageOptions { ListMigrations = false }; + options.SetDefaultVisibility(true); + + 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("111_MigrationOne", content); + } + + [Fact] + public async Task EnableMigrationCommands_is_respected() + { + var options = new DatabaseErrorPageOptions { EnableMigrationCommands = false }; + options.SetDefaultVisibility(true); + + 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 { MigrationsEndPointPath = new PathString("/HitThisEndPoint") }; + options.SetDefaultVisibility(true); + + 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(); + 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/test/Microsoft.AspNet.Diagnostics.Entity.Tests/Helpers/AssertHelpers.cs b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/Helpers/AssertHelpers.cs new file mode 100644 index 0000000000..bc230c5419 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/Helpers/AssertHelpers.cs @@ -0,0 +1,41 @@ +// 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 Xunit; + +namespace Microsoft.AspNet.Diagnostics.Entity.Tests.Helpers +{ + public static class AssertHelpers + { + public static void DisplaysScaffoldFirstMigration(Type contextType, string content) + { + Assert.Contains(Strings.DatabaseErrorPage_NoDbOrMigrationsTitle(contextType.Name), content); + } + + public static void NotDisplaysScaffoldFirstMigration(Type contextType, string content) + { + Assert.DoesNotContain(Strings.DatabaseErrorPage_NoDbOrMigrationsTitle(contextType.Name), content); + } + + public static void DisplaysApplyMigrations(Type contextType, string content) + { + Assert.Contains(Strings.DatabaseErrorPage_PendingMigrationsTitle(contextType.Name), content); + } + + public static void NotDisplaysApplyMigrations(Type contextType, string content) + { + Assert.DoesNotContain(Strings.DatabaseErrorPage_PendingMigrationsTitle(contextType.Name), content); + } + + public static void DisplaysScaffoldNextMigraion(Type contextType, string content) + { + Assert.Contains(Strings.DatabaseErrorPage_PendingChangesTitle(contextType.Name), content); + } + + public static void NotDisplaysScaffoldNextMigraion(Type contextType, string content) + { + Assert.DoesNotContain(Strings.DatabaseErrorPage_PendingChangesTitle(contextType.Name), content); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.Tests/Microsoft.AspNet.Diagnostics.Entity.Tests.kproj b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/Microsoft.AspNet.Diagnostics.Entity.Tests.kproj new file mode 100644 index 0000000000..5039f0a889 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/Microsoft.AspNet.Diagnostics.Entity.Tests.kproj @@ -0,0 +1,14 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 5486117b-a742-49e0-94fc-12b76f061803 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.Tests/TestHelperExtensions.cs b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/TestHelperExtensions.cs new file mode 100644 index 0000000000..d9e793e6e6 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/TestHelperExtensions.cs @@ -0,0 +1,20 @@ +// 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.Framework.DependencyInjection; + +namespace Microsoft.Data.Entity.Tests +{ + public static class TestHelperExtensions + { + public static EntityServicesBuilder AddProviderServices(this EntityServicesBuilder entityServicesBuilder) + { + return entityServicesBuilder.AddInMemoryStore(); + } + + public static DbContextOptions UseProviderOptions(this DbContextOptions options) + { + return options; + } + } +} diff --git a/test/Microsoft.AspNet.Diagnostics.Entity.Tests/project.json b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/project.json new file mode 100644 index 0000000000..69fa4bca68 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Entity.Tests/project.json @@ -0,0 +1,15 @@ +{ + "dependencies": { + "EntityFramework.InMemory": "7.0.0-*", + "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-*", + "Moq": "4.2.1312.1622", + "Xunit.KRunner": "1.0.0-*" + }, + "code": [ "**\\*.cs", "..\\Shared\\ApiConsistencyTestBase.cs", "..\\Shared\\TestHelpers.cs" ], + "commands": { + "test": "Xunit.KRunner" + }, + "frameworks": { + "aspnet50": { } + } +}