Quindi il mio approccio con questa sfida ha significato immergersi attraverso un intero carico di sorgenti di riferimento per comprendere i diversi tipi disponibili per Roslyn.
Per anteporre la soluzione finale, permette di creare l'interfaccia del modulo, metteremo questo in Contracts.dll
:
public interface IModule
{
public int Order { get; }
public string Name { get; }
public Version Version { get; }
IEnumerable<ServiceDescriptor> GetServices();
}
public interface IModuleProvider
{
IEnumerable<IModule> GetModules();
}
E mettiamoci anche definire il provider di base:
public abstract class ModuleProviderBase
{
private readonly List<IModule> _modules = new List<IModule>();
protected ModuleProviderBase()
{
Setup();
}
public IEnumerable<IModule> GetModules()
{
return _modules.OrderBy(m => m.Order);
}
protected void AddModule<T>() where T : IModule, new()
{
var module = new T();
_modules.Add(module);
}
protected virtual void Setup() { }
}
Ora, in questo architettura, il modulo non è in realtà qualcosa di più di un descrittore, quindi non dovrebbe prendere delle dipendenze, ma esprime semplicemente i servizi che offre.
Ora un modulo di esempio potrebbe essere simile, in DefaultLogger.dll
:
public class DefaultLoggerModule : ModuleBase
{
public override int Order { get { return ModuleOrder.Level3; } }
public override IEnumerable<ServiceDescriptor> GetServices()
{
yield return ServiceDescriptor.Instance<ILoggerFactory>(new DefaultLoggerFactory());
}
}
ho lasciato fuori l'attuazione di ModuleBase
per brevità.
Ora, nel mio progetto web, aggiungo un riferimento a Contracts.dll
e DefaultLogger.dll
, e quindi aggiungere la seguente implementazione del mio fornitore del modulo:
public partial class ModuleProvider : ModuleProviderBase { }
E ora, mio ICompileModule
:
using T = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree;
using F = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using K = Microsoft.CodeAnalysis.CSharp.SyntaxKind;
public class DiscoverModulesCompileModule : ICompileModule
{
private static MethodInfo GetMetadataMethodInfo = typeof(PortableExecutableReference)
.GetMethod("GetMetadata", BindingFlags.NonPublic | BindingFlags.Instance);
private static FieldInfo CachedSymbolsFieldInfo = typeof(AssemblyMetadata)
.GetField("CachedSymbols", BindingFlags.NonPublic | BindingFlags.Instance);
private ConcurrentDictionary<MetadataReference, string[]> _cache
= new ConcurrentDictionary<MetadataReference, string[]>();
public void AfterCompile(IAfterCompileContext context) { }
public void BeforeCompile(IBeforeCompileContext context)
{
// Firstly, I need to resolve the namespace of the ModuleProvider instance in this current compilation.
string ns = GetModuleProviderNamespace(context.Compilation.SyntaxTrees);
// Next, get all the available modules in assembly and compilation references.
var modules = GetAvailableModules(context.Compilation).ToList();
// Map them to a collection of statements
var statements = modules.Select(m => F.ParseStatement("AddModule<" + module + ">();")).ToList();
// Now, I'll create the dynamic implementation as a private class.
var cu = F.CompilationUnit()
.AddMembers(
F.NamespaceDeclaration(F.IdentifierName(ns))
.AddMembers(
F.ClassDeclaration("ModuleProvider")
.WithModifiers(F.TokenList(F.Token(K.PartialKeyword)))
.AddMembers(
F.MethodDeclaration(F.PredefinedType(F.Token(K.VoidKeyword)), "Setup")
.WithModifiers(
F.TokenList(
F.Token(K.ProtectedKeyword),
F.Token(K.OverrideKeyword)))
.WithBody(F.Block(statements))
)
)
)
.NormalizeWhitespace(indentation("\t"));
var tree = T.Create(cu);
context.Compilation = context.Compilation.AddSyntaxTrees(tree);
}
// Rest of implementation, described below
}
Essenzialmente questo modulo fa alcuni passaggi;
1 - Risolve lo spazio dei nomi dell'istanza ModuleProvider
nel progetto Web, ad es. SampleWeb
.
2 - Individua tutti i moduli disponibili tramite i riferimenti, che vengono restituiti come una raccolta di stringhe, ad es. new [] { "SampleLogger.DefaultLoggerModule"}
3 - Conversione quelli alle dichiarazioni del genere AddModule<SampleLogger.DefaultLoggerModule>();
4 - Creare un partial
implementazione di ModuleProvider
che stiamo aggiungendo alla nostra classifica:
namespace SampleWeb
{
partial class ModuleProvider
{
protected override void Setup()
{
AddModule<SampleLogger.DefaultLoggerModule>();
}
}
}
Quindi, come ho scoperto i moduli disponibili? Ci sono tre fasi:
1 - I complessi di riferimento (ad esempio, quelli forniti attraverso NuGet)
2 - Le compilazioni riferimento (ad esempio, i progetti riferimento nella soluzione).
3 - Le dichiarazioni del modulo nella compilation corrente.
E per ciascuna compilazione di riferimento, ripetiamo quanto sopra.
private IEnumerable<string> GetAvailableModules(Compilation compilation)
{
var list = new List<string>();
string[] modules = null;
// Get the available references.
var refs = compilation.References.ToList();
// Get the assembly references.
var assemblies = refs.OfType<PortableExecutableReference>().ToList();
foreach (var assemblyRef in assemblies)
{
if (!_cache.TryGetValue(assemblyRef, out modules))
{
modules = GetAssemblyModules(assemblyRef);
_cache.AddOrUpdate(assemblyRef, modules, (k, v) => modules);
list.AddRange(modules);
}
else
{
// We've already included this assembly.
}
}
// Get the compilation references
var compilations = refs.OfType<CompilationReference>().ToList();
foreach (var compliationRef in compilations)
{
if (!_cache.TryGetValue(compilationRef, out modules))
{
modules = GetAvailableModules(compilationRef.Compilation).ToArray();
_cache.AddOrUpdate(compilationRef, modules, (k, v) => modules);
list.AddRange(modules);
}
else
{
// We've already included this compilation.
}
}
// Finally, deal with modules in the current compilation.
list.AddRange(GetModuleClassDeclarations(compilation));
return list;
}
moduli Quindi, per ottenere assembly di riferimento:
private IEnumerable<string> GetAssemblyModules(PortableExecutableReference reference)
{
var metadata = GetMetadataMethodInfo.Invoke(reference, nul) as AssemblyMetadata;
if (metadata != null)
{
var assemblySymbol = ((IEnumerable<IAssemblySymbol>)CachedSymbolsFieldInfo.GetValue(metadata)).First();
// Only consider our assemblies? Sample*?
if (assemblySymbol.Name.StartsWith("Sample"))
{
var types = GetTypeSymbols(assemblySymbol.GlobalNamespace).Where(t => Filter(t));
return types.Select(t => GetFullMetadataName(t)).ToArray();
}
}
return Enumerable.Empty<string>();
}
Dobbiamo fare una piccola riflessione qui come il metodo GetMetadata
non è pubblico, e più tardi, quando afferriamo i metadati, il campo CachedSymbols
è anche non pubblico, quindi più riflesso lì. In termini di identificazione di ciò che è disponibile, dobbiamo prendere il IEnumerable<IAssemblySymbol>
dalla proprietà CachedSymbols
. Questo ci fornisce tutti i simboli memorizzati nella cache nell'assieme di riferimento. Roslyn fa questo per noi, in modo che possiamo poi abusarne:
private IEnumerable<ITypeSymbol> GetTypeSymbols(INamespaceSymbol ns)
{
foreach (var typeSymbols in ns.GetTypeMembers().Where(t => !t.Name.StartsWith("<")))
{
yield return typeSymbol;
}
foreach (var namespaceSymbol in ns.GetNamespaceMembers())
{
foreach (var typeSymbol in GetTypeSymbols(ns))
{
yield return typeSymbol;
}
}
}
Il metodo GetTypeSymbols
cammina attraverso gli spazi dei nomi e scopre tutti i tipi.Abbiamo poi catena risultato al metodo del filtro, che garantisce implementa nostra interfaccia richiesto:
private bool Filter(ITypeSymbol symbol)
{
return symbol.IsReferenceType
&& !symbol.IsAbstract
&& !symbol.IsAnonymousType
&& symbol.AllInterfaces.Any(i => i.GetFullMetadataName(i) == "Sample.IModule");
}
Con GetFullMetadataName
essendo un metodo di utilità:
private static string GetFullMetadataName(INamespaceOrTypeSymbol symbol)
{
ISymbol s = symbol;
var builder = new StringBuilder(s.MetadataName);
var last = s;
while (!!IsRootNamespace(s))
{
builder.Insert(0, '.');
builder.Insert(0, s.MetadataName);
s = s.ContainingSymbol;
}
return builder.ToString();
}
private static bool IsRootNamespace(ISymbol symbol)
{
return symbol is INamespaceSymbol && ((INamespaceSymbol)symbol).IsGlobalNamespace;
}
Next up, dichiarazioni di modulo nella compilazione corrente:
private IEnumerable<string> GetModuleClassDeclarations(Compilation compilation)
{
var trees = compilation.SyntaxTrees.ToArray();
var models = trees.Select(compilation.GetSemanticModel(t)).ToArray();
for (var i = 0; i < trees.Length; i++)
{
var tree = trees[i];
var model = models[i];
var types = tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().ToList();
foreach (var type in types)
{
var symbol = model.GetDeclaredSymbol(type) as ITypeSymbol;
if (symbol != null && Filter(symbol))
{
yield return GetFullMetadataName(symbol);
}
}
}
}
E questo è davvero! Così, ora in fase di compilazione, la mia volontà ICompileModule
:
- Scopri tutti i moduli disponibili
- Implementare un override del mio metodo
ModuleProvider.Setup
con tutti i moduli di riferimento noti.
Questo significa che posso aggiungere la mia startup:
public class Startup
{
public ModuleProvider ModuleProvider = new ModuleProvider();
public void ConfigureServices(IServiceCollection services)
{
var descriptors = ModuleProvider.GetModules() // Ordered
.SelectMany(m => m.GetServices());
// Apply descriptors to services.
}
public void Configure(IApplicationBuilder app)
{
var modules = ModuleProvider.GetModules(); // Ordered.
// Startup code.
}
}
Massively over-ingegnerizzato, abbastanza complesso, ma un pò impressionante credo!
Avete considerato di utilizzare una classe statica globale per gestire queste istanze in runtime? (utilizzando un attributo personalizzato/piccolo snippet nella classe base) –