diff --git a/Oqtane.Client/AssemblyInfo.cs b/Oqtane.Client/AssemblyInfo.cs new file mode 100644 index 00000000..d598bfb9 --- /dev/null +++ b/Oqtane.Client/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Microsoft.Extensions.Localization; + +[assembly: RootNamespace("Oqtane")] diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 2dff5419..596de1a6 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -70,7 +70,7 @@ } - @((MarkupString) _message) + @if (_image != string.Empty) { @@ -91,9 +91,9 @@ private string _progressbarid = string.Empty; private string _filter = "*"; private bool _haseditpermission = false; - private string _message = string.Empty; private string _image = string.Empty; private string _guid; + private ModuleMessage _message = new ModuleMessage(); [Parameter] public string Id { get; set; } // optional - for setting the id of the FileManager component for accessibility @@ -205,7 +205,6 @@ private async Task FolderChanged(ChangeEventArgs e) { - _message = string.Empty; try { FolderId = int.Parse((string)e.Value); @@ -217,13 +216,14 @@ catch (Exception ex) { await logger.LogError(ex, "Error Loading Files {Error}", ex.Message); - _message = "
Error Loading Files
"; + + _message.Message = "Error Loading Files"; + _message.Type = MessageType.Error; } } private async Task FileChanged(ChangeEventArgs e) { - _message = string.Empty; FileId = int.Parse((string)e.Value); await SetImage(); @@ -273,7 +273,10 @@ if (result == string.Empty) { await logger.LogInformation("File Upload Succeeded {Files}", upload); - _message = "
File Upload Succeeded
"; + + _message.Message = "File Upload Succeeded"; + _message.Type = MessageType.Success; + await GetFiles(); if (upload.Length == 1) @@ -290,30 +293,36 @@ else { await logger.LogError("File Upload Failed For {Files}", result.Replace(",", ", ")); - _message = "
File Upload Failed
"; + + _message.Message = "File Upload Failed"; + _message.Type = MessageType.Error; } } catch (Exception ex) { await logger.LogError(ex, "File Upload Failed {Error}", ex.Message); - _message = "
File Upload Failed
"; + + _message.Message = "File Upload Failed"; + _message.Type = MessageType.Error; } } else { - _message = "
You Have Not Selected A File To Upload
"; + _message.Message = "You Have Not Selected A File To Upload"; + _message.Type = MessageType.Warning; } } private async Task DeleteFile() { - _message = string.Empty; - try { await FileService.DeleteFileAsync(FileId); await logger.LogInformation("File Deleted {File}", FileId); - _message = "
File Deleted
"; + + _message.Message = "File Deleted"; + _message.Type = MessageType.Success; + await GetFiles(); FileId = -1; await SetImage(); @@ -322,7 +331,9 @@ catch (Exception ex) { await logger.LogError(ex, "Error Deleting File {File} {Error}", FileId, ex.Message); - _message = "
Error Deleting File
"; + + _message.Message = "Error Deleting File"; + _message.Type = MessageType.Error; } } diff --git a/Oqtane.Client/Modules/Controls/RichTextEditor.razor b/Oqtane.Client/Modules/Controls/RichTextEditor.razor index 5eeb17a2..0d6d0a4e 100644 --- a/Oqtane.Client/Modules/Controls/RichTextEditor.razor +++ b/Oqtane.Client/Modules/Controls/RichTextEditor.razor @@ -8,7 +8,7 @@ @if (_filemanagervisible) { - @((MarkupString)_message) +
}
@@ -85,7 +85,7 @@ private FileManager _fileManager; private string _content = string.Empty; private string _original = string.Empty; - private string _message = string.Empty; + private ModuleMessage _message = new ModuleMessage(); [Parameter] public string Content { get; set; } @@ -144,7 +144,6 @@ public void CloseFileManager() { _filemanagervisible = false; - _message = string.Empty; StateHasChanged(); } @@ -189,17 +188,16 @@ var interop = new RichTextEditorInterop(JSRuntime); await interop.InsertImage(_editorElement, ContentUrl(fileid)); _filemanagervisible = false; - _message = string.Empty; } else { - _message = "
You Must Select An Image To Insert
"; + _message.Message = "You Must Select An Image To Insert"; + _message.Type = MessageType.Warning; } } else { _filemanagervisible = true; - _message = string.Empty; } StateHasChanged(); } diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index bff51215..515bf8f5 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -32,6 +32,7 @@ + diff --git a/Oqtane.Client/Program.cs b/Oqtane.Client/Program.cs index 8a1fd2b8..912720d9 100644 --- a/Oqtane.Client/Program.cs +++ b/Oqtane.Client/Program.cs @@ -1,19 +1,18 @@ -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using Microsoft.Extensions.DependencyInjection; -using System.Threading.Tasks; -using Oqtane.Services; -using System.Reflection; -using System; +using System; using System.Collections.Generic; +using System.IO; +using System.IO.Compression; using System.Linq; using System.Net.Http; -using System.Net.Http.Json; -using Oqtane.Modules; -using Oqtane.Shared; -using Oqtane.Providers; +using System.Reflection; +using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Authorization; -using System.IO.Compression; -using System.IO; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Modules; +using Oqtane.Providers; +using Oqtane.Shared; +using Oqtane.Services; namespace Oqtane.Client { @@ -28,6 +27,9 @@ namespace Oqtane.Client builder.Services.AddSingleton(httpClient); builder.Services.AddOptions(); + // Register localization services + builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); + // register auth services builder.Services.AddAuthorizationCore(); builder.Services.AddScoped(); @@ -101,8 +103,8 @@ namespace Oqtane.Client // asemblies and debug symbols are packaged in a zip file using (ZipArchive archive = new ZipArchive(new MemoryStream(zip))) { - Dictionary dlls = new Dictionary(); - Dictionary pdbs = new Dictionary(); + var dlls = new Dictionary(); + var pdbs = new Dictionary(); foreach (ZipArchiveEntry entry in archive.Entries) { @@ -115,7 +117,15 @@ namespace Oqtane.Client switch (Path.GetExtension(entry.Name)) { case ".dll": - dlls.Add(entry.Name, file); + // Loads the stallite assemblies early + if (entry.Name.EndsWith(Constants.StalliteAssemblyExtension)) + { + Assembly.Load(file); + } + else + { + dlls.Add(entry.Name, file); + } break; case ".pdb": pdbs.Add(entry.Name, file); diff --git a/Oqtane.Client/UI/Installer.razor b/Oqtane.Client/UI/Installer.razor index 204197f6..11c0eec7 100644 --- a/Oqtane.Client/UI/Installer.razor +++ b/Oqtane.Client/UI/Installer.razor @@ -119,7 +119,7 @@


- @((MarkupString) _message) +
@@ -135,7 +135,7 @@ private string _hostPassword = ""; private string _confirmPassword = ""; private string _hostEmail = ""; - private string _message = ""; + private ModuleMessage _message = new ModuleMessage(); private string _integratedSecurityDisplay = "display: none;"; private string _loadingDisplay = "display: none;"; @@ -201,13 +201,15 @@ } else { - _message = "
" + installation.Message + "
"; + _message.Message = installation.Message; + _message.Type = MessageType.Error; _loadingDisplay = "display: none;"; } } else { - _message = "
Please Enter All Fields And Ensure Passwords Match And Are Greater Than 5 Characters In Length
"; + _message.Message = "Please Enter All Fields And Ensure Passwords Match And Are Greater Than 5 Characters In Length"; + _message.Type = MessageType.Error; } } diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 50b45710..d3529a98 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -1,17 +1,16 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Oqtane.Models; -using Oqtane.Shared; -using Oqtane.Infrastructure; -using System; +using System; using System.IO; using System.Reflection; using System.Linq; using System.IO.Compression; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Oqtane.Infrastructure; +using Oqtane.Models; using Oqtane.Modules; +using Oqtane.Shared; using Oqtane.Themes; -using System.Diagnostics; namespace Oqtane.Controllers { @@ -21,12 +20,14 @@ namespace Oqtane.Controllers private readonly IConfigurationRoot _config; private readonly IInstallationManager _installationManager; private readonly IDatabaseManager _databaseManager; + private readonly ILocalizationManager _localizationManager; - public InstallationController(IConfigurationRoot config, IInstallationManager installationManager, IDatabaseManager databaseManager) + public InstallationController(IConfigurationRoot config, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager) { _config = config; _installationManager = installationManager; _databaseManager = databaseManager; + _localizationManager = localizationManager; } // POST api/ @@ -73,6 +74,21 @@ namespace Oqtane.Controllers // get list of assemblies which should be downloaded to browser var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies(); var list = assemblies.Select(a => a.GetName().Name).ToList(); + var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + + // Get the satellite assemblies + foreach (var culture in _localizationManager.GetSupportedCultures()) + { + if (culture == Constants.DefaultCulture) + { + continue; + } + + foreach (var resourceFile in Directory.EnumerateFiles(Path.Combine(binFolder, culture))) + { + list.Add(Path.Combine(culture, Path.GetFileNameWithoutExtension(resourceFile))); + } + } // get module and theme dependencies foreach (var assembly in assemblies) @@ -96,7 +112,6 @@ namespace Oqtane.Controllers } // create zip file containing assemblies and debug symbols - string binfolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); byte[] zipfile; using (var memoryStream = new MemoryStream()) { @@ -106,17 +121,17 @@ namespace Oqtane.Controllers foreach (string file in list) { entry = archive.CreateEntry(file + ".dll"); - using (var filestream = new FileStream(Path.Combine(binfolder, file + ".dll"), FileMode.Open, FileAccess.Read)) + using (var filestream = new FileStream(Path.Combine(binFolder, file + ".dll"), FileMode.Open, FileAccess.Read)) using (var entrystream = entry.Open()) { filestream.CopyTo(entrystream); } // include debug symbols ( we may want to consider restricting this to only host users or when running in debug mode for performance ) - if (System.IO.File.Exists(Path.Combine(binfolder, file + ".pdb"))) + if (System.IO.File.Exists(Path.Combine(binFolder, file + ".pdb"))) { entry = archive.CreateEntry(file + ".pdb"); - using (var filestream = new FileStream(Path.Combine(binfolder, file + ".pdb"), FileMode.Open, FileAccess.Read)) + using (var filestream = new FileStream(Path.Combine(binFolder, file + ".pdb"), FileMode.Open, FileAccess.Read)) using (var entrystream = entry.Open()) { filestream.CopyTo(entrystream); diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs index d1c77301..67676d16 100644 --- a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs @@ -1,8 +1,10 @@ using System; +using System.Globalization; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; using Oqtane.Infrastructure; namespace Oqtane.Extensions @@ -22,5 +24,22 @@ namespace Oqtane.Extensions return app; } + + public static IApplicationBuilder UseOqtaneLocalization(this IApplicationBuilder app) + { + var localizationManager = app.ApplicationServices.GetService(); + var defaultCulture = localizationManager.GetDefaultCulture(); + var supportedCultures = localizationManager.GetSupportedCultures(); + + CultureInfo.CurrentUICulture = new CultureInfo(defaultCulture); + + app.UseRequestLocalization(options => { + options.SetDefaultCulture(defaultCulture) + .AddSupportedUICultures(supportedCultures) + .AddSupportedUICultures(supportedCultures); + }); + + return app; + } } } diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index f49d189a..4090be8d 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting; using Oqtane.Infrastructure; using Oqtane.Modules; using Oqtane.Services; +using Oqtane.Shared; using Oqtane.UI; // ReSharper disable once CheckNamespace @@ -14,10 +15,12 @@ namespace Microsoft.Extensions.DependencyInjection { public static class OqtaneServiceCollectionExtensions { - public static IServiceCollection AddOqtaneParts(this IServiceCollection services, Runtime runtime) + public static IServiceCollection AddOqtane(this IServiceCollection services, Runtime runtime) { LoadAssemblies(); + LoadSatelliteAssemblies(); services.AddOqtaneServices(runtime); + return services; } @@ -119,6 +122,55 @@ namespace Microsoft.Extensions.DependencyInjection } } + private static void LoadSatelliteAssemblies() + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); + if (assemblyPath == null) + { + return; + } + + AssemblyLoadContext.Default.Resolving += ResolveDependencies; + + using (var serviceScope = ServiceActivator.GetScope()) + { + var localizationManager = serviceScope.ServiceProvider.GetService(); + foreach (var culture in localizationManager.GetSupportedCultures()) + { + if (culture == Constants.DefaultCulture) + { + continue; + } + + var assembliesFolder = new DirectoryInfo(Path.Combine(assemblyPath, culture)); + foreach (var assemblyFile in assembliesFolder.EnumerateFiles(Constants.StalliteAssemblyExtension)) + { + AssemblyName assemblyName; + try + { + assemblyName = AssemblyName.GetAssemblyName(assemblyFile.FullName); + } + catch + { + Console.WriteLine($"Not Satellite Assembly : {assemblyFile.Name}"); + continue; + } + + try + { + Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(File.ReadAllBytes(assemblyFile.FullName))); + Console.WriteLine($"Loaded : {assemblyName}"); + } + catch (Exception e) + { + Console.WriteLine($"Failed : {assemblyName}\n{e}"); + } + } + } + } + } + private static Assembly ResolveDependencies(AssemblyLoadContext context, AssemblyName name) { var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) + "\\" + name.Name + ".dll"; diff --git a/Oqtane.Server/Infrastructure/Interfaces/ILocalizationManager.cs b/Oqtane.Server/Infrastructure/Interfaces/ILocalizationManager.cs new file mode 100644 index 00000000..fa20eef4 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Interfaces/ILocalizationManager.cs @@ -0,0 +1,9 @@ +namespace Oqtane.Infrastructure +{ + public interface ILocalizationManager + { + string GetDefaultCulture(); + + string[] GetSupportedCultures(); + } +} diff --git a/Oqtane.Server/Infrastructure/Localization/LocalizationOptions.cs b/Oqtane.Server/Infrastructure/Localization/LocalizationOptions.cs new file mode 100644 index 00000000..6330130e --- /dev/null +++ b/Oqtane.Server/Infrastructure/Localization/LocalizationOptions.cs @@ -0,0 +1,9 @@ +namespace Oqtane.Infrastructure +{ + public class LocalizationOptions + { + public string DefaultCulture { get; set; } + + public string[] SupportedCultures { get; set; } + } +} diff --git a/Oqtane.Server/Infrastructure/LocalizationManager.cs b/Oqtane.Server/Infrastructure/LocalizationManager.cs new file mode 100644 index 00000000..22b4ca79 --- /dev/null +++ b/Oqtane.Server/Infrastructure/LocalizationManager.cs @@ -0,0 +1,29 @@ +using System.Collections; +using Microsoft.Extensions.Options; +using Oqtane.Shared; + +namespace Oqtane.Infrastructure +{ + public class LocalizationManager : ILocalizationManager + { + private static readonly string DefaultCulture = Constants.DefaultCulture; + private static readonly string[] SupportedCultures = new[] { DefaultCulture }; + + private readonly LocalizationOptions _localizationOptions; + + public LocalizationManager(IOptions localizationOptions) + { + _localizationOptions = localizationOptions.Value; + } + + public string GetDefaultCulture() + => string.IsNullOrEmpty(_localizationOptions.DefaultCulture) + ? DefaultCulture + : _localizationOptions.DefaultCulture; + + public string[] GetSupportedCultures() + => _localizationOptions.SupportedCultures.IsNullOrEmpty() + ? SupportedCultures + : _localizationOptions.SupportedCultures; + } +} diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 44237543..31523442 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -43,6 +43,7 @@ + diff --git a/Oqtane.Server/Pages/_Host.cshtml b/Oqtane.Server/Pages/_Host.cshtml index 0fe00957..e4b6e5a7 100644 --- a/Oqtane.Server/Pages/_Host.cshtml +++ b/Oqtane.Server/Pages/_Host.cshtml @@ -1,10 +1,17 @@ @page "/" @namespace Oqtane.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@using System.Globalization +@using Microsoft.AspNetCore.Localization @using Microsoft.Extensions.Configuration @inject IConfiguration Configuration @model Oqtane.Pages.HostModel +@{ + // Set localization cookie + var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture)); + HttpContext.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue); +} diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index ee53f340..98d43bcd 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -51,6 +51,9 @@ namespace Oqtane // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { + // Register localization services + services.AddLocalization(options => options.ResourcesPath = "Resources"); + services.AddServerSideBlazor(); // setup HttpClient for server side in a client side compatible fashion ( with auth cookie ) @@ -125,6 +128,8 @@ namespace Oqtane .AddSignInManager() .AddDefaultTokenProviders(); + services.Configure(Configuration.GetSection("Localization")); + services.Configure(options => { // Password settings @@ -187,6 +192,7 @@ namespace Oqtane services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -196,8 +202,11 @@ namespace Oqtane services.AddTransient(); services.AddTransient(); + // TODO: Check if there's a better way instead of building service provider + ServiceActivator.Configure(services.BuildServiceProvider()); + // load the external assemblies into the app domain, install services - services.AddOqtaneParts(_runtime); + services.AddOqtane(_runtime); services.AddMvc() .AddNewtonsoftJson() @@ -225,6 +234,10 @@ namespace Oqtane } // to allow install middleware it should be moved up app.ConfigureOqtaneAssemblies(env); + + // Allow oqtane localization middleware + app.UseOqtaneLocalization(); + app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseBlazorFrameworkFiles(); diff --git a/Oqtane.Server/appsettings.json b/Oqtane.Server/appsettings.json index 0ab499f0..1af87b4c 100644 --- a/Oqtane.Server/appsettings.json +++ b/Oqtane.Server/appsettings.json @@ -11,5 +11,9 @@ "DefaultTheme": "", "DefaultLayout": "", "DefaultContainer": "" + }, + "Localization": { + "DefaultCulture": "", + "SupportedCultures": [] } } \ No newline at end of file diff --git a/Oqtane.Shared/Extensions/EnumerableExtensions.cs b/Oqtane.Shared/Extensions/EnumerableExtensions.cs new file mode 100644 index 00000000..a4bdde7c --- /dev/null +++ b/Oqtane.Shared/Extensions/EnumerableExtensions.cs @@ -0,0 +1,8 @@ +namespace System.Collections +{ + public static class EnumerableExtensions + { + public static bool IsNullOrEmpty(this IEnumerable source) + => source == null || source.GetEnumerator().MoveNext() == false; + } +} diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 5a5acbaf..db62f416 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; namespace Oqtane.Shared { @@ -46,7 +47,7 @@ namespace Oqtane.Shared public const string RegisteredRole = "Registered Users"; public const string ImageFiles = "jpg,jpeg,jpe,gif,bmp,png,svg,ico"; - public const string UploadableFiles = "jpg,jpeg,jpe,gif,bmp,png,svg,ico,mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg"; + public const string UploadableFiles = "jpg,jpeg,jpe,gif,bmp,png,svg,ico,mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg,csv"; public const string ReservedDevices = "CON,NUL,PRN,COM0,COM1,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,LPT0,LPT1,LPT2,LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,CONIN$,CONOUT$"; public static readonly char[] InvalidFileNameChars = @@ -57,5 +58,9 @@ namespace Oqtane.Shared (Char) 28, (Char) 29, (Char) 30, (Char) 31, ':', '*', '?', '\\', '/' }; public static readonly string[] InvalidFileNameEndingChars = { ".", " " }; + + public static readonly string StalliteAssemblyExtension = ".resources.dll"; + + public static readonly string DefaultCulture = CultureInfo.InstalledUICulture.Name; } -} \ No newline at end of file +} diff --git a/Oqtane.Shared/Shared/ServiceActivator.cs b/Oqtane.Shared/Shared/ServiceActivator.cs new file mode 100644 index 00000000..5718d174 --- /dev/null +++ b/Oqtane.Shared/Shared/ServiceActivator.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Oqtane.Shared +{ + public static class ServiceActivator + { + private static IServiceProvider _serviceProvider = null; + + public static void Configure(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public static IServiceScope GetScope(IServiceProvider serviceProvider = null) + { + var provider = serviceProvider ?? _serviceProvider; + + return provider?.GetRequiredService().CreateScope(); + } + } +}