如何构建动态命令对象?

How to build a dynamic command object?

我会尽量说清楚。

  1. 一个插件架构使用反射和2个Attributes和一个抽象class:
    PluginEntryAttribute(Targets.Assembly, typeof(MyPlugin))
    PluginImplAttribute(Targets.Class, ...)
    abstract class Plugin
  2. 命令通过接口和委托路由到插件:
    例如:public delegate TTarget Command<TTarget>(object obj);
  3. 使用以 Command<> 作为目标的扩展方法,CommandRouter 在正确的目标接口上执行委托:
    例如:
public static TResult Execute<TTarget, TResult>(this Command<TTarget> target, Func<TTarget, TResult> func) {
     return CommandRouter.Default.Execute(func);
}

将这些放在一起,我有一个 class 硬编码命令委托,如下所示:

public class Repositories {
     public static Command<IDispatchingRepository> Dispatching = (o) => { return (IDispatchingRepository)o; };
     public static Command<IPositioningRepository> Positioning = (o) => { return (IPositioningRepository)o; };
     public static Command<ISchedulingRepository> Scheduling = (o) => { return (ISchedulingRepository)o; };
     public static Command<IHistographyRepository> Histography = (o) => { return (IHistographyRepository)o; };
}

当对象要从存储库中查询时,实际执行是这样的:

var expBob = Dispatching.Execute(repo => repo.AddCustomer("Bob"));  
var actBob = Dispatching.Execute(repo => repo.GetCustomer("Bob"));  

我的问题是:如何从插件动态创建 class 作为 Repositories

我可以看出可能需要另一个属性。大致如下:

[RoutedCommand("Dispatching", typeof(IDispatchingRepository)")]
public Command<IDispatchingRepository> Dispatching = (o) => { return (IDispatchingRepository)o; };

这只是一个想法,但我不知道如何创建动态 menu 之类的 Repositories class。

为了完整性,CommandRouter.Execute(...) 方法和相关的 Dictionary<,>:

private readonly Dictionary<Type, object> commandTargets;

internal TResult Execute<TTarget, TResult>(Func<TTarget, TResult> func) {
     var result = default(TResult);

     if (commandTargets.TryGetValue(typeof(TTarget), out object target)) {
          result = func((TTarget)target);
     }

     return result;
}

好的,我不确定这是否是您要找的。我假设每个插件都包含以下定义的字段:

public Command<T> {Name} = (o) => { return (T)o; };

您提供的代码示例:

public Command<IDispatchingRepository> Dispatching = (o) => { return (IDispatchingRepository)o; };

在 .NET Core 中动态创建 class 的一种方法是使用 Microsoft.CodeAnalysis.CSharp nuget - 这是 Roslyn。

结果是使用 class 编译的程序集调用 DynamicRepositories,所有插件的所有命令字段从所有加载的 dll 到当前 AppDomain 表示为静态 public 字段.

代码有 3 个主要组成部分:DynamicRepositoriesBuildInfo class、GetDynamicRepositoriesBuildInfo 方法和 LoadDynamicRepositortyIntoAppDomain 方法。

DynamicRepositoriesBuildInfo - 来自插件的命令字段信息和动态复杂化期间需要加载的所有程序集。这将是定义 Command 类型和 Command 类型的通用参数的程序集(例如:IDispatchingRepository

GetDynamicRepositoriesBuildInfo 方法 - 通过扫描加载的程序集 PluginEntryAttributePluginImplAttribute.

使用反射创建 DynamicRepositoriesBuildInfo

LoadDynamicRepositortyIntoAppDomain 方法 - DynamicRepositoriesBuildInfo 它使用单个 public class App.Dynamic.DynamicRepositories

创建名为 DynamicRepository.dll 的程序集

这是代码

public class DynamicRepositoriesBuildInfo
{
 public IReadOnlyCollection<Assembly> ReferencesAssemblies { get; }
    public IReadOnlyCollection<FieldInfo> PluginCommandFieldInfos { get; }

    public DynamicRepositoriesBuildInfo(
        IReadOnlyCollection<Assembly> referencesAssemblies,
        IReadOnlyCollection<FieldInfo> pluginCommandFieldInfos)
    {
        this.ReferencesAssemblies = referencesAssemblies;
        this.PluginCommandFieldInfos = pluginCommandFieldInfos;
    }
}


private static DynamicRepositoriesBuildInfo GetDynamicRepositoriesBuildInfo()
    {
    var pluginCommandProperties = (from a in AppDomain.CurrentDomain.GetAssemblies()
                                   let entryAttr = a.GetCustomAttribute<PluginEntryAttribute>()
                                   where entryAttr != null
                                   from t in a.DefinedTypes
                                   where t == entryAttr.PluginType
                                   from p in t.GetFields(BindingFlags.Public | BindingFlags.Instance)
                                   where p.FieldType.GetGenericTypeDefinition() == typeof(Command<>)
                                   select p).ToList();

    var referenceAssemblies = pluginCommandProperties
        .Select(x => x.DeclaringType.Assembly)
        .ToList();

    referenceAssemblies.AddRange(
        pluginCommandProperties
        .SelectMany(x => x.FieldType.GetGenericArguments())
        .Select(x => x.Assembly)
    );

    var buildInfo = new DynamicRepositoriesBuildInfo(
        pluginCommandFieldInfos: pluginCommandProperties,
        referencesAssemblies: referenceAssemblies.Distinct().ToList()
    );

    return buildInfo;
}

private static Assembly LoadDynamicRepositortyIntoAppDomain()
        {
            var buildInfo = GetDynamicRepositoriesBuildInfo();

            var csScriptBuilder = new StringBuilder();
            csScriptBuilder.AppendLine("using System;");
            csScriptBuilder.AppendLine("namespace App.Dynamic");
            csScriptBuilder.AppendLine("{");
            csScriptBuilder.AppendLine("    public class DynamicRepositories");
            csScriptBuilder.AppendLine("    {");
            foreach (var commandFieldInfo in buildInfo.PluginCommandFieldInfos)
            {
                var commandNamespaceStr = commandFieldInfo.FieldType.Namespace;
                var commandTypeStr = commandFieldInfo.FieldType.Name.Split('`')[0];
                var commandGenericArgStr = commandFieldInfo.FieldType.GetGenericArguments().Single().FullName;
                var commandFieldNameStr = commandFieldInfo.Name;

                csScriptBuilder.AppendLine($"public {commandNamespaceStr}.{commandTypeStr}<{commandGenericArgStr}> {commandFieldNameStr} => (o) => ({commandGenericArgStr})o;");
            }

            csScriptBuilder.AppendLine("    }");
            csScriptBuilder.AppendLine("}");

            var sourceText = SourceText.From(csScriptBuilder.ToString());
            var parseOpt = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3);
            var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, parseOpt);
            var references = new List<MetadataReference>
            {
                MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
                MetadataReference.CreateFromFile(typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly.Location),
            };

            references.AddRange(buildInfo.ReferencesAssemblies.Select(a => MetadataReference.CreateFromFile(a.Location)));

            var compileOpt = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
                    optimizationLevel: OptimizationLevel.Release,
                    assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default);

            var compilation = CSharpCompilation.Create(
                    "DynamicRepository.dll",
                    new[] { syntaxTree },
                    references: references,
                    options: compileOpt);

            using (var memStream = new MemoryStream())
            {
                var result = compilation.Emit(memStream);
                if (result.Success)
                {
                    var assembly = AppDomain.CurrentDomain.Load(memStream.ToArray());

                    return assembly;
                }
                else
                {
                    throw new ArgumentException();
                }
            }
        }

代码的执行方式

var assembly = LoadDynamicRepositortyIntoAppDomain();
var type = assembly.GetType("App.Dynamic.DynamicRepositories");

type 变量表示已编译的 class,其中所有插件命令都作为 public 静态字段。一旦你开始使用动态代码编译/构建,你就失去了所有的类型安全。如果您需要从 type 变量执行一些代码,您将需要反射。

所以如果你有

PluginA 
{
  public Command<IDispatchingRepository> Dispatching= (o) => ....
}

PluginB 
{
   public Command<IDispatchingRepository> Scheduling = (o) => ....
}

动态创建类型如下所示

public class DynamicRepositories 
{
    public static Command<IDispatchingRepository> Dispatching= (o) => ....
    public static Command<IDispatchingRepository> Scheduling = (o) => ....
}

这是另一种做法,不需要动态构建代码。

我假设插件框架的代码如下。请注意,我没有对摘要做出任何假设 Plugin class,因为我没有进一步的信息。

#region Plugin Framework

public delegate TTarget Command<out TTarget>(object obj);

/// <summary>
/// Abstract base class for plugins.
/// </summary>
public abstract class Plugin
{
}

#endregion

接下来,这里有两个示例插件。请注意 DynamicTarget 自定义属性,我将在下一步中对其进行描述。

#region Sample Plugin: ICustomerRepository

/// <summary>
/// Sample model class, representing a customer.
/// </summary>
public class Customer
{
    public Customer(string name)
    {
        Name = name;
    }

    public string Name { get; }
}

/// <summary>
/// Sample target interface.
/// </summary>
public interface ICustomerRepository
{
    Customer AddCustomer(string name);
    Customer GetCustomer(string name);
}

/// <summary>
/// Sample plugin.
/// </summary>
[DynamicTarget(typeof(ICustomerRepository))]
public class CustomerRepositoryPlugin : Plugin, ICustomerRepository
{
    private readonly Dictionary<string, Customer> _customers = new Dictionary<string, Customer>();

    public Customer AddCustomer(string name)
    {
        var customer = new Customer(name);
        _customers[name] = customer;
        return customer;
    }

    public Customer GetCustomer(string name)
    {
        return _customers[name];
    }
}

#endregion

#region Sample Plugin: IProductRepository

/// <summary>
/// Sample model class, representing a product.
/// </summary>
public class Product
{
    public Product(string name)
    {
        Name = name;
    }

    public string Name { get; }
}

/// <summary>
/// Sample target interface.
/// </summary>
public interface IProductRepository
{
    Product AddProduct(string name);
    Product GetProduct(string name);
}

/// <summary>
/// Sample plugin.
/// </summary>
[DynamicTarget(typeof(IProductRepository))]
public class ProductRepositoryPlugin : Plugin, IProductRepository
{
    private readonly Dictionary<string, Product> _products = new Dictionary<string, Product>();

    public Product AddProduct(string name)
    {
        var product = new Product(name);
        _products[name] = product;
        return product;
    }

    public Product GetProduct(string name)
    {
        return _products[name];
    }
}

#endregion

下面是静态 Repositories class 使用两个示例插件时的样子:

#region Static Repositories Example Class from Question

public static class Repositories
{
    public static readonly Command<ICustomerRepository> CustomerRepositoryCommand = o => (ICustomerRepository) o;
    public static readonly Command<IProductRepository> ProductRepositoryCommand = o => (IProductRepository) o;
}

#endregion

要开始对您的问题进行实际回答,这里是用于标记插件的自定义属性。此自定义属性已用于上面显示的两个示例插件。

/// <summary>
/// Marks a plugin as the target of a <see cref="Command{TTarget}" />, specifying
/// the type to be registered with the <see cref="DynamicCommands" />.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public class DynamicTargetAttribute : Attribute
{
    public DynamicTargetAttribute(Type type)
    {
        Type = type;
    }

    public Type Type { get; }
}

自定义属性在后面DynamicRepositoryclass的RegisterDynamicTargets(Assembly)中进行解析,以识别要注册的插件和类型(如ICustomerRepository)。目标已注册到如下所示的 CommandRouter

/// <summary>
/// A dynamic command repository.
/// </summary>
public static class DynamicCommands
{
    /// <summary>
    /// For all assemblies in the current domain, registers all targets marked with the
    /// <see cref="DynamicTargetAttribute" />.
    /// </summary>
    public static void RegisterDynamicTargets()
    {
        foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            RegisterDynamicTargets(assembly);
        }
    }

    /// <summary>
    /// For the given <see cref="Assembly" />, registers all targets marked with the
    /// <see cref="DynamicTargetAttribute" />.
    /// </summary>
    /// <param name="assembly"></param>
    public static void RegisterDynamicTargets(Assembly assembly)
    {
        IEnumerable<Type> types = assembly
            .GetTypes()
            .Where(type => type.CustomAttributes
                .Any(ca => ca.AttributeType == typeof(DynamicTargetAttribute)));

        foreach (Type type in types)
        {
            // Note: This assumes that we simply instantiate an instance upon registration.
            // You might have a different convention with your plugins (e.g., they might be
            // singletons accessed via an Instance or Default property). Therefore, you
            // might have to change this.
            object target = Activator.CreateInstance(type);

            IEnumerable<CustomAttributeData> customAttributes = type.CustomAttributes
                .Where(ca => ca.AttributeType == typeof(DynamicTargetAttribute));

            foreach (CustomAttributeData customAttribute in customAttributes)
            {
                CustomAttributeTypedArgument argument = customAttribute.ConstructorArguments.First();
                CommandRouter.Default.RegisterTarget((Type) argument.Value, target);
            }
        }
    }

    /// <summary>
    /// Registers the given target.
    /// </summary>
    /// <typeparam name="TTarget">The type of the target.</typeparam>
    /// <param name="target">The target.</param>
    public static void RegisterTarget<TTarget>(TTarget target)
    {
        CommandRouter.Default.RegisterTarget(target);
    }

    /// <summary>
    /// Gets the <see cref="Command{TTarget}" /> for the given <typeparamref name="TTarget" />
    /// type.
    /// </summary>
    /// <typeparam name="TTarget">The target type.</typeparam>
    /// <returns>The <see cref="Command{TTarget}" />.</returns>
    public static Command<TTarget> Get<TTarget>()
    {
        return obj => (TTarget) obj;
    }

    /// <summary>
    /// Extension method used to help dispatch the command.
    /// </summary>
    /// <typeparam name="TTarget">The type of the target.</typeparam>
    /// <typeparam name="TResult">The type of the result of the function invoked on the target.</typeparam>
    /// <param name="_">The <see cref="Command{TTarget}" />.</param>
    /// <param name="func">The function invoked on the target.</param>
    /// <returns>The result of the function invoked on the target.</returns>
    public static TResult Execute<TTarget, TResult>(this Command<TTarget> _, Func<TTarget, TResult> func)
    {
        return CommandRouter.Default.Execute(func);
    }
}

上面的实用程序 class 提供了一个简单的 Command<TTarget> Get<TTarget>() 方法,而不是动态创建属性,您可以使用该方法创建 Command<TTarget> 实例,然后在 Execute扩展方法。后一种方法最终委托给接下来显示的CommandRouter

/// <summary>
/// Command router used to dispatch commands to targets.
/// </summary>
public class CommandRouter
{
    public static readonly CommandRouter Default = new CommandRouter();

    private readonly Dictionary<Type, object> _commandTargets = new Dictionary<Type, object>();

    /// <summary>
    /// Registers a target.
    /// </summary>
    /// <typeparam name="TTarget">The type of the target instance.</typeparam>
    /// <param name="target">The target instance.</param>
    public void RegisterTarget<TTarget>(TTarget target)
    {
        _commandTargets[typeof(TTarget)] = target;
    }

    /// <summary>
    /// Registers a target instance by <see cref="Type" />.
    /// </summary>
    /// <param name="type">The <see cref="Type" /> of the target.</param>
    /// <param name="target">The target instance.</param>
    public void RegisterTarget(Type type, object target)
    {
        _commandTargets[type] = target;
    }

    internal TResult Execute<TTarget, TResult>(Func<TTarget, TResult> func)
    {
        var result = default(TResult);

        if (_commandTargets.TryGetValue(typeof(TTarget), out object target))
        {
            result = func((TTarget)target);
        }

        return result;
    }
}

#endregion

最后,这里有一些单元测试展示了上述 class 是如何工作的。

#region Unit Tests

public class DynamicCommandTests
{
    [Fact]
    public void TestUsingStaticRepository_StaticDeclaration_Success()
    {
        ICustomerRepository customerRepository = new CustomerRepositoryPlugin();
        CommandRouter.Default.RegisterTarget(customerRepository);

        Command<ICustomerRepository> command = Repositories.CustomerRepositoryCommand;

        Customer expected = command.Execute(repo => repo.AddCustomer("Bob"));
        Customer actual = command.Execute(repo => repo.GetCustomer("Bob"));

        Assert.Equal(expected, actual);
        Assert.Equal("Bob", actual.Name);
    }

    [Fact]
    public void TestUsingDynamicRepository_ManualRegistration_Success()
    {
        ICustomerRepository customerRepository = new CustomerRepositoryPlugin();
        DynamicCommands.RegisterTarget(customerRepository);

        Command<ICustomerRepository> command = DynamicCommands.Get<ICustomerRepository>();

        Customer expected = command.Execute(repo => repo.AddCustomer("Bob"));
        Customer actual = command.Execute(repo => repo.GetCustomer("Bob"));

        Assert.Equal(expected, actual);
        Assert.Equal("Bob", actual.Name);
    }

    [Fact]
    public void TestUsingDynamicRepository_DynamicRegistration_Success()
    {
        // Register all plugins, i.e., CustomerRepositoryPlugin and ProductRepositoryPlugin
        // in this test case.
        DynamicCommands.RegisterDynamicTargets();

        // Invoke ICustomerRepository methods on CustomerRepositoryPlugin target.
        Command<ICustomerRepository> customerCommand = DynamicCommands.Get<ICustomerRepository>();

        Customer expectedBob = customerCommand.Execute(repo => repo.AddCustomer("Bob"));
        Customer actualBob = customerCommand.Execute(repo => repo.GetCustomer("Bob"));

        Assert.Equal(expectedBob, actualBob);
        Assert.Equal("Bob", actualBob.Name);

        // Invoke IProductRepository methods on ProductRepositoryPlugin target.
        Command<IProductRepository> productCommand = DynamicCommands.Get<IProductRepository>();

        Product expectedHammer = productCommand.Execute(repo => repo.AddProduct("Hammer"));
        Product actualHammer = productCommand.Execute(repo => repo.GetProduct("Hammer"));

        Assert.Equal(expectedHammer, actualHammer);
        Assert.Equal("Hammer", actualHammer.Name);
    }
}

#endregion

您可以找到整个实现 here