From 7c814a67b30895083daf57262c4a5d370b8e5aae Mon Sep 17 00:00:00 2001 From: Pavel Vesely Date: Mon, 11 May 2020 11:05:17 +0200 Subject: [PATCH 1/3] IServerStartup implementation --- Oqtane.Client/Modules/ModuleBase.cs | 2 +- .../ApplicationBuilderExtensions.cs | 26 +++++++++++++++++++ .../Extensions/AssemblyExtensions.cs | 19 +++++++++++++- .../Extensions/OqtaneMvcBuilderExtensions.cs | 17 ++++++++++++ .../OqtaneServiceCollectionExtensions.cs | 8 +++++- .../Interfaces/IServerStartup.cs | 17 ++++++++++++ Oqtane.Server/Startup.cs | 11 ++++---- 7 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs create mode 100644 Oqtane.Server/Infrastructure/Interfaces/IServerStartup.cs diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index c81f8d10..188da769 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -9,7 +9,7 @@ using Oqtane.UI; namespace Oqtane.Modules { - public class ModuleBase : ComponentBase, IModuleControl + public abstract class ModuleBase : ComponentBase, IModuleControl { private Logger _logger; diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 00000000..d1c77301 --- /dev/null +++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Oqtane.Infrastructure; + +namespace Oqtane.Extensions +{ + public static class ApplicationBuilderExtensions + { + public static IApplicationBuilder ConfigureOqtaneAssemblies(this IApplicationBuilder app, IWebHostEnvironment env) + { + var startUps = AppDomain.CurrentDomain + .GetOqtaneAssemblies() + .SelectMany(x => x.GetInstances()); + + foreach (var startup in startUps) + { + startup.Configure(app, env); + } + + return app; + } + } +} diff --git a/Oqtane.Server/Extensions/AssemblyExtensions.cs b/Oqtane.Server/Extensions/AssemblyExtensions.cs index 443b8cc6..cc04b6da 100644 --- a/Oqtane.Server/Extensions/AssemblyExtensions.cs +++ b/Oqtane.Server/Extensions/AssemblyExtensions.cs @@ -31,7 +31,24 @@ namespace System.Reflection } return assembly.GetTypes() - .Where(t => t.GetInterfaces().Contains(interfaceType)); + //.Where(t => t.GetInterfaces().Contains(interfaceType)); + .Where(x => interfaceType.IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract); + } + + public static IEnumerable GetInstances(this Assembly assembly) where T : class + { + if (assembly is null) + { + throw new ArgumentNullException(nameof(assembly)); + } + var type = typeof(T); + var list = assembly.GetTypes() + .Where(x => type.IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract && !x.IsGenericType); + + foreach (var type1 in list) + { + if (Activator.CreateInstance(type1) is T instance) yield return instance; + } } public static bool IsOqtaneAssembly(this Assembly assembly) diff --git a/Oqtane.Server/Extensions/OqtaneMvcBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneMvcBuilderExtensions.cs index fdb801df..b3f489bf 100644 --- a/Oqtane.Server/Extensions/OqtaneMvcBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneMvcBuilderExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Oqtane.Infrastructure; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection @@ -30,6 +31,22 @@ namespace Microsoft.Extensions.DependencyInjection } } } + + return mvcBuilder; + } + + + public static IMvcBuilder ConfigureOqtaneMvc(this IMvcBuilder mvcBuilder) + { + var startUps = AppDomain.CurrentDomain + .GetOqtaneAssemblies() + .SelectMany(x => x.GetInstances()); + + foreach (var startup in startUps) + { + startup.ConfigureMvc(mvcBuilder); + } + return mvcBuilder; } } diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 5c093f48..64bffb8a 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -53,11 +53,17 @@ namespace Microsoft.Extensions.DependencyInjection services.AddSingleton(hostedServiceType, serviceType); } } + + var startUps = assembly.GetInstances(); + foreach (var startup in startUps) + { + startup.ConfigureServices(services); + } } - return services; } + private static void LoadAssemblies() { var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); diff --git a/Oqtane.Server/Infrastructure/Interfaces/IServerStartup.cs b/Oqtane.Server/Infrastructure/Interfaces/IServerStartup.cs new file mode 100644 index 00000000..bbdff5d6 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Interfaces/IServerStartup.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Oqtane.Infrastructure +{ + public interface IServerStartup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + void ConfigureServices(IServiceCollection services); + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + void Configure(IApplicationBuilder app, IWebHostEnvironment env); + void ConfigureMvc(IMvcBuilder mvcBuilder); + } +} + diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index de4c89a7..d3ba352b 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; +using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Security; @@ -187,14 +188,13 @@ namespace Oqtane services.AddTransient(); services.AddTransient(); - // load the external assemblies into the app domain + // load the external assemblies into the app domain, install services services.AddOqtaneParts(); services.AddMvc() + .AddNewtonsoftJson() .AddOqtaneApplicationParts() // register any Controllers from custom modules - .AddNewtonsoftJson(); - - + .ConfigureOqtaneMvc(); // any additional configuration from IStart classes. services.AddSwaggerGen(c => { @@ -217,14 +217,12 @@ namespace Oqtane // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } - app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseBlazorFrameworkFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); - app.UseSwagger(); app.UseSwaggerUI(c => { @@ -237,6 +235,7 @@ namespace Oqtane endpoints.MapControllers(); endpoints.MapFallbackToPage("/_Host"); }); + app.ConfigureOqtaneAssemblies(env); } } } From 7f157582cc14d1e70de9ff69c6b63d867784898d Mon Sep 17 00:00:00 2001 From: Mike Casas Date: Mon, 11 May 2020 13:47:12 -0400 Subject: [PATCH 2/3] Update IModuleControl.cs Added additional comments. --- Oqtane.Client/Modules/IModuleControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/IModuleControl.cs b/Oqtane.Client/Modules/IModuleControl.cs index c4f2fdee..ee5120ba 100644 --- a/Oqtane.Client/Modules/IModuleControl.cs +++ b/Oqtane.Client/Modules/IModuleControl.cs @@ -7,6 +7,6 @@ namespace Oqtane.Modules SecurityAccessLevel SecurityAccessLevel { get; } // defines the security access level for this control - defaults to View string Title { get; } // title to display for this control - defaults to module title string Actions { get; } // allows for routing by configuration rather than by convention ( comma delimited ) - defaults to using component file name - bool UseAdminContainer { get; } // container for embedding module control - defaults to true + bool UseAdminContainer { get; } // container for embedding module control - defaults to true. false will suppress the default modal UI popup behavior and render the component in the page. } } From da73d519d78d6b8a123800aff9a8d40359ff74e1 Mon Sep 17 00:00:00 2001 From: Pavel Vesely Date: Mon, 11 May 2020 13:10:22 +0200 Subject: [PATCH 3/3] IClientStartup implementation --- Oqtane.Client/Program.cs | 35 +++++++++++++++---- .../Controllers/ModuleDefinitionController.cs | 10 ++++++ .../OqtaneServiceCollectionExtensions.cs | 15 ++++++-- Oqtane.Server/Startup.cs | 9 +++-- .../Extensions/AssemblyExtensions.cs | 11 ++++++ Oqtane.Shared/Interfaces/IClientStartup.cs | 11 ++++++ Oqtane.Shared/Oqtane.Shared.csproj | 1 + 7 files changed, 80 insertions(+), 12 deletions(-) rename {Oqtane.Server => Oqtane.Shared}/Extensions/AssemblyExtensions.cs (84%) create mode 100644 Oqtane.Shared/Interfaces/IClientStartup.cs diff --git a/Oqtane.Client/Program.cs b/Oqtane.Client/Program.cs index 74fa8de4..26bac719 100644 --- a/Oqtane.Client/Program.cs +++ b/Oqtane.Client/Program.cs @@ -4,8 +4,10 @@ using System.Threading.Tasks; using Oqtane.Services; using System.Reflection; using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Net.Http.Json; using Oqtane.Modules; using Oqtane.Shared; using Oqtane.Providers; @@ -19,10 +21,9 @@ namespace Oqtane.Client { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("app"); + HttpClient httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)}; - builder.Services.AddSingleton( - new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) } - ); + builder.Services.AddSingleton(httpClient); builder.Services.AddOptions(); // register auth services @@ -57,14 +58,16 @@ namespace Oqtane.Client builder.Services.AddScoped(); builder.Services.AddScoped(); + await LoadClientAssemblies(httpClient); + // dynamically register module contexts and repository services Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { - Type[] implementationtypes = assembly.GetTypes() - .Where(item => item.GetInterfaces().Contains(typeof(IService))) - .ToArray(); - foreach (Type implementationtype in implementationtypes) + var implementationTypes = assembly.GetTypes() + .Where(item => item.GetInterfaces().Contains(typeof(IService))); + + foreach (Type implementationtype in implementationTypes) { Type servicetype = Type.GetType(implementationtype.AssemblyQualifiedName.Replace(implementationtype.Name, "I" + implementationtype.Name)); if (servicetype != null) @@ -76,9 +79,27 @@ namespace Oqtane.Client builder.Services.AddScoped(implementationtype, implementationtype); // no interface defined for service } } + + assembly.GetInstances() + .ToList() + .ForEach(x => x.ConfigureServices(builder.Services)); } await builder.Build().RunAsync(); } + + private static async Task LoadClientAssemblies(HttpClient http) + { + var list = await http.GetFromJsonAsync>($"/~/api/ModuleDefinition/load"); + // get list of loaded assemblies on the client ( in the client-side hosting module the browser client has its own app domain ) + var assemblyList = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList(); + foreach (var name in list) + { + if (assemblyList.Contains(name)) continue; + // download assembly from server and load + var bytes = await http.GetByteArrayAsync($"/~/api/ModuleDefinition/load/{name}.dll"); + Assembly.Load(bytes); + } + } } } diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs index e1b9534c..fcd9dcd8 100644 --- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs +++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs @@ -170,6 +170,16 @@ namespace Oqtane.Controllers return null; } } + // GET api//load/assembyname + [HttpGet("load")] + public List Load() + { + var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies(); + var list = AppDomain.CurrentDomain.GetOqtaneClientAssemblies().Select(a => a.GetName().Name).ToList(); + var deps = assemblies.SelectMany(a => a.GetReferencedAssemblies()).Distinct(); + list.AddRange(deps.Where(a=>a.Name.EndsWith(".oqtane",StringComparison.OrdinalIgnoreCase)).Select(a=>a.Name)); + return list; + } // POST api/?moduleid=x [HttpPost] diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 64bffb8a..3e778869 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -8,21 +8,23 @@ using Microsoft.Extensions.Hosting; using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Modules; +using Oqtane.Services; using Oqtane.Shared; +using Oqtane.UI; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection { public static class OqtaneServiceCollectionExtensions { - public static IServiceCollection AddOqtaneParts(this IServiceCollection services) + public static IServiceCollection AddOqtaneParts(this IServiceCollection services, Runtime runtime) { LoadAssemblies(); - services.AddOqtaneServices(); + services.AddOqtaneServices(runtime); return services; } - private static IServiceCollection AddOqtaneServices(this IServiceCollection services) + private static IServiceCollection AddOqtaneServices(this IServiceCollection services, Runtime runtime) { if (services is null) { @@ -59,6 +61,13 @@ namespace Microsoft.Extensions.DependencyInjection { startup.ConfigureServices(services); } + + if (runtime == Runtime.Server) + { + assembly.GetInstances() + .ToList() + .ForEach(x => x.ConfigureServices(services)); + } } return services; } diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index d3ba352b..34f73ffe 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -19,7 +19,8 @@ using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Security; using Oqtane.Services; -using Oqtane.Shared; +using Oqtane.Shared; +using Oqtane.UI; namespace Oqtane { @@ -27,6 +28,7 @@ namespace Oqtane { public IConfigurationRoot Configuration { get; } private string _webRoot; + private Runtime _runtime; public Startup(IWebHostEnvironment env) { @@ -34,6 +36,9 @@ namespace Oqtane .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); Configuration = builder.Build(); + + _runtime = (Configuration.GetSection("Runtime").Value == "WebAssembly") ? Runtime.WebAssembly : Runtime.Server; + _webRoot = env.WebRootPath; AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(env.ContentRootPath, "Data")); } @@ -189,7 +194,7 @@ namespace Oqtane services.AddTransient(); // load the external assemblies into the app domain, install services - services.AddOqtaneParts(); + services.AddOqtaneParts(_runtime); services.AddMvc() .AddNewtonsoftJson() diff --git a/Oqtane.Server/Extensions/AssemblyExtensions.cs b/Oqtane.Shared/Extensions/AssemblyExtensions.cs similarity index 84% rename from Oqtane.Server/Extensions/AssemblyExtensions.cs rename to Oqtane.Shared/Extensions/AssemblyExtensions.cs index cc04b6da..e97de64f 100644 --- a/Oqtane.Server/Extensions/AssemblyExtensions.cs +++ b/Oqtane.Shared/Extensions/AssemblyExtensions.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Oqtane.Services; using Oqtane.Shared; // ReSharper disable once CheckNamespace @@ -35,6 +36,11 @@ namespace System.Reflection .Where(x => interfaceType.IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract); } + public static IEnumerable GetTypes(this Assembly assembly) + { + return assembly.GetTypes(typeof(T)); + } + public static IEnumerable GetInstances(this Assembly assembly) where T : class { if (assembly is null) @@ -65,5 +71,10 @@ namespace System.Reflection { return appDomain.GetAssemblies().Where(a => a.IsOqtaneAssembly()); } + public static IEnumerable GetOqtaneClientAssemblies(this AppDomain appDomain) + { + return appDomain.GetOqtaneAssemblies() + .Where(a => a.GetTypes().Any()); + } } } diff --git a/Oqtane.Shared/Interfaces/IClientStartup.cs b/Oqtane.Shared/Interfaces/IClientStartup.cs new file mode 100644 index 00000000..c063431c --- /dev/null +++ b/Oqtane.Shared/Interfaces/IClientStartup.cs @@ -0,0 +1,11 @@ + +using Microsoft.Extensions.DependencyInjection; + +namespace Oqtane.Services +{ + public interface IClientStartup + { + // This method gets called by the runtime. Use this method to add services to the container. + void ConfigureServices(IServiceCollection services); + } +} diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index c50111cd..eca4e4ee 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -18,6 +18,7 @@ +