Browse Source

Optional services and improved plugin loading.

pull/349/head
Sebastian Stehle 7 years ago
parent
commit
4b80525d92
  1. 3
      extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs
  2. 16
      extensions/Squidex.Extensions/SquidexExtensions.cs
  3. 12
      src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs
  4. 2
      src/Squidex.Infrastructure/Plugins/IWebPlugin.cs
  5. 69
      src/Squidex.Infrastructure/Plugins/PluginManager.cs
  6. 69
      src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs
  7. 7
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  8. 8
      src/Squidex/Config/Domain/AssetServices.cs
  9. 2
      src/Squidex/Config/Domain/EntitiesServices.cs
  10. 4
      src/Squidex/Config/Domain/EventStoreServices.cs
  11. 4
      src/Squidex/Config/Domain/InfrastructureServices.cs
  12. 2
      src/Squidex/Config/Domain/LoggingServices.cs
  13. 24
      src/Squidex/Config/Domain/StoreServices.cs
  14. 6
      src/Squidex/Config/Domain/SubscriptionServices.cs
  15. 90
      src/Squidex/Pipeline/Plugins/PluginExtensions.cs
  16. 3
      src/Squidex/Squidex.csproj
  17. 5
      src/Squidex/WebStartup.cs
  18. 24
      src/Squidex/appsettings.json

3
extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs

@ -16,6 +16,9 @@ namespace Squidex.Extensions.Actions.Twitter
{
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.Configure<TwitterOptions>(
configuration.GetSection("twitter"));
RuleActionRegistry.Add<TweetAction>();
}
}

16
extensions/Squidex.Extensions/SquidexExtensions.cs

@ -1,16 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Reflection;
namespace Squidex.Extensions
{
public static class SquidexExtensions
{
public static readonly Assembly Assembly = typeof(SquidexExtensions).Assembly;
}
}

12
src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs

@ -126,8 +126,16 @@ namespace Squidex.Infrastructure.Log
return writer.WriteObject(nameof(exception), exception, (ctx, w) =>
{
w.WriteProperty("type", ctx.GetType().FullName);
w.WriteProperty("message", ctx.Message);
w.WriteProperty("stackTrace", ctx.StackTrace);
if (ctx.Message != null)
{
w.WriteProperty("message", ctx.Message);
}
if (ctx.StackTrace != null)
{
w.WriteProperty("stackTrace", ctx.StackTrace);
}
});
}

2
src/Squidex.Infrastructure/Plugins/IWebPlugin.cs

@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Builder;
namespace Squidex.Infrastructure.Plugins
{
public interface IWebPlugin
public interface IWebPlugin : I
{
void Configure(IApplicationBuilder app);
}

69
src/Squidex.Infrastructure/Plugins/PluginManager.cs

@ -12,35 +12,59 @@ using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.Log;
namespace Squidex.Infrastructure.Plugins
{
public sealed class PluginManager
{
private readonly HashSet<IPlugin> plugins = new HashSet<IPlugin>();
private readonly HashSet<IPlugin> loadedPlugins = new HashSet<IPlugin>();
private readonly List<(string Plugin, string Action, Exception Exception)> exceptions = new List<(string, string, Exception)>();
public void Add(Assembly assembly)
public IReadOnlyCollection<IPlugin> Plugins
{
get { return loadedPlugins; }
}
public void Add(string name, Assembly assembly)
{
Guard.NotNull(assembly, nameof(assembly));
var pluginTypes =
assembly.GetTypes()
.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract);
.Where(t => typeof(IPlugin).IsAssignableFrom(t))
.Where(t => !t.IsAbstract);
foreach (var pluginType in pluginTypes)
{
var plugin = (IPlugin)Activator.CreateInstance(pluginType);
try
{
var plugin = (IPlugin)Activator.CreateInstance(pluginType);
plugins.Add(plugin);
loadedPlugins.Add(plugin);
}
catch (Exception ex)
{
LogException(name, "Instantiating", ex);
}
}
}
public void LogException(string plugin, string action, Exception exception)
{
Guard.NotNull(plugin, nameof(plugin));
Guard.NotNull(action, nameof(action));
Guard.NotNull(exception, nameof(exception));
exceptions.Add((plugin, action, exception));
}
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
Guard.NotNull(services, nameof(services));
Guard.NotNull(configuration, nameof(configuration));
foreach (var plugin in plugins)
foreach (var plugin in loadedPlugins)
{
plugin.ConfigureServices(services, configuration);
}
@ -50,10 +74,41 @@ namespace Squidex.Infrastructure.Plugins
{
Guard.NotNull(app, nameof(app));
foreach (var plugin in plugins.OfType<IWebPlugin>())
foreach (var plugin in loadedPlugins.OfType<IWebPlugin>())
{
plugin.Configure(app);
}
}
public void Log(ISemanticLog log)
{
Guard.NotNull(log, nameof(log));
if (loadedPlugins.Count > 0 || exceptions.Count > 0)
{
var status = exceptions.Count > 0 ? "CompletedWithErrors" : "Completed";
log.LogInformation(w => w
.WriteProperty("action", "pluginsLoaded")
.WriteProperty("status", status)
.WriteArray("errors", e =>
{
foreach (var error in exceptions)
{
e.WriteObject(x => x
.WriteProperty("plugin", error.Plugin)
.WriteProperty("action", error.Action)
.WriteException(error.Exception));
}
})
.WriteArray("plugins", a =>
{
foreach (var plugin in loadedPlugins)
{
a.WriteValue(plugin.GetType().ToString());
}
}));
}
}
}
}

69
src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs

@ -1,69 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using CoreTweet;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Squidex.Extensions.Actions.Twitter;
namespace Squidex.Areas.Api.Controllers.Rules
{
public sealed class TwitterController : Controller
{
private readonly TwitterOptions twitterOptions;
public TwitterController(IOptions<TwitterOptions> twitterOptions)
{
this.twitterOptions = twitterOptions.Value;
}
public sealed class TokenRequest
{
public string PinCode { get; set; }
public string RequestToken { get; set; }
public string RequestTokenSecret { get; set; }
}
[HttpGet]
[Route("rules/twitter/auth")]
public async Task<IActionResult> Auth()
{
var session = await OAuth.AuthorizeAsync(twitterOptions.ClientId, twitterOptions.ClientSecret);
return Ok(new
{
session.AuthorizeUri,
session.RequestToken,
session.RequestTokenSecret
});
}
[HttpPost]
[Route("rules/twitter/token")]
public async Task<IActionResult> AuthComplete([FromBody] TokenRequest request)
{
var session = new OAuth.OAuthSession
{
ConsumerKey = twitterOptions.ClientId,
ConsumerSecret = twitterOptions.ClientSecret,
RequestToken = request.RequestToken,
RequestTokenSecret = request.RequestTokenSecret
};
var tokens = await session.GetTokensAsync(request.PinCode);
return Ok(new
{
tokens.AccessToken,
tokens.AccessTokenSecret
});
}
}
}

7
src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -12,7 +12,6 @@ using Orleans;
using Squidex.Areas.Api.Controllers.UI.Models;
using Squidex.Config;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Extensions.Actions.Twitter;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans;
using Squidex.Pipeline;
@ -22,18 +21,16 @@ namespace Squidex.Areas.Api.Controllers.UI
public sealed class UIController : ApiController
{
private readonly MyUIOptions uiOptions;
private readonly TwitterOptions twitterOptions;
private readonly IGrainFactory grainFactory;
public UIController(ICommandBus commandBus,
IOptions<MyUIOptions> uiOptions,
IOptions<TwitterOptions> twitterOptions,
IGrainFactory grainFactory)
: base(commandBus)
{
this.uiOptions = uiOptions.Value;
this.grainFactory = grainFactory;
this.twitterOptions = twitterOptions.Value;
}
/// <summary>
@ -55,8 +52,6 @@ namespace Squidex.Areas.Api.Controllers.UI
result.Value.Add("mapType", uiOptions.Map?.Type ?? "OSM");
result.Value.Add("mapKey", uiOptions.Map?.GoogleMaps?.Key);
result.Value.Add("supportTwitterAction", twitterOptions.IsConfigured());
return Ok(result.Value);
}

8
src/Squidex/Config/Domain/AssetServices.cs

@ -29,14 +29,14 @@ namespace Squidex.Config.Domain
var path = config.GetRequiredValue("assetStore:folder:path");
services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService<ISemanticLog>()))
.As<IAssetStore>();
.AsOptional<IAssetStore>();
},
["GoogleCloud"] = () =>
{
var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket");
services.AddSingletonAs(c => new GoogleCloudAssetStore(bucketName))
.As<IAssetStore>();
.AsOptional<IAssetStore>();
},
["AzureBlob"] = () =>
{
@ -44,7 +44,7 @@ namespace Squidex.Config.Domain
var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName");
services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName))
.As<IAssetStore>();
.AsOptional<IAssetStore>();
},
["MongoDb"] = () =>
{
@ -64,7 +64,7 @@ namespace Squidex.Config.Domain
return new MongoGridFsAssetStore(gridFsbucket);
})
.As<IAssetStore>();
.AsOptional<IAssetStore>();
}
});

2
src/Squidex/Config/Domain/EntitiesServices.cs

@ -114,7 +114,7 @@ namespace Squidex.Config.Domain
.As<ITagGenerator<CreateAsset>>();
services.AddSingletonAs<JintScriptEngine>()
.As<IScriptEngine>();
.AsOptional<IScriptEngine>();
services.AddCommandPipeline();
services.AddBackupHandlers();

4
src/Squidex/Config/Domain/EventStoreServices.cs

@ -39,7 +39,7 @@ namespace Squidex.Config.Domain
return new MongoEventStore(mongDatabase, c.GetRequiredService<IEventNotifier>());
})
.As<IEventStore>();
.AsOptional<IEventStore>();
},
["GetEventStore"] = () =>
{
@ -53,7 +53,7 @@ namespace Squidex.Config.Domain
.As<IEventStoreConnection>();
services.AddSingletonAs(c => new GetEventStore(connection, c.GetRequiredService<IJsonSerializer>(), eventStorePrefix, eventStoreProjectionHost))
.As<IEventStore>();
.AsOptional<IEventStore>();
services.AddHealthChecks()
.AddCheck<GetEventStoreHealthCheck>("EventStore", tags: new[] { "node" });

4
src/Squidex/Config/Domain/InfrastructureServices.cs

@ -62,10 +62,10 @@ namespace Squidex.Config.Domain
.As<IActionContextAccessor>();
services.AddSingletonAs<DefaultUserResolver>()
.As<IUserResolver>();
.AsOptional<IUserResolver>();
services.AddSingletonAs<AssetUserPictureStore>()
.As<IUserPictureStore>();
.AsOptional<IUserPictureStore>();
services.AddSingletonAs<DefaultXmlRepository>()
.As<IXmlRepository>();

2
src/Squidex/Config/Domain/LoggingServices.cs

@ -62,7 +62,7 @@ namespace Squidex.Config.Domain
.As<IAppLogStore>();
services.AddSingletonAs<NoopLogStore>()
.As<ILogStore>();
.AsOptional<ILogStore>();
}
}
}

24
src/Squidex/Config/Domain/StoreServices.cs

@ -78,32 +78,32 @@ namespace Squidex.Config.Domain
.As<IMigration>();
services.AddSingletonAs<MongoUsageRepository>()
.As<IUsageRepository>();
.AsOptional<IUsageRepository>();
services.AddSingletonAs<MongoRuleEventRepository>()
.As<IRuleEventRepository>();
.AsOptional<IRuleEventRepository>();
services.AddSingletonAs<MongoHistoryEventRepository>()
.As<IHistoryEventRepository>();
.AsOptional<IHistoryEventRepository>();
services.AddSingletonAs<MongoPersistedGrantStore>()
.As<IPersistedGrantStore>();
.AsOptional<IPersistedGrantStore>();
services.AddSingletonAs<MongoRoleStore>()
.As<IRoleStore<IdentityRole>>();
.AsOptional<IRoleStore<IdentityRole>>();
services.AddSingletonAs<MongoUserStore>()
.As<IUserStore<IdentityUser>>()
.As<IUserFactory>();
.AsOptional<IUserStore<IdentityUser>>()
.AsOptional<IUserFactory>();
services.AddSingletonAs<MongoAssetRepository>()
.As<IAssetRepository>()
.As<ISnapshotStore<AssetState, Guid>>();
.AsOptional<IAssetRepository>()
.AsOptional<ISnapshotStore<AssetState, Guid>>();
services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetRequiredService<IAppProvider>(), c.GetRequiredService<IJsonSerializer>()))
.As<IContentRepository>()
.As<ISnapshotStore<ContentState, Guid>>()
.As<IEventConsumer>();
.AsOptional<IContentRepository>()
.AsOptional<ISnapshotStore<ContentState, Guid>>()
.AsOptional<IEventConsumer>();
}
});

6
src/Squidex/Config/Domain/SubscriptionServices.cs

@ -23,13 +23,13 @@ namespace Squidex.Config.Domain
services.AddSingletonAs(c => c.GetRequiredService<IOptions<MyUsageOptions>>()?.Value?.Plans.OrEmpty());
services.AddSingletonAs<ConfigAppPlansProvider>()
.As<IAppPlansProvider>();
.AsOptional<IAppPlansProvider>();
services.AddSingletonAs<NoopAppPlanBillingManager>()
.As<IAppPlanBillingManager>();
.AsOptional<IAppPlanBillingManager>();
services.AddSingletonAs<NoopUserEvents>()
.As<IUserEvents>();
.AsOptional<IUserEvents>();
}
}
}

90
src/Squidex/Pipeline/Plugins/PluginExtensions.cs

@ -6,13 +6,15 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using McMaster.NETCore.Plugins;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Extensions;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Plugins;
namespace Squidex.Pipeline.Plugins
@ -29,50 +31,94 @@ namespace Squidex.Pipeline.Plugins
if (options.Plugins != null)
{
foreach (var pluginPath in options.Plugins)
foreach (var path in options.Plugins)
{
PluginLoader plugin = null;
if (pluginPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
{
plugin = PluginLoader.CreateFromAssemblyFile(pluginPath, SharedTypes);
}
else
{
plugin = PluginLoader.CreateFromConfigFile(pluginPath, SharedTypes);
}
var plugin = LoadPlugin(path);
if (plugin != null)
{
var pluginAssembly = plugin.LoadDefaultAssembly();
try
{
var pluginAssembly = plugin.LoadDefaultAssembly();
AddParts(mvcBuilder, pluginAssembly);
AddParts(mvcBuilder, pluginAssembly);
var relatedAssemblies = pluginAssembly.GetCustomAttributes<RelatedAssemblyAttribute>();
foreach (var relatedAssembly in RelatedAssemblyAttribute.GetRelatedAssemblies(pluginAssembly, false))
{
AddParts(mvcBuilder, relatedAssembly);
}
foreach (var relatedAssembly in relatedAssemblies)
pluginManager.Add(path, pluginAssembly);
}
catch (Exception ex)
{
var assembly = plugin.LoadAssembly(relatedAssembly.AssemblyFileName);
AddParts(mvcBuilder, assembly);
pluginManager.LogException(path, "LoadingAssembly", ex);
}
pluginManager.Add(pluginAssembly);
}
else
{
pluginManager.LogException(path, "LoadingPlugin", new FileNotFoundException($"Cannot find plugin at {path}"));
}
}
}
pluginManager.Add(SquidexExtensions.Assembly);
pluginManager.ConfigureServices(mvcBuilder.Services, configuration);
mvcBuilder.Services.AddSingleton(pluginManager);
}
private static PluginLoader LoadPlugin(string pluginPath)
{
var fullPath = GetPaths(pluginPath);
foreach (var candidate in GetPaths(pluginPath))
{
if (candidate.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase))
{
return PluginLoader.CreateFromAssemblyFile(candidate.FullName, SharedTypes);
}
if (candidate.Extension.Equals(".json", StringComparison.OrdinalIgnoreCase))
{
return PluginLoader.CreateFromConfigFile(candidate.FullName, SharedTypes);
}
}
return null;
}
private static IEnumerable<FileInfo> GetPaths(string pluginPath)
{
var candidate = new FileInfo(Path.GetFullPath(pluginPath));
if (candidate.Exists)
{
yield return candidate;
}
if (!Path.IsPathRooted(pluginPath))
{
candidate = new FileInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), pluginPath));
if (candidate.Exists)
{
yield return candidate;
}
}
}
public static void UsePlugins(this IApplicationBuilder app)
{
var pluginManager = app.ApplicationServices.GetRequiredService<PluginManager>();
pluginManager.Configure(app);
var log = app.ApplicationServices.GetService<ISemanticLog>();
if (log != null)
{
pluginManager.Log(log);
}
}
private static void AddParts(IMvcBuilder mvcBuilder, Assembly assembly)

3
src/Squidex/Squidex.csproj

@ -61,9 +61,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.3.1" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="2.0.1" />
<PackageReference Include="Ben.BlockingDetector" Version="0.0.3" />
<PackageReference Include="EventStore.ClientAPI.NetCore" Version="4.1.0.23" />
<PackageReference Include="IdentityServer4" Version="2.3.2" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.7.0" />
@ -96,7 +94,6 @@
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="4.0.12" />
<PackageReference Include="Squidex.ClientLibrary" Version="2.8.0" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.6" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

5
src/Squidex/WebStartup.cs

@ -78,8 +78,6 @@ namespace Squidex
config.GetSection("translations:deepL"));
services.Configure<ReadonlyOptions>(
config.GetSection("mode"));
services.Configure<TwitterOptions>(
config.GetSection("twitter"));
services.Configure<RobotsTxtOptions>(
config.GetSection("robots"));
services.Configure<GCHealthCheckOptions>(
@ -100,9 +98,10 @@ namespace Squidex
services.Configure<MyNewsOptions>(
config.GetSection("news"));
services.AddHostedService<InitializerHost>();
var provider = services.AddAndBuildOrleans(configuration, afterServices =>
{
afterServices.AddHostedService<InitializerHost>();
afterServices.AddHostedService<MigratorHost>();
});

24
src/Squidex/appsettings.json

@ -12,18 +12,25 @@
*/
"baseUrl": "http://localhost:5000",
/*7
/*
* Set it to true to redirect the user from http to https permanently.
*/
"enforceHttps": false
},
/*
* Define optional paths to plugins.
*/
"plugins": [
"Squidex.Extensions.dll"
],
"etags": {
/*
* Set to true, to use strong etags.
*/
"strong": false
},
"strong": false
},
"ui": {
/*
@ -313,17 +320,6 @@
"privacyUrl": "https://squidex.io/privacy"
},
"twitter": {
/*
* The client id for twitter.
*/
"clientId": "QZhb3HQcGCvE6G8yNNP9ksNet",
/*
* The client secret for twitter.
*/
"clientSecret": "Pdu9wdN72T33KJRFdFy1w4urBKDRzIyuKpc0OItQC2E616DuZD"
},
"news": {
/*
* The app name where the news are stored.

Loading…
Cancel
Save