Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to build a dynamic command object?

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;
}
like image 575
IAbstract Avatar asked Jan 19 '20 21:01

IAbstract


People also ask

How do you create a dynamic object?

You can create custom dynamic objects by using the classes in the System. Dynamic namespace. For example, you can create an ExpandoObject and specify the members of that object at run time. You can also create your own type that inherits the DynamicObject class.

What is Dynamic C#?

In C# 4.0, a new type is introduced that is known as a dynamic type. It is used to avoid the compile-time type checking. The compiler does not check the type of the dynamic type variable at compile time, instead of this, the compiler gets the type at the run time.


2 Answers

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) => ....
}
like image 195
vasil oreshenski Avatar answered Oct 13 '22 18:10

vasil oreshenski


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.

like image 1
Thomas Barnekow Avatar answered Oct 13 '22 18:10

Thomas Barnekow