Mat*_*ott 15 c# metaprogramming roslyn dnx
我想在我正在开发的框架中绕过一些经典的汇编扫描技术.
所以,说我已经定义了以下合同:
public interface IModule
{
}
Run Code Online (Sandbox Code Playgroud)
这就是说Contracts.dll.
现在,如果我想发现这个接口的所有实现,我们可能会做类似以下的事情:
public IEnumerable<IModule> DiscoverModules()
{
var contractType = typeof(IModule);
var assemblies = AppDomain.Current.GetAssemblies() // Bad but will do
var types = assemblies
.SelectMany(a => a.GetExportedTypes)
.Where(t => contractType.IsAssignableFrom(t))
.ToList();
return types.Select(t => Activator.CreateInstance(t));
}
Run Code Online (Sandbox Code Playgroud)
不是一个很好的例子,但它会做.
现在,这些类型的汇编扫描技术可能完全不足,而且它们都在运行时完成,通常会影响启动性能.
在新的DNX环境中,我们可以使用ICompileModule实例作为元编程工具,因此您可以将实现捆绑ICompileModule到Compiler\Preprocess项目中的文件夹中,并让它做一些时髦的事情.
我的目标是使用一个ICompileModule实现来完成我们在运行时在编译时所做的工作.
IModuleModuleList用一个实现来调用它,该实现产生每个模块的实例.public static class ModuleList
{
public static IEnumerable<IModule>() GetModules()
{
yield return new Module1();
yield return new Module2();
}
}
Run Code Online (Sandbox Code Playgroud)
将该类添加到编译单元后,我们可以调用它并在运行时获取静态模块列表,而不必搜索所有附加的程序集.我们实际上卸载了编译器而不是运行时的工作.
鉴于我们可以通过References属性访问编译的所有引用,我无法看到如何获取任何有用的信息,例如可能访问字节代码,也许加载用于反射的程序集,或类似的东西.
思考?
思考?
是.
通常,在模块环境中,您希望根据上下文动态加载模块,或者 - 如果适用 - 来自第三方.相比之下,使用Roslyn编译器框架,您基本上可以获得编译时的这些信息,从而将模块限制为静态引用.
就在昨天,我发布了动态加载工厂的代码.这里有属性,加载DLL等的更新:GoF Factory的命名约定?.据我所知,它与你想要实现的非常相似.这种方法的好处是你可以在运行时动态加载新的DLL.如果你试试,你会发现它很快.
您还可以进一步限制您处理的装配.例如,如果您不处理mscorlib和System.*(或者甚至可能是所有GAC程序集),它当然会更快地工作.不过,正如我所说,它应该不是问题; 只扫描类型和属性是一个非常快速的过程.
好的,更多的信息和背景.
现在,你可能正在寻找一个有趣的谜题.我可以理解,玩弄技术毕竟是很有趣的.下面的答案(马修本人)将为您提供所需的所有信息.
如果您想平衡编译时代码生成与运行时解决方案的优缺点,请从我的经验中获取更多信息.
几年前,我认为让自己的C#解析器/生成器框架进行AST转换是个好主意.它与Roslyn的功能非常相似; 基本上它将整个项目转换为AST树,然后您可以对其进行规范化,生成代码,对面向方面的编程内容进行额外检查并添加新的语言结构.我最初的目标是在C#中添加对面向方面编程的支持,为此我有一些实际应用.我将为您提供详细信息,但对于此上下文,可以说基于代码生成的模块/工厂也是我尝试过的事情之一.
性能,灵活性和代码量(在非库解决方案中)是我在运行时和编译时决策之间加权决策的关键方面.让我们分解吧:
但是关于性能的说明是有序的.我在代码中使用反射不仅仅是工厂模式.我基本上有一个包含所有设计模式(以及其他许多东西)的"工具"库.举几个例子:我在运行时自动生成代码,例如工厂,责任链,装饰器,模拟,缓存/代理(以及更多).其中一些已经要求我扫描组件.
作为一个简单的经验法则,我总是使用一个属性来表示必须更改某些内容.您可以利用这个优势:通过简单地将每个类型(具有正确的程序集/命名空间的属性)存储在某个地方的单例/字典中,您可以使应用程序更快(因为您只需要扫描一次).从Microsoft扫描程序集也不是很有用.我对大型项目进行了大量测试,发现在我发现的最坏情况下,扫描在应用程序启动时增加了大约10毫秒.请注意,这只是每个appdomain实例化一次,这意味着你甚至都不会注意到它.
激活类型实际上是你将获得的唯一"真正的"性能惩罚.可以通过发出IL代码来优化该惩罚; 这真的不那么难.最终结果是它在这里没有任何区别.
总结一下,这是我的结论:
根据我的经验,尽管很多框架都希望支持即插即用架构,这些架构可以从组件的下降中受益,但实际情况是,实际上并没有完全适用的用例.
如果它不适用,您可能想要考虑不首先使用工厂模式.此外,如果它适用,我已经表明它没有真正的缺点,即:如果你正确实现它.不幸的是,我必须在此承认,我已经看到很多不好的实现.
至于它实际上并不适用的事实,我认为这只是部分正确.嵌入式数据提供程序非常常见(逻辑上遵循3层架构).我还使用工厂来连接诸如通信/ WCF API,缓存提供者和装饰器(逻辑上遵循n层架构)之类的东西.一般来说,它可用于您能想到的任何类型的提供商.
如果参数是性能损失,您基本上想要删除整个类型扫描过程.就个人而言,我将其用于大量不同的事情,最着名的是缓存,统计,日志记录和配置.此外,我认为业绩下滑可以忽略不计.
只需2美分; HTH.
因此,我应对这一挑战的方法意味着深入研究整个参考源,以了解罗斯林可以使用的不同类型。
为了给最终解决方案加前缀,让我们创建模块接口,将其放入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();
}
Run Code Online (Sandbox Code Playgroud)
让我们还定义基本提供程序:
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() { }
}
Run Code Online (Sandbox Code Playgroud)
现在,在这种架构中,该模块实际上只不过是一个描述符,因此不应该采用依赖关系,它仅表示其提供的服务。
现在,一个示例模块可能类似于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());
}
}
Run Code Online (Sandbox Code Playgroud)
ModuleBase为了简洁起见,我省略了实施。
现在,在我的Web项目中,添加对Contracts.dll和的引用DefaultLogger.dll,然后添加模块提供程序的以下实现:
public partial class ModuleProvider : ModuleProviderBase { }
Run Code Online (Sandbox Code Playgroud)
现在,我的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
}
Run Code Online (Sandbox Code Playgroud)
本质上,此模块执行一些步骤;
1-解决ModuleProviderWeb项目中实例的名称空间,例如SampleWeb。
2 -发现所有可用的模块通过引用,这些都返回字符串的集合,如新的[] {“SampleLogger.DefaultLoggerModule”}
3 -转换那些的那种陈述AddModule<SampleLogger.DefaultLoggerModule>();
4 -创建partial的实施ModuleProvider,我们正在增加我们的编译:
namespace SampleWeb
{
partial class ModuleProvider
{
protected override void Setup()
{
AddModule<SampleLogger.DefaultLoggerModule>();
}
}
}
Run Code Online (Sandbox Code Playgroud)
那么,我如何发现可用的模块?分为三个阶段:
1-引用的程序集(例如,通过NuGet提供的程序集)
2-引用的程序集(例如,解决方案中的引用项目)。
3-当前编译中的模块声明。
对于每个引用的编译,我们重复以上内容。
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;
}
Run Code Online (Sandbox Code Playgroud)
因此,要获取程序集引用的模块:
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>();
}
Run Code Online (Sandbox Code Playgroud)
由于该GetMetadata方法不是公开的,因此我们需要在此处进行一些反思,稍后,当我们获取元数据时,该CachedSymbols字段也是非公开的,因此在此处进行了更多的反思。在确定可用资源方面,我们需要IEnumerable<IAssemblySymbol>从CachedSymbols属性中获取。这为我们提供了参考程序集中的所有缓存符号。罗斯林(Roslyn)为我们这样做,因此我们可以滥用它:
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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
该GetTypeSymbols方法遍历命名空间并发现所有类型。然后,我们将结果链接到filter方法,以确保它实现了我们所需的接口:
private bool Filter(ITypeSymbol symbol)
{
return symbol.IsReferenceType
&& !symbol.IsAbstract
&& !symbol.IsAnonymousType
&& symbol.AllInterfaces.Any(i => i.GetFullMetadataName(i) == "Sample.IModule");
}
Run Code Online (Sandbox Code Playgroud)
用GetFullMetadataName是一个实用方法:
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;
}
Run Code Online (Sandbox Code Playgroud)
接下来,当前编译中的模块声明:
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);
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
就是这样!因此,现在在编译时,我ICompileModule将:
ModuleProvider.Setup使用所有已知的引用模块实现对我的方法的重写。这意味着我可以添加启动项:
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.
}
}
Run Code Online (Sandbox Code Playgroud)
大规模过度设计,相当复杂,但我认为还真棒!