Skip to content

Add AsyncMigrationBase, update base classes and call async methods#17057

Merged
AndyButland merged 10 commits intov16/devfrom
v15/feature/asyncmigrations
Mar 3, 2025
Merged

Add AsyncMigrationBase, update base classes and call async methods#17057
AndyButland merged 10 commits intov16/devfrom
v15/feature/asyncmigrations

Conversation

@ronaldbarendse
Copy link
Copy Markdown
Contributor

Prerequisites

  • I have added steps to test this contribution in the description below

Description

Similar to #16536, this PR makes it possible to (correctly) call async methods in migrations. I've done the following:

  • Added AsyncMigrationBase with new RunAsync() and MigrateAsync() methods, made this the base class of MigrationBase and moved most methods into this new class;
  • Similarly,. added UnscopedAsyncMigrationBase and made this the base class of UnscopedMigrationBase, added AsyncPackageMigrationBase and made this the base class of PackageMigrationBase;
  • Refactored IMigrationPlanExecutor to only have an ExecutePlanAsync() method;
  • Updated all calls to use the new async methods.

Testing should be done by ensuring a clean install creates and updates the database correctly and checking whether the code and tests are all good 😄


[Obsolete]
private void HandlePostMigrations(ExecutedMigrationPlan result)
private async Task HandlePostMigrationsAsync(ExecutedMigrationPlan result)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The post migrations have been obsoleted for a while (even stated for removal in v13, which didn't happen), but removing them should be done in a separate PR.

namespace Umbraco.Cms.Infrastructure.Migrations;

public class NoopMigration : MigrationBase
public class NoopMigration : AsyncMigrationBase
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be reverted back to MigrationBase to avoid an extra breaking change (so the base class doesn't change)... We have similar base class changes in UnscopedMigrationBase and PackageMigrationBase, which also don't inherit from MigrationBase anymore, but can't be fixed (because you can't simultaneously inherit from two base classes).

@bergmania
Copy link
Copy Markdown
Member

To me, I seems like there is a lot of breaking changes, that is not strictly necessary. Couldn't we let all the existing logic work as obsoleted, but introduce async migrations next to the existing. Thereby you would not have to change any base classes, you could just make a new version of the classes, E.g. NoopMigrationAsync

@AndyButland
Copy link
Copy Markdown
Contributor

I'm minded that we don't accept this one. I've tried, but I can't find a clean way to do it in a non-breaking way, and even if we accept breaking changes across majors it'll impact a lot of packages, which we likely don't want to unnecessarily do. Given migrations are one-off tasks on start-up only after an upgrade, and not something we expect to happen under heavy load, it feels like the benefit of async here may not be worth the disruption. Again, I'll run it by the team for opinions first.

@ronaldbarendse
Copy link
Copy Markdown
Contributor Author

I'm minded that we don't accept this one. I've tried, but I can't find a clean way to do it in a non-breaking way, and even if we accept breaking changes across majors it'll impact a lot of packages, which we likely don't want to unnecessarily do. Given migrations are one-off tasks on start-up only after an upgrade, and not something we expect to happen under heavy load, it feels like the benefit of async here may not be worth the disruption. Again, I'll run it by the team for opinions first.

The only real breaking change would be the requirement to call IMigrationPlanExecutor.ExecutePlanAsync(), instead of ExecutePlan(). We could add that one back again as obsolete and use GetAwaiter().GetResult(), as not having async support can potentially require doing this in your own migrations anyway (as some services don't have a synchronous implementation anymore).

@umbracocommunity
Copy link
Copy Markdown

This pull request has been mentioned on Umbraco community forum. There might be relevant details there:

https://forum.umbraco.com/t/package-migration-plans-do-they-support-async-at-all/316/6

# Conflicts:
#	src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs
@AndyButland AndyButland changed the base branch from v15/dev to v16/dev March 3, 2025 08:16
@AndyButland
Copy link
Copy Markdown
Contributor

AndyButland commented Mar 3, 2025

I've been looking at this some more this morning, so have now:

  • Updated it to the latest in v15/dev
  • Restored a few removed methods to reduce the number of breaking changes.
  • Tested a clean install ✅
  • Tested the following package migration, using a combination of the existing classes (to verify existing package migrations will work without changes, and new ones using the asynchronous methods) ✅
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Infrastructure.Migrations;

namespace Umbraco.Cms.Web.UI.Custom.PackageMigration;

public class TestMigrationPlan : PackageMigrationPlan
{
    public TestMigrationPlan()
        : base("Test Package", "TestPackageMigration")
    { }

    protected override void DefinePlan()
    {
        To<NoopMigration>("C09FDF8B-FF19-44EE-92B2-B7627D44370F");
        To<TestMigrationStep>("5B3ACA87-D4FC-4067-9CED-D78DC2E892AF");
        To<TestMigrationStep2>("924FCE33-EB4F-4A27-829F-38BB842DC8A5");
    }
}
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Migrations;
using Umbraco.Cms.Infrastructure.Packaging;

namespace Umbraco.Cms.Web.UI.Custom.PackageMigration;

public class TestMigrationStep : PackageMigrationBase
{
    private readonly IUserGroupService _userGroupService;

    public TestMigrationStep(
        IPackagingService packagingService,
        IMediaService mediaService,
        MediaFileManager mediaFileManager,
        MediaUrlGeneratorCollection mediaUrlGenerators,
        IShortStringHelper shortStringHelper,
        IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
        IMigrationContext context,
        IOptions<PackageMigrationSettings> packageMigrationsSettings,
        IUserGroupService userGroupService)
        : base(
              packagingService,
              mediaService,
              mediaFileManager,
              mediaUrlGenerators,
              shortStringHelper,
              contentTypeBaseServiceProvider,
              context,
              packageMigrationsSettings)
    {
        _userGroupService = userGroupService;
    }

    protected override void Migrate()
    {
        IUserGroup? adminUserGroup = _userGroupService.GetAsync(Guid.Parse("E5E7F6C8-7F9C-4B5B-8D5D-9E1E5A4F7E4D")).GetAwaiter().GetResult();
        Logger.LogInformation($"Running migration {nameof(TestMigrationStep)}. Admin user group has name {adminUserGroup?.Name}");
    }
}
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Migrations;
using Umbraco.Cms.Infrastructure.Packaging;

namespace Umbraco.Cms.Web.UI.Custom.PackageMigration;

public class TestMigrationStep2 : AsyncPackageMigrationBase
{
    private readonly IUserGroupService _userGroupService;

    public TestMigrationStep2(
        IPackagingService packagingService,
        IMediaService mediaService,
        MediaFileManager mediaFileManager,
        MediaUrlGeneratorCollection mediaUrlGenerators,
        IShortStringHelper shortStringHelper,
        IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
        IMigrationContext context,
        IOptions<PackageMigrationSettings> packageMigrationsSettings,
        IUserGroupService userGroupService,
        IUserService userService)
        : base(
              packagingService,
              mediaService,
              mediaFileManager,
              mediaUrlGenerators,
              shortStringHelper,
              contentTypeBaseServiceProvider,
              context,
              packageMigrationsSettings)
    {
        _userGroupService = userGroupService;
    }

    protected override async Task MigrateAsync()
    {
        IUserGroup? adminUserGroup = await _userGroupService.GetAsync(Guid.Parse("E5E7F6C8-7F9C-4B5B-8D5D-9E1E5A4F7E4D"));
        Logger.LogInformation($"Running migration {nameof(TestMigrationStep2)}. Admin user group has name {adminUserGroup?.Name}");
    }
}
  • Retargeted to v16/dev, on the basis that there are still breaking changes it doesn't seem we can really avoid, but that they are limited now to code we don't expect anyone using the migration functionality to be affected by (i.e. source breaking rather than binary breaking).

@AndyButland AndyButland added the status/needs-docs Requires new or updated documentation label Mar 3, 2025
@AndyButland AndyButland merged commit 13c788d into v16/dev Mar 3, 2025
20 of 21 checks passed
@AndyButland AndyButland deleted the v15/feature/asyncmigrations branch March 3, 2025 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/backend category/breaking status/needs-docs Requires new or updated documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants