Created
November 12, 2017 11:11
-
-
Save sdcondon/338b64dbc3863c9962e89dc399e04e99 to your computer and use it in GitHub Desktop.
Basic example of using MEF and the Roslyn scripting API to create a scriptable extension system.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.CodeAnalysis.CSharp.Scripting; | |
using Microsoft.CodeAnalysis.Scripting; | |
using System; | |
using System.Collections.Generic; | |
using System.Composition; | |
using System.Composition.Hosting; | |
using System.Composition.Hosting.Core; | |
using System.IO; | |
using System.Linq; | |
using System.Reflection; | |
/// <summary> | |
/// Basic example of using MEF and the Roslyn scripting API to create a scriptable extension system. | |
/// After compilation, try adding a file called print.csx with the content 'Console.WriteLine(Arguments[0])' | |
/// to the directory containing the exe. | |
/// </summary> | |
class Program | |
{ | |
private CompositionHost container; // NB: IDisposable - but won't bother dealing with this gracefully here. | |
private Program() | |
{ | |
var containerConfiguration = new ContainerConfiguration() | |
.WithProvider(new ScriptedCommandProvider(AppContext.BaseDirectory)); | |
container = containerConfiguration.CreateContainer(); | |
container.SatisfyImports(this); | |
} | |
[ImportMany] | |
public IEnumerable<Lazy<ICommand, CommandMetadata>> Commands | |
{ | |
get; | |
set; | |
} | |
public static void Main(string[] args) | |
{ | |
Program p = new Program(); // Composition is performed in the constructor | |
Console.WriteLine("Loaded commands: {0}", string.Join(", ", p.Commands.Select(c => c.Metadata.Keyword))); | |
string commandLine = null; | |
do | |
{ | |
Console.Write("Enter Command: "); | |
commandLine = Console.ReadLine(); | |
var commandLineParts = commandLine.Split(' '); | |
var command = p.Commands.SingleOrDefault(x => commandLineParts[0].StartsWith(x.Metadata.Keyword)); | |
if (command != null) | |
{ | |
command.Value.Execute(commandLineParts.Skip(1).ToArray()); | |
} | |
else | |
{ | |
Console.WriteLine("[Command '{0}' not recognized]", commandLineParts[0]); | |
} | |
} | |
while (!string.IsNullOrWhiteSpace(commandLine)); | |
} | |
} | |
/// <summary> | |
/// Interface that we want to allow scripted implementations of. Obviously this'd get more | |
/// comlicated for interfaces with more than a single method.. | |
/// </summary> | |
public interface ICommand | |
{ | |
/// <summary> | |
/// Execute the command, doing some useful work. | |
/// </summary> | |
/// <param name="arguments">The arguments to pass to the command.</param> | |
void Execute(string[] arguments); | |
} | |
/// <summary> | |
/// Class for our console app to use to interpret the MEF metadata accompanying the scripted commands. | |
/// </summary> | |
public sealed class CommandMetadata | |
{ | |
/// <summary> | |
/// Initializes a new instance of the <see cref="CommandMetadata"/> class from an MEF-provided metadata dictionary. | |
/// </summary> | |
/// <param name="metadata">Metadata dictionary.</param> | |
public CommandMetadata(IDictionary<string, object> metadata) | |
{ | |
Keyword = (string)metadata[nameof(Keyword)]; | |
} | |
/// <summary> | |
/// The keyword used to invoke the command. | |
/// </summary> | |
public string Keyword { get; } | |
} | |
/// <summary> | |
/// MEF ExportDescriptorProvider that provides descriptors for ICommand implementations from scripts found in a particular directory. | |
/// </summary> | |
public sealed class ScriptedCommandProvider : ExportDescriptorProvider | |
{ | |
private string scriptDirectory; | |
/// <summary> | |
/// Initializes a new instance of the <see cref="ScriptProvider{T}"/> class. | |
/// </summary> | |
/// <param name="scriptDirectory">The directory in which all the scripts can be found.</param> | |
public ScriptedCommandProvider(string scriptDirectory) | |
{ | |
this.scriptDirectory = scriptDirectory; | |
} | |
/// <inheritdoc /> | |
public override IEnumerable<ExportDescriptorPromise> GetExportDescriptors(CompositionContract contract, DependencyAccessor descriptorAccessor) | |
{ | |
if (contract.ContractType != typeof(ICommand)) | |
{ | |
return NoExportDescriptors; | |
} | |
return Directory | |
.EnumerateFiles(scriptDirectory, "*.csx") | |
.Select(p => new ExportDescriptorPromise( | |
contract, | |
p, | |
false, | |
NoDependencies, | |
_ => MakeScriptedCommandDescriptor(p))); | |
} | |
private ExportDescriptor MakeScriptedCommandDescriptor(string scriptPath) | |
{ | |
return ExportDescriptor.Create( | |
(context, operation) => new ScriptedCommand(scriptPath), | |
new Dictionary<string, object>() | |
{ | |
{ nameof(CommandMetadata.Keyword), Path.GetFileNameWithoutExtension(scriptPath) } | |
}); | |
} | |
} | |
/// <summary> | |
/// Implementation of <see cref="ICommand"/> that gets its logic from a script. | |
/// </summary> | |
internal sealed class ScriptedCommand : ICommand | |
{ | |
private Script script; | |
/// <summary> | |
/// Initializes a new instance of the <see cref="ScriptedCommand"/> class. | |
/// </summary> | |
/// <param name="path">The full path of the file where the logic of the command can be found.</param> | |
public ScriptedCommand(string path) | |
{ | |
var opts = ScriptOptions.Default | |
.AddReferences(typeof(object).GetTypeInfo().Assembly) | |
.AddReferences(typeof(ICommand).GetTypeInfo().Assembly) | |
.AddImports("System"); | |
script = CSharpScript.Create(File.ReadAllText(path), opts, typeof(ScriptedCommandGlobals)); | |
} | |
/// <inheritdoc /> | |
public void Execute(string[] arguments) | |
{ | |
// NB: As implemented, first invocation will be slow as it compiles. | |
// Can mitigate by invoking Compile - and this could happen asynchronously - | |
// but Lazy nature of MEF ImportMany means compilation would have to be initiated | |
// by the provider, not in the command object itself. Which wouldn't make for as | |
// graceful an example, so not bothering here. | |
script.RunAsync(new ScriptedCommandGlobals() { Arguments = arguments }); | |
} | |
} | |
/// <summary> | |
/// Globals object for scripted commands - essentially the interface between the app and the scripts. | |
/// </summary> | |
public class ScriptedCommandGlobals | |
{ | |
public string[] Arguments { get; set; } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment