Framework .NET Core – Compiler et exécuter dynamiquement du code C#

Le Framework .NET et le Framework .NET Core permettent de compiler et exécuter dynamiquement du code C#, qui peut être fourni par l’utilisateur ou par un logiciel, sans qu’il soit nécessaire de redémarrer le logiciel.
La première technique proposée par Microsoft pour la compilation dynamique de code C# consistait à utiliser CodeDom, présent nativement au sein du Framework .NET.
En 2014, Microsoft nous a proposé Roslyn, premier compilateur open-source du langage C#. Dans cet article, nous étudierons comment compiler et exécuter du code C# :

Le code fourni ci-dessous a été mis au point avec le Framework .NET Core 2.1.

Préparation
Dans un premier temps, vous devez installer le package NuGet nommé Microsoft.CodeAnalysis.CSharp, afin de pouvoir faire appel au compilateur Roselyn pour compiler un bloc de code C# lors de l’exécution de l’application.

Puis dans une classe, déclarer ces variables et modifier leur valeur si nécessaire :
[cc lang= »C# »]
public static readonly string nomClasse = « ClasseFonction »;
public static readonly string nomFonction = « Fonction »;
public static readonly string pathNugetCache = @ »<à modifier>\.nuget\packages »;
[/cc]

Le bloc code C# doit être incorporé dans une fonction d’une classe (ou une procédure). La variable nomClasse est le nom de cette classe et nomFonction, le nom de la fonction dans la classe.

Compilation d’un bloc de code
Voici le code permettant de compiler le bloc de code C# passé en paramètre (contenant l’implémentation d’une classe). Un assembly de type DLL est généré et son nom complet est retourné dans le paramètre de sortie nommé aPathAssembly.
Si le bloc de code C# fourni en paramètre est compilé sans erreur, alors la fonction Compiler renvoie une chaîne de caractère vide. Dans le cas contraire, elle retourne la liste des erreurs.
[cc lang= »C# »]
private static string Compiler(string aCode, out string aPathAssembly)
{
string sImplementationClasse, nameAssembly, erreursCompilation;
CSharpCompilation cSharpCompilation;
List listeReferences;
SyntaxTree syntaxTree;
CSharpCompilationOptions cSharpCompilationOptions;
EmitResult emitResult;
List listeDiagnostics;

// Initialisation.
listeReferences = new List();
listeReferences.Add(MetadataReference.CreateFromFile(Path.Combine(pathNugetCache, @ »system.diagnostics.debug\4.3.0\ref\netstandard1.3\System.Diagnostics.Debug.dll »)));
listeReferences.Add(MetadataReference.CreateFromFile(Path.Combine(pathNugetCache, @ »System.Runtime\4.3.0\ref\netstandard1.5\System.Runtime.dll »)));
listeReferences.Add(MetadataReference.CreateFromFile(Path.Combine(pathNugetCache, @ »System.Runtime.Extensions\4.3.0\ref\netstandard1.5\System.Runtime.Extensions.dll »)));
listeReferences.Add(MetadataReference.CreateFromFile($@ »{Assembly.GetEntryAssembly().Location} »));
cSharpCompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
nameAssembly = $ »{Guid.NewGuid().ToString()}.dll »;
aPathAssembly = Path.Combine(@ »c:\dll », nameAssembly);
erreursCompilation = string.Empty;

// Compilation.
syntaxTree = CSharpSyntaxTree.ParseText(aCode, encoding: Encoding.ASCII);
cSharpCompilation = CSharpCompilation.Create(Path.GetFileNameWithoutExtension(nameAssembly), new List() { syntaxTree }, listeReferences, cSharpCompilationOptions);
emitResult = cSharpCompilation.Emit(aPathAssembly);

// Erreur de compilation ?
listeDiagnostics = emitResult.Diagnostics.Where(o => o.Severity == DiagnosticSeverity.Error).ToList();
if (!emitResult.Success && listeDiagnostics.Count() > 0)
{
foreach (Diagnostic diagnostic in listeDiagnostics)
{
erreursCompilation += string.Format(« Erreur {0}) => {1}{1} », diagnostic.Id, diagnostic.GetMessage(), Environment.NewLine);
}
}

// Retour.
return erreursCompilation;
}
[/cc]

Exécution de l’assembly
La méthode Executer présentée ci-dessous utilise la réflection pour exécuter l’assembly passé en paramètre. Le tableau de données passé en paramètre contient les paramètres de la fonction précédemment compilée. Cette fonction retourne les données résultant de l’exécution de la fonction.
[cc lang= »C# »]
public static object Executer(string aPathAssembly, object[] aListeParametres)
{
// Variables locales.
Assembly assembly;
Type classeFonction;
object result;

// Initialisation.
assembly = AppDomain.CurrentDomain.Load(Assembly.LoadFrom(aPathAssembly).FullName);
classeFonction = assembly.GetType(Program.nomClasse);

// Exécution.
result = classeFonction.InvokeMember(Program.nomFonction, BindingFlags.InvokeMethod, null, null, aListeParametres);

// Retour.
return result;
}
[/cc]

Pour tester
Le bloc de code présenté ci-dessous permet d’exécuter une fonction C# fournie sous forme d’une chaîne de caractères. Cette fonction consiste à fournir le résultat de l’addition de deux nombres (i et j).
Après avoir implémenté la classe C# sous forme d’une chaîne de caractères, la fonction Compiler est appelée pour la compiler et générer l’assembly. Puis le code contenu dans cet assembly est exécuté en appelant la méthode Executer.
[cc lang= »C# »]
static void Main(string[] args)
{
// Variables locales.
string code, pathDll, resultatCompilation;
object result;

try
{
// Initialisation.
result = string.Empty;

code = « using System; » + Environment.NewLine +
« using System.Diagnostics; » + Environment.NewLine +
$ »public class {Program.nomClasse} » + Environment.NewLine +
« { » + Environment.NewLine +
$ »public static object {Program.nomFonction}(int i, int j) » + Environment.NewLine +
« { » + Environment.NewLine +
« long result = i + j; » + Environment.NewLine +
« return result; » + Environment.NewLine +
« } » + Environment.NewLine +
« public static void Main() { } » + Environment.NewLine +
« } »;

// Compilation
resultatCompilation = Program.Compiler(code, out pathDll);

if (resultatCompilation == string.Empty)
{
// Exécution.
result = Program.Executer(pathDll, new object[] { 10, 5 }).ToString();
}
else
{
// Affichage de l’erreur de compilation.
Console.WriteLine($ »Erreurs de compilation : {resultatCompilation} »);
}

// Affichage du résultat.
Console.WriteLine($ »Résultat : {result.ToString()} »);
}
catch (Exception aEx)
{
Console.WriteLine(aEx.Message);
}
finally
{
Console.ReadKey();
}
}
[/cc]

About: James RAVAILLE

Travaillant avec la plateforme Microsoft .NET depuis 2002, j'alterne les missions de formation et d'ingénierie avec cette plateforme. J'écris ce blog pour transmettre mes connaissances à tout développeur, qu'il soit débutant ou expérimenté.