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
  • Loading branch information
AndriySvyryd committed Jul 10, 2024
1 parent 24fdd2f commit 3e1018b
Show file tree
Hide file tree
Showing 24 changed files with 538 additions and 119 deletions.
14 changes: 14 additions & 0 deletions src/EFCore.Relational/Diagnostics/RelationalEventId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private enum Id
MigrationsNotFound,
MigrationAttributeMissingWarning,
ColumnOrderIgnoredWarning,
PendingModelChangesWarning,

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

/// <summary>
/// Column order was ignored.
/// </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);

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

private static EventId MakeQueryId(Id id)
Expand Down
34 changes: 34 additions & 0 deletions src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2309,6 +2309,40 @@ 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 PendingModelChanges(
this IDiagnosticsLogger<DbLoggerCategory.Migrations> diagnostics,
Type contextType)
{
var definition = RelationalResources.LogPendingModelChanges(diagnostics);

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

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<Type>)definition;
var p = (DbContextTypeEventData)payload;
return d.GenerateMessage(p.ContextType);
}

/// <summary>
/// Logs for the <see cref="RelationalEventId.QueryPossibleUnintendedUseOfEqualsWarning" /> event.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,15 @@ 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,35 @@ 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="seed">
/// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied.
/// </param>
/// <param name="targetMigration">
/// The target migration to migrate the database to, or <see langword="null" /> to migrate to the latest.
/// </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)
=> databaseFacade.GetRelationalService<IMigrator>().Migrate(targetMigration, seed);

/// <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 +171,40 @@ 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="seed">
/// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied.
/// </param>
/// <param name="targetMigration">
/// The target migration to migrate the database to, or <see langword="null" /> to migrate to the latest.
/// </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,
CancellationToken cancellationToken = default)
=> databaseFacade.GetRelationalService<IMigrator>().MigrateAsync(targetMigration, seed, cancellationToken);

/// <summary>
/// Executes the given SQL against the database and returns the number of rows affected.
/// </summary>
Expand Down Expand Up @@ -974,29 +1037,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,8 @@ 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));

/// <summary>
/// Information/metadata for a <see cref="RelationalOptionsExtension" />.
Expand Down
21 changes: 20 additions & 1 deletion src/EFCore.Relational/Migrations/IMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ public interface IMigrator
/// <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>
[RequiresUnreferencedCode("Migration generation currently isn't compatible with trimming")]
[RequiresDynamicCode("Migrations operations are not supported with NativeAOT")]
void Migrate(string? targetMigration = null);
void Migrate(string? targetMigration = null, Action<DbContext, IMigratorData>? seed = null);

/// <summary>
/// Migrates the database to either a specified target migration or up to the latest
Expand All @@ -46,13 +49,17 @@ public interface IMigrator
/// <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="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the asynchronous operation</returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
[RequiresUnreferencedCode("Migration generation currently isn't compatible with trimming")]
[RequiresDynamicCode("Migrations operations are not supported with NativeAOT")]
Task MigrateAsync(
string? targetMigration = null,
Func<DbContext, IMigratorData, CancellationToken, Task>? seed = null,
CancellationToken cancellationToken = default);

/// <summary>
Expand All @@ -78,4 +85,16 @@ string GenerateScript(
string? fromMigration = null,
string? toMigration = null,
MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default);

/// <summary>
/// Returns <see langword="true" /> if the model has pending changes to be applied.
/// </summary>
/// <returns>
/// <see langword="true" /> if the database model has pending changes
/// and a new migration has to be added.
/// </returns>
[RequiresDynamicCode(
"Migrations operations are not supported with NativeAOT"
+ " Use a migration bundle or an alternate way of executing migration operations.")]
bool HasPendingModelChanges();
}
29 changes: 29 additions & 0 deletions src/EFCore.Relational/Migrations/IMigratorData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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.Migrations;

/// <summary>
/// A class that holds the results from the last migrations application.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-migrations">Database migrations</see> for more information and examples.
/// </remarks>
public interface IMigratorData
{
/// <summary>
/// The migrations that were applied to the database.
/// </summary>
public IReadOnlyList<Migration> AppliedMigrations { get; }

/// <summary>
/// The migrations that were reverted from the database.
/// </summary>
public IReadOnlyList<Migration> RevertedMigrations { get; }

/// <summary>
/// The target migration.
/// <see langword="null" /> if all migrations were reverted or no target migration was specified.
/// </summary>
public Migration? TargetMigration { get; }
}
47 changes: 47 additions & 0 deletions src/EFCore.Relational/Migrations/IMigratorPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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.Migrations;

/// <summary>
/// <para>
/// A service on the EF internal service provider that allows providers or extensions to execute logic
/// after <see cref="IMigrator.Migrate(string?, Action{DbContext, IMigratorData}?)"/> is called.
/// </para>
/// <para>
/// This type is typically used by providers or extensions. It is generally not used in application code.
/// </para>
/// </summary>
/// <remarks>
/// The service lifetime is <see cref="ServiceLifetime.Singleton" />. This means a single instance
/// is used by many <see cref="DbContext" /> instances. The implementation must be thread-safe.
/// This service cannot depend on services registered as <see cref="ServiceLifetime.Scoped" />.
/// </remarks>
public interface IMigratorPlugin
{
/// <summary>
/// Called by <see cref="IMigrator.Migrate(string?, Action{DbContext, IMigratorData}?)"/> before the seeding action.
/// </summary>
/// <param name="context">The <see cref="DbContext" /> that is being migrated.</param>
/// <param name="data">The <see cref="IMigratorData" /> that contains the result of the migrations application.</param>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-migrations">Database migrations</see> for more information and examples.
/// </remarks>
void Migrated(DbContext context, IMigratorData data);

/// <summary>
/// Called by <see cref="IMigrator.MigrateAsync(string?, Func{DbContext, IMigratorData, CancellationToken, Task}?, CancellationToken)"/> before the seeding action.
/// </summary>
/// <param name="context">The <see cref="DbContext" /> that is being migrated.</param>
/// <param name="data">The <see cref="IMigratorData" /> that contains the result of the migrations application.</param>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-migrations">Database migrations</see> for more information and examples.
/// </remarks>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the asynchronous operation</returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
Task MigratedAsync(
DbContext context,
IMigratorData data,
CancellationToken cancellationToken = default);
}
Loading

0 comments on commit 3e1018b

Please sign in to comment.