Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extending ISetupEventHandler with new events #14184

Merged
merged 20 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using OrchardCore.Abstractions.Setup;
using OrchardCore.Setup.Events;
using OrchardCore.Setup.Services;

namespace OrchardCore.Settings.Services
{
Expand All @@ -18,19 +18,17 @@ public SetupEventHandler(ISiteService siteService)
_siteService = siteService;
}

public async Task Setup(
IDictionary<string, object> properties,
Action<string, string> reportError
)
public async Task SetupAsync(SetupContext context)
{
// Updating site settings
// Updating site settings.
var siteSettings = await _siteService.LoadSiteSettingsAsync();
siteSettings.SiteName = properties.TryGetValue(SetupConstants.SiteName, out var siteName) ? siteName?.ToString() : String.Empty;
siteSettings.SuperUser = properties.TryGetValue(SetupConstants.AdminUserId, out var adminUserId) ? adminUserId?.ToString() : String.Empty;
siteSettings.TimeZoneId = properties.TryGetValue(SetupConstants.SiteTimeZone, out var siteTimeZone) ? siteTimeZone?.ToString() : String.Empty;
siteSettings.SiteName = context.Properties.TryGetValue(SetupConstants.SiteName, out var siteName) ? siteName?.ToString() : String.Empty;
siteSettings.SuperUser = context.Properties.TryGetValue(SetupConstants.AdminUserId, out var adminUserId) ? adminUserId?.ToString() : String.Empty;
siteSettings.TimeZoneId = context.Properties.TryGetValue(SetupConstants.SiteTimeZone, out var siteTimeZone) ? siteTimeZone?.ToString() : String.Empty;

await _siteService.UpdateSiteSettingsAsync(siteSettings);

// TODO: Add Encryption Settings in
// TODO: Add Encryption Settings in.
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public async Task<ActionResult> IndexPOST(SetupViewModel model)
var executionId = await _setupService.SetupAsync(setupContext);

// Check if any Setup component failed (e.g., database connection validation)
if (setupContext.Errors.Any())
if (setupContext.Errors.Count > 0)
{
foreach (var error in setupContext.Errors)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,8 @@ public async Task<ActionResult> Setup(SetupApiViewModel model)

var executionId = await _setupService.SetupAsync(setupContext);

// Check if a component in the Setup failed
if (setupContext.Errors.Any())
// Check if a component in the Setup failed.
if (setupContext.Errors.Count > 0)
{
foreach (var error in setupContext.Errors)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ public override async Task<ActivityExecutionResult> ExecuteAsync(WorkflowExecuti

var executionId = await SetupService.SetupAsync(setupContext);

// Check if a component in the Setup failed
if (setupContext.Errors.Any())
// Check if a component in the Setup failed.
if (setupContext.Errors.Count > 0)
{
var updater = _updateModelAccessor.ModelUpdater;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using OrchardCore.Abstractions.Setup;
using OrchardCore.Setup.Events;
using OrchardCore.Setup.Services;
using OrchardCore.Users.Models;

namespace OrchardCore.Users.Services
Expand All @@ -19,22 +19,22 @@ public SetupEventHandler(IUserService userService)
_userService = userService;
}

public Task Setup(
IDictionary<string, object> properties,
Action<string, string> reportError
)
public Task SetupAsync(SetupContext context)
{
var user = new User
{
UserName = properties.TryGetValue(SetupConstants.AdminUsername, out var adminUserName) ? adminUserName?.ToString() : String.Empty,
UserId = properties.TryGetValue(SetupConstants.AdminUserId, out var adminUserId) ? adminUserId?.ToString() : String.Empty,
Email = properties.TryGetValue(SetupConstants.AdminEmail, out var adminEmail) ? adminEmail?.ToString() : String.Empty,
UserName = context.Properties.TryGetValue(SetupConstants.AdminUsername, out var adminUserName) ? adminUserName?.ToString() : String.Empty,
UserId = context.Properties.TryGetValue(SetupConstants.AdminUserId, out var adminUserId) ? adminUserId?.ToString() : String.Empty,
Email = context.Properties.TryGetValue(SetupConstants.AdminEmail, out var adminEmail) ? adminEmail?.ToString() : String.Empty,
EmailConfirmed = true
};

user.RoleNames.Add("Administrator");

return _userService.CreateUserAsync(user, properties[SetupConstants.AdminPassword]?.ToString(), reportError);
return _userService.CreateUserAsync(user, context.Properties[SetupConstants.AdminPassword]?.ToString(), (key, message) =>
{
context.Errors[key] = message;
});
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using OrchardCore.Setup.Services;

namespace OrchardCore.Setup.Events
namespace OrchardCore.Setup.Events;

public interface ISetupEventHandler
{
[Obsolete($"This method is obsolete and will be removed in future releases. Please use '{nameof(SetupAsync)}' instead.", false)]
Task Setup(IDictionary<string, object> properties, Action<string, string> reportError)
=> Task.CompletedTask;

/// <summary>
/// Called during the process of setting up a new tenant.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
Task SetupAsync(SetupContext context)
=> Task.CompletedTask;
Copy link
Member

Choose a reason for hiding this comment

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

In the meantime I will approve

But as a last idea the default implementation of SetupAsync() could be to call Setup() so that we don't need to call the obsolete Setup() from the SetupService and only use the rule pragma here, all in one place.

Something like this, quickly written so may need to be tweaked.

Task SetupAsync(SetupContext context) =>
    Setup(context.Properties, (key, message) => context.Errors[key] = message );


/// <summary>
/// Called when a tenant fails to setup.
/// </summary>
/// <returns></returns>
Task FailedAsync(SetupContext context)
=> Task.CompletedTask;

/// <summary>
/// Contract that is called when a tenant is set up.
/// Called when a new tenant is successfully setup.
/// </summary>
public interface ISetupEventHandler
{
Task Setup(
IDictionary<string, object> properties,
Action<string, string> reportError
);
}
/// <returns></returns>
Task SucceededAsync()
=> Task.CompletedTask;
}
72 changes: 39 additions & 33 deletions src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public async Task<string> SetupAsync(SetupContext context)
{
var executionId = await SetupInternalAsync(context);

if (context.Errors.Any())
if (context.Errors.Count > 0)
{
context.ShellSettings.State = initialState;
await _shellHost.ReloadShellContextAsync(context.ShellSettings, eventSource: false);
Expand All @@ -113,36 +113,33 @@ public async Task<string> SetupAsync(SetupContext context)

private async Task<string> SetupInternalAsync(SetupContext context)
{
string executionId;

if (_logger.IsEnabled(LogLevel.Information))
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
{
_logger.LogInformation("Running setup for tenant '{TenantName}'.", context.ShellSettings.Name);
_logger.LogInformation("Running setup for tenant '{TenantName}'.", context.ShellSettings?.Name);
}

// Features to enable for Setup
string[] hardcoded =
// Features to enable for Setup.
string[] coreFeatures =
{
_applicationName,
"OrchardCore.Features",
"OrchardCore.Scripting",
"OrchardCore.Recipes"
};

context.EnabledFeatures = hardcoded.Union(context.EnabledFeatures ?? Enumerable.Empty<string>()).Distinct().ToList();
context.EnabledFeatures = coreFeatures.Union(context.EnabledFeatures ?? Enumerable.Empty<string>()).Distinct().ToList();

// Set shell state to "Initializing" so that subsequent HTTP requests are responded to with "Service Unavailable" while Orchard is setting up.
context.ShellSettings.AsInitializing();

// Due to database collation we normalize the userId to lower invariant.
// During setup there are no users so we do not need to check unicity.
context.Properties[SetupConstants.AdminUserId] = _setupUserIdGenerator.GenerateUniqueId().ToLowerInvariant();
var adminUserId = _setupUserIdGenerator.GenerateUniqueId().ToLowerInvariant();
context.Properties[SetupConstants.AdminUserId] = adminUserId;

var recipeEnvironmentFeature = new RecipeEnvironmentFeature();
if (context.Properties.TryGetValue(SetupConstants.AdminUserId, out var adminUserId))
{
recipeEnvironmentFeature.Properties[SetupConstants.AdminUserId] = adminUserId;
}
recipeEnvironmentFeature.Properties[SetupConstants.AdminUserId] = adminUserId;

if (context.Properties.TryGetValue(SetupConstants.AdminUsername, out var adminUsername))
{
recipeEnvironmentFeature.Properties[SetupConstants.AdminUsername] = adminUsername;
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -180,7 +177,7 @@ private async Task<string> SetupInternalAsync(SetupContext context)
break;
}

if (context.Errors.Any())
if (context.Errors.Count > 0)
{
return null;
}
Expand All @@ -189,34 +186,31 @@ private async Task<string> SetupInternalAsync(SetupContext context)
// In theory this environment can be used to resolve any normal components by interface, and those
// components will exist entirely in isolation - no crossover between the safemode container currently in effect
// It is used to initialize the database before the recipe is run.

var shellDescriptor = new ShellDescriptor
{
Features = context.EnabledFeatures.Select(id => new ShellFeature { Id = id }).ToList()
Features = context.EnabledFeatures.Select(id => new ShellFeature(id)).ToList()
};

string executionId;

await using (var shellContext = await _shellContextFactory.CreateDescribedContextAsync(shellSettings, shellDescriptor))
{
await (await shellContext.CreateScopeAsync()).UsingServiceScopeAsync(async scope =>
{
try
{
// Create the "minimum shell descriptor"
await scope
.ServiceProvider
.GetService<IShellDescriptorManager>()
.UpdateShellDescriptorAsync(0,
shellContext.Blueprint.Descriptor.Features);
// Create the "minimum" shell descriptor.
await scope.ServiceProvider.GetService<IShellDescriptorManager>()
.UpdateShellDescriptorAsync(0, shellContext.Blueprint.Descriptor.Features);
}
catch (Exception e)
{
_logger.LogError(e, "An error occurred while initializing the datastore.");
context.Errors.Add(String.Empty, S["An error occurred while initializing the datastore: {0}", e.Message]);
return;
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
}
});

if (context.Errors.Any())
if (context.Errors.Count > 0)
{
return null;
jtkech marked this conversation as resolved.
Show resolved Hide resolved
}
Expand All @@ -228,32 +222,44 @@ await scope
await recipeExecutor.ExecuteAsync(executionId, context.Recipe, context.Properties, _applicationLifetime.ApplicationStopping);
}
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved

// Reloading the shell context as the recipe has probably updated its features
// Reloading the shell context as the recipe has probably updated its features.
await (await _shellHost.GetScopeAsync(shellSettings)).UsingAsync(async scope =>
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<SetupService>>();
var handlers = scope.ServiceProvider.GetServices<ISetupEventHandler>();

await handlers.InvokeAsync((handler, ctx) => handler.SetupAsync(ctx), context, _logger);

void reportError(string key, string message)
{
context.Errors[key] = message;
}

// Invoke modules to react to the setup event
var setupEventHandlers = scope.ServiceProvider.GetServices<ISetupEventHandler>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<SetupService>>();
#pragma warning disable CS0618 // Type or member is obsolete
await handlers.InvokeAsync((handler, ctx) => handler.Setup(ctx.Properties, reportError), context, logger);
#pragma warning restore CS0618 // Type or member is obsolete

await setupEventHandlers.InvokeAsync((handler, context) => handler.Setup(
context.Properties,
reportError
), context, logger);
if (context.Errors.Count > 0)
{
await handlers.InvokeAsync((handler) => handler.FailedAsync(context), _logger);
}
});

if (context.Errors.Any())
if (context.Errors.Count > 0)
{
return executionId;
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
}

// Update the shell state
// Update the shell state.
await _shellHost.UpdateShellSettingsAsync(shellSettings.AsRunning());

await (await _shellHost.GetScopeAsync(shellSettings)).UsingAsync(async scope =>
{
var handlers = scope.ServiceProvider.GetServices<ISetupEventHandler>();

await handlers.InvokeAsync((handler) => handler.SucceededAsync(), _logger);
});

return executionId;
}
}
Expand Down