Как построить объект команды Dynami c? - PullRequest
7 голосов
/ 20 января 2020

Я постараюсь сделать это как можно более понятным.

  1. A Плагин архитектура с использованием отражения и 2 Attribute с и абстрактного класса:
    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);
}

Собирая все вместе, у меня есть класс, жестко запрограммированный с делегатами команд, например:

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"));  

У меня такой вопрос: как я могу динамически создать такой класс как Repositories из плагинов?

Я вижу вероятность того, что может понадобиться другой атрибут. Что-то вроде:

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

Это просто идея, но я в растерянности относительно того, как бы я все еще создал динамическое c меню сортов как класс Repositories.

Для полноты, метод 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;
}

Ответы [ 2 ]

1 голос
/ 26 января 2020

Вот еще один вариант, который не требует динамического построения кода.

Я предполагаю следующий код для инфраструктуры плагинов. Обратите внимание, что я не делал никаких предположений относительно абстрактного класса Plugin, потому что у меня не было никакой дополнительной информации.

#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

Вот как будет выглядеть ваш класс stati c Repositories с двумя примерами плагинов:

#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; }
}

Пользовательский атрибут анализируется в RegisterDynamicTargets(Assembly) следующего класса DynamicRepository для идентификации подключаемых модулей и типов (например, 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);
    }
}

Вместо динамического создания свойств, вышеуказанный служебный класс предлагает простой метод 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

Наконец, вот несколько модульных тестов, показывающих, как работают вышеупомянутые классы.

#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

Вы можете найти вся реализация здесь .

1 голос
/ 26 января 2020

ОК, я не уверен, что это то, что вы ищете. Я предполагаю, что каждый плагин содержит поле следующего определения:

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

пример из предоставленного вами кода:

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

Один из способов динамического создания класса в. NET Core это с помощью Microsoft.CodeAnalysis.CSharp nuget - это Roslyn.

В результате получается скомпилированная сборка с классом DynamicRepositories, имеющим все поля команд из всех плагинов из всех загруженных библиотек в текущий AppDomain представлен как stati c publi c fields.

Код имеет 3 основных компонента: DynamicRepositoriesBuildInfo класс, GetDynamicRepositoriesBuildInfo метод и LoadDynamicRepositortyIntoAppDomain метод.

DynamicRepositoriesBuildInfo - информация для полей команд из плагинов и всех сборок, которые необходимо было загрузить во время усложнения динамического c. Это будут сборки, которые определяют тип Command и аргументы generi c типа Command (например: IDispatchingRepository)

GetDynamicRepositoriesBuildInfo - создает DynamicRepositoriesBuildInfo с использованием отражения от сканирование загруженных сборок для метода PluginEntryAttribute и PluginImplAttribute.

LoadDynamicRepositortyIntoAppDomain - DynamicRepositoriesBuildInfo создает сборку DynamicRepository.dll с одним publi c классом App.Dynamic.DynamicRepositories

Вот код

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 представляет скомпилированный класс, который имеет все команды плагина как publi c stati c полей. Вы теряете безопасность всех типов, как только начинаете использовать динамическую компиляцию / сборку кода c. Если вам нужно выполнить некоторый код из переменной 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) => ....
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...