Skip to content

Commit

Permalink
Add extension point to Migrate
Browse files Browse the repository at this point in the history
Allow to specify a target migration in Migrate call
Warn on Migrate when there are pending model changes

Fixes #17568
Fixes #33732
Fixes #34196
  • Loading branch information
AndriySvyryd committed Jul 26, 2024
1 parent b981879 commit f0fbcbe
Show file tree
Hide file tree
Showing 25 changed files with 855 additions and 139 deletions.
50 changes: 50 additions & 0 deletions src/EFCore.Relational/Diagnostics/MigrationCommandEventData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Diagnostics;

/// <summary>
/// The <see cref="DiagnosticSource" /> event payload for
/// <see cref="RelationalEventId" /> events of a specific migration.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-diagnostics">Logging, events, and diagnostics</see> for more information and examples.
/// </remarks>
public class MigrationCommandEventData : MigratorEventData
{
/// <summary>
/// Constructs the event payload.
/// </summary>
/// <param name="eventDefinition">The event definition.</param>
/// <param name="messageGenerator">A delegate that generates a log message for this event.</param>
/// <param name="migrator">
/// The <see cref="IMigrator" /> in use.
/// </param>
/// <param name="migration">
/// The <see cref="Migration" /> being processed.
/// </param>
/// <param name="command">
/// The <see cref="MigrationCommand" /> being processed.
/// </param>
public MigrationCommandEventData(
EventDefinitionBase eventDefinition,
Func<EventDefinitionBase, EventData, string> messageGenerator,
IMigrator migrator,
Migration migration,
MigrationCommand command)
: base(eventDefinition, messageGenerator, migrator)
{
Migration = migration;
MigrationCommand = command;
}

/// <summary>
/// The <see cref="Migration" /> being processed.
/// </summary>
public virtual Migration Migration { get; }

/// <summary>
/// The <see cref="MigrationCommand" /> being processed.
/// </summary>
public virtual MigrationCommand MigrationCommand { get; }
}
28 changes: 28 additions & 0 deletions src/EFCore.Relational/Diagnostics/RelationalEventId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ private enum Id
MigrationsNotFound,
MigrationAttributeMissingWarning,
ColumnOrderIgnoredWarning,
PendingModelChangesWarning,
NonTransactionalMigrationOperationWarning,

// Query events
QueryClientEvaluationWarning = CoreEventId.RelationalBaseId + 500,
Expand Down Expand Up @@ -721,6 +723,32 @@ private static EventId MakeMigrationsId(Id id)
/// </remarks>
public static readonly EventId ColumnOrderIgnoredWarning = MakeMigrationsId(Id.ColumnOrderIgnoredWarning);

/// <summary>
/// The model contains changes compared to the last migration.
/// </summary>
/// <remarks>
/// <para>
/// This event is in the <see cref="DbLoggerCategory.Migrations" /> category.
/// </para>
/// <para>
/// This event uses the <see cref="DbContextTypeEventData" /> payload when used with a <see cref="DiagnosticSource" />.
/// </para>
/// </remarks>
public static readonly EventId PendingModelChangesWarning = MakeMigrationsId(Id.PendingModelChangesWarning);

/// <summary>
/// A migration contains a non-transactional operation.
/// </summary>
/// <remarks>
/// <para>
/// This event is in the <see cref="DbLoggerCategory.Migrations" /> category.
/// </para>
/// <para>
/// This event uses the <see cref="MigrationCommandEventData" /> payload when used with a <see cref="DiagnosticSource" />.
/// </para>
/// </remarks>
public static readonly EventId NonTransactionalMigrationOperationWarning = MakeMigrationsId(Id.NonTransactionalMigrationOperationWarning);

private static readonly string _queryPrefix = DbLoggerCategory.Query.Name + ".";

private static EventId MakeQueryId(Id id)
Expand Down
84 changes: 84 additions & 0 deletions src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2309,6 +2309,90 @@ private static string MigrationAttributeMissingWarning(EventDefinitionBase defin
return d.GenerateMessage(p.MigrationType.Name);
}

/// <summary>
/// Logs for the <see cref="RelationalEventId.PendingModelChangesWarning" /> event.
/// </summary>
/// <param name="diagnostics">The diagnostics logger to use.</param>
/// <param name="contextType">The <see cref="DbContext" /> type being used.</param>
public static void PendingModelChangesWarning(
this IDiagnosticsLogger<DbLoggerCategory.Migrations> diagnostics,
Type contextType)
{
var definition = RelationalResources.LogPendingModelChanges(diagnostics);

if (diagnostics.ShouldLog(definition))
{
definition.Log(diagnostics, contextType.ShortDisplayName());
}

if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
{
var eventData = new DbContextTypeEventData(
definition,
PendingModelChanges,
contextType);

diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
}
}

private static string PendingModelChanges(EventDefinitionBase definition, EventData payload)
{
var d = (EventDefinition<string>)definition;
var p = (DbContextTypeEventData)payload;
return d.GenerateMessage(p.ContextType.ShortDisplayName());
}

/// <summary>
/// Logs for the <see cref="RelationalEventId.NonTransactionalMigrationOperationWarning" /> event.
/// </summary>
/// <param name="diagnostics">The diagnostics logger to use.</param>
/// <param name="migrator">The <see cref="IMigrator" /> in use.</param>
/// <param name="migration">The <see cref="Migration" /> being processed.</param>
/// <param name="command">The <see cref="MigrationCommand" /> being processed.</param>
public static void NonTransactionalMigrationOperationWarning(
this IDiagnosticsLogger<DbLoggerCategory.Migrations> diagnostics,
IMigrator migrator,
Migration migration,
MigrationCommand command)
{
var definition = RelationalResources.LogNonTransactionalMigrationOperationWarning(diagnostics);

if (diagnostics.ShouldLog(definition))
{
var commandText = command.CommandText;
if (commandText.Length > 100)
{
commandText = commandText.Substring(0, 100) + "...";
}
definition.Log(diagnostics, commandText, migration.GetType().ShortDisplayName());
}

if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
{
var eventData = new MigrationCommandEventData(
definition,
NonTransactionalMigrationOperationWarning,
migrator,
migration,
command);

diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
}
}

private static string NonTransactionalMigrationOperationWarning(EventDefinitionBase definition, EventData payload)
{
var d = (EventDefinition<string, string>)definition;
var p = (MigrationCommandEventData)payload;
var commandText = p.MigrationCommand.CommandText;
if (commandText.Length > 100)
{
commandText = commandText.Substring(0, 100) + "...";
}
return d.GenerateMessage(commandText, p.Migration.GetType().ShortDisplayName());
}

/// <summary>
/// Logs for the <see cref="RelationalEventId.QueryPossibleUnintendedUseOfEqualsWarning" /> event.
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,24 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions
[EntityFrameworkInternal]
public EventDefinitionBase? LogColumnOrderIgnoredWarning;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public EventDefinitionBase? LogPendingModelChanges;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public EventDefinitionBase? LogNonTransactionalMigrationOperationWarning;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,40 @@ public static async Task<IEnumerable<string>> GetPendingMigrationsAsync(
public static void Migrate(this DatabaseFacade databaseFacade)
=> databaseFacade.GetRelationalService<IMigrator>().Migrate();

/// <summary>
/// Applies migrations for the context to the database. Will create the database
/// if it does not already exist.
/// </summary>
/// <param name="targetMigration">
/// The target migration to migrate the database to, or <see langword="null" /> to migrate to the latest.
/// </param>
/// <param name="seed">
/// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied.
/// </param>
/// <param name="lockTimeout">
/// The maximum amount of time that the migration lock should be held. Unless a catastrophic failure occurs, the
/// lock is released when the migration operation completes.
/// </param>
/// <remarks>
/// <para>
/// Note that this API is mutually exclusive with <see cref="DatabaseFacade.EnsureCreated" />. EnsureCreated does not use migrations
/// to create the database and therefore the database that is created cannot be later updated using migrations.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-migrations">Database migrations</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
[RequiresDynamicCode(
"Migrations operations are not supported with NativeAOT"
+ " Use a migration bundle or an alternate way of executing migration operations.")]
public static void Migrate(
this DatabaseFacade databaseFacade,
Action<DbContext, IMigratorData>? seed,
string? targetMigration = null,
TimeSpan? lockTimeout = null)
=> databaseFacade.GetRelationalService<IMigrator>().Migrate(targetMigration, seed, lockTimeout);

/// <summary>
/// Asynchronously applies any pending migrations for the context to the database. Will create the database
/// if it does not already exist.
Expand All @@ -142,6 +176,45 @@ public static Task MigrateAsync(
CancellationToken cancellationToken = default)
=> databaseFacade.GetRelationalService<IMigrator>().MigrateAsync(cancellationToken: cancellationToken);

/// <summary>
/// Asynchronously applies migrations for the context to the database. Will create the database
/// if it does not already exist.
/// </summary>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
/// <param name="targetMigration">
/// The target migration to migrate the database to, or <see langword="null" /> to migrate to the latest.
/// </param>
/// <param name="seed">
/// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied.
/// </param>
/// <param name="lockTimeout">
/// The maximum amount of time that the migration lock should be held. Unless a catastrophic failure occurs, the
/// lock is released when the migration operation completes.
/// </param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <remarks>
/// <para>
/// Note that this API is mutually exclusive with <see cref="DatabaseFacade.EnsureCreated" />.
/// <see cref="DatabaseFacade.EnsureCreated" /> does not use migrations to create the database and therefore the database
/// that is created cannot be later updated using migrations.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-migrations">Database migrations</see> for more information and examples.
/// </para>
/// </remarks>
/// <returns>A task that represents the asynchronous migration operation.</returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
[RequiresDynamicCode(
"Migrations operations are not supported with NativeAOT"
+ " Use a migration bundle or an alternate way of executing migration operations.")]
public static Task MigrateAsync(
this DatabaseFacade databaseFacade,
Func<DbContext, IMigratorData, CancellationToken, Task>? seed,
string? targetMigration = null,
TimeSpan? lockTimeout = null,
CancellationToken cancellationToken = default)
=> databaseFacade.GetRelationalService<IMigrator>().MigrateAsync(targetMigration, seed, lockTimeout, cancellationToken);

/// <summary>
/// Executes the given SQL against the database and returns the number of rows affected.
/// </summary>
Expand Down Expand Up @@ -974,29 +1047,7 @@ public static bool IsRelational(this DatabaseFacade databaseFacade)
"Migrations operations are not supported with NativeAOT"
+ " Use a migration bundle or an alternate way of executing migration operations.")]
public static bool HasPendingModelChanges(this DatabaseFacade databaseFacade)
{
var modelDiffer = databaseFacade.GetRelationalService<IMigrationsModelDiffer>();
var migrationsAssembly = databaseFacade.GetRelationalService<IMigrationsAssembly>();

var modelInitializer = databaseFacade.GetRelationalService<IModelRuntimeInitializer>();

var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
if (snapshotModel is IMutableModel mutableModel)
{
snapshotModel = mutableModel.FinalizeModel();
}

if (snapshotModel is not null)
{
snapshotModel = modelInitializer.Initialize(snapshotModel);
}

var designTimeModel = databaseFacade.GetRelationalService<IDesignTimeModel>();

return modelDiffer.HasDifferences(
snapshotModel?.GetRelationalModel(),
designTimeModel.Model.GetRelationalModel());
}
=> databaseFacade.GetRelationalService<IMigrator>().HasPendingModelChanges();

private static IRelationalDatabaseFacadeDependencies GetFacadeDependencies(DatabaseFacade databaseFacade)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ public static readonly IDictionary<Type, ServiceCharacteristics> RelationalServi
typeof(IAggregateMethodCallTranslatorPlugin),
new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true)
},
{ typeof(IMemberTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }
{ typeof(IMemberTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) },
{ typeof(IMigratorPlugin), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }
};

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,9 @@ public static CoreOptionsExtension WithDefaultWarningConfiguration(CoreOptionsEx
.TryWithExplicit(RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable, WarningBehavior.Throw)
.TryWithExplicit(RelationalEventId.IndexPropertiesMappedToNonOverlappingTables, WarningBehavior.Throw)
.TryWithExplicit(RelationalEventId.ForeignKeyPropertiesMappedToUnrelatedTables, WarningBehavior.Throw)
.TryWithExplicit(RelationalEventId.StoredProcedureConcurrencyTokenNotMapped, WarningBehavior.Throw));
.TryWithExplicit(RelationalEventId.StoredProcedureConcurrencyTokenNotMapped, WarningBehavior.Throw)
.TryWithExplicit(RelationalEventId.PendingModelChangesWarning, WarningBehavior.Throw)
.TryWithExplicit(RelationalEventId.NonTransactionalMigrationOperationWarning, WarningBehavior.Throw));

/// <summary>
/// Information/metadata for a <see cref="RelationalOptionsExtension" />.
Expand Down
Loading

0 comments on commit f0fbcbe

Please sign in to comment.