How to build a dynamic command object?
Asked Answered
C

2

7

I'll try to make this as clear as possible.

  1. A Plugin architecture using reflection and 2 Attributes and an abstract class:
    PluginEntryAttribute(Targets.Assembly, typeof(MyPlugin))
    PluginImplAttribute(Targets.Class, ...)
    abstract class Plugin
  2. Commands are routed to a plugin via an interface and a delegate:
    Ex: public delegate TTarget Command<TTarget>(object obj);
  3. Using extension methods with Command<> as the target, a CommandRouter executes the delegate on the correct target interface:
    Ex:
public static TResult Execute<TTarget, TResult>(this Command<TTarget> target, Func<TTarget, TResult> func) {
     return CommandRouter.Default.Execute(func);
}

Putting this together, I have a class hard-coded with the command delegates like so:

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

When an object wants to query from the repository, practical execution looks like this:

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

My question is this: how can I create such a class as Repositories dynamically from the plugins?

I can see the possibility that another attribute might be necessary. Something along the lines of:

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

This is just an idea, but I'm at a loss as to how I'd still create a dynamic menu of sorts like the Repositories class.

For completeness, the CommandRouter.Execute(...) method and related 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;
}
Colt answered 19/1, 2020 at 21:21 Comment(9)
Use AppDomain.CurrentDomain.GetAssemblies().SelectMany( asbly => asbly.GetTypes()) to get the repository types (see garywoodfine.com/get-c-classes-implementing-interface for more on using reflection to find implementing types).Elston
P.S. Seems funny that your Execute extension method does not use the 'target' parameter.Elston
@sjb-sjb: Execute is an extension method for the generic type. We don't actually need the target so much as the type information for the command delegate. Sort of like having a stub interface.Colt
@Colt Are you targeting .NET Core or .NET Framework?Corinthians
@PavelAnikhouski: .NET Core ... updated tags.Colt
If you want to keep the repository structure, you could use CodeDom link in connection with an attribute (marking the interface and providing a command name) to just generate the Repository class dynamically. This may collide with dynamic Plugin loading, though.Leverick
Is the Repositories class will aways have those 4 commands and you just need to build correct ones with expected interfaces depending on the plugins ... or the Repositories class will be really dynamic and you can have another 15 commands at different scenarios - if so how would you use the Repositories if you don't know its structure in advance ? If you are going full reflection on this one i think marker interfaces for the repositories class used across the different plugin assemblies i way better then going full reflection.Thunderhead
Also could you elaborate more on the structure - how are the Plugin abstract class, Plugin Impl. Attribute and the repositories interfaces connected ? In classic plugin arch. you will have marker interfaces which implemntation you can swap from different dlls, so i am little lost. Any additional info appreciated :)Thunderhead
@vasiloreshenski: I don't have interfaces because everything operates on commands. Thus, the need to build a command structure from plugins. PluginEntry decorates an assembly. One plugin per assembly. Thus PluginImpl is the plugin itself. abstract Plugin does provide some standard information/methods.Colt
T
1

OK, i am not sure if this is what you are looking for. I am assuming that each plugin contains field of the following definition:

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

example from code provided by you:

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

One way to dynamically create class in .NET Core is by using the Microsoft.CodeAnalysis.CSharp nuget - this is Roslyn.

The result is compiled assembly with class called DynamicRepositories having all command fields from all plugins from all loaded dlls into the current AppDomain represented as static public fields.

The code has 3 main components: DynamicRepositoriesBuildInfo class, GetDynamicRepositoriesBuildInfo method and LoadDynamicRepositortyIntoAppDomain method.

DynamicRepositoriesBuildInfo - information for the command fields from the plugins and all assemblies needed to be loaded during the dynamic complication. This will be the assemblies which defines the Command type and the generic arguments of the Command type (ex: IDispatchingRepository)

GetDynamicRepositoriesBuildInfo method - creates DynamicRepositoriesBuildInfo using reflection by scanning loaded assemblies for the PluginEntryAttribute and PluginImplAttribute.

LoadDynamicRepositortyIntoAppDomain method - DynamicRepositoriesBuildInfo it creates assembly called DynamicRepository.dll with single public class App.Dynamic.DynamicRepositories

Here is the code

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();
                }
            }
        }

This is how to execute the code

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

The type variable represents the compiled class which has all the plugin commands as public static fields. You are loosing all type safety once you start using dynamic code compilation / building. If you need to execute some code from the type variable you will need reflection.

So if you have

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

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

the dynamically create type will look like this

public class DynamicRepositories 
{
    public static Command<IDispatchingRepository> Dispatching= (o) => ....
    public static Command<IDispatchingRepository> Scheduling = (o) => ....
}
Thunderhead answered 26/1, 2020 at 16:26 Comment(0)
D
1

Here's another take, which does not require building code dynamically.

I'm assuming the following code for the plugin framework. Note that I did not make any assumptions regarding the abstract Plugin class, because I had no further information.

#region Plugin Framework

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

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

#endregion

Next, here are two sample plugins. Note the DynamicTarget custom attributes, which I will describe in the next step.

#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

Here's what your static Repositories class would look like with the two sample plugins:

#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

To begin the actual answer to your question here's the custom attribute used to mark the plugins. This custom attribute has been used on the two example plugins shown above.

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

The custom attribute is parsed in the RegisterDynamicTargets(Assembly) of the following DynamicRepository class to identify the plugins and the types (e.g., ICustomerRepository) to be registered. The targets are registered with the CommandRouter shown below.

/// <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);
    }
}

Instead of dynamically creating properties, the above utility class offers a simple Command<TTarget> Get<TTarget>() method, with which you can create the Command<TTarget> instance, which is then used in the Execute extension method. The latter method finally delegates to the CommandRouter shown next.

/// <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

Finally, here are a few unit tests showing how the above classes work.

#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

You can find the whole implementation here.

Dreary answered 26/1, 2020 at 19:12 Comment(3)
Intriguing. Is Roslyn portable? I want to be able to install on Linux. Also, I'm trying to find a way to add bounty for this answer. I think both are excellent options.Colt
I've never used Roslyn but I believe it is usable on Linux.Dreary
I'd assume so if I can implement this via .Net Core. w^^wColt

© 2022 - 2024 — McMap. All rights reserved.