我正在构建一个允许用户定义、编辑和执行C#脚本的应用程序。
该定义由方法名称、参数名称数组和方法的内部代码组成,例如:
Script1
根据这一定义,可以生成以下代码:
public static object Script1(object arg1, object arg2)
{
return $"Arg1: {arg1}, Arg2: {arg2}";
}
我成功地设置了一个AdhocWorkspace
和一个像这样的Project
:
private readonly CSharpCompilationOptions _options = new CSharpCompilationOptions(OutputKind.ConsoleApplication,
moduleName: "MyModule",
mainTypeName: "MyMainType",
scriptClassName: "MyScriptClass"
)
.WithUsings("System");
private readonly MetadataReference[] _references = {
MetadataReference.CreateFromFile(typeof(object).Assembly.Location)
};
private void InitializeWorkspaceAndProject(out AdhocWorkspace ws, out ProjectId projectId)
{
var assemblies = new[]
{
Assembly.Load("Microsoft.CodeAnalysis"),
Assembly.Load("Microsoft.CodeAnalysis.CSharp"),
Assembly.Load("Microsoft.CodeAnalysis.Features"),
Assembly.Load("Microsoft.CodeAnalysis.CSharp.Features")
};
var partTypes = MefHostServices.DefaultAssemblies.Concat(assemblies)
.Distinct()
.SelectMany(x => x.GetTypes())
.ToArray();
var compositionContext = new ContainerConfiguration()
.WithParts(partTypes)
.CreateContainer();
var host = MefHostServices.Create(compositionContext);
ws = new AdhocWorkspace(host);
var projectInfo = ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"MyProject",
"MyProject",
LanguageNames.CSharp,
compilationOptions: _options, parseOptions: new CSharpParseOptions(LanguageVersion.CSharp7_3, DocumentationMode.None, SourceCodeKind.Script)).
WithMetadataReferences(_references);
projectId = ws.AddProject(projectInfo).Id;
}
我可以创建这样的文件:
var document = _workspace.AddDocument(_projectId, "MyFile.cs", SourceText.From(code)).WithSourceCodeKind(SourceCodeKind.Script);
对于用户定义的每个脚本,我目前正在创建一个单独的Document
。
使用以下方法也可以执行代码:
首先,汇编所有文件:
public async Task<Compilation> GetCompilations(params Document[] documents)
{
var treeTasks = documents.Select(async (d) => await d.GetSyntaxTreeAsync());
var trees = await Task.WhenAll(treeTasks);
return CSharpCompilation.Create("MyAssembly", trees, _references, _options);
}
然后,从编译中创建程序集:
public Assembly GetAssembly(Compilation compilation)
{
try
{
using (MemoryStream ms = new MemoryStream())
{
var emitResult = compilation.Emit(ms);
if (!emitResult.Success)
{
foreach (Diagnostic diagnostic in emitResult.Diagnostics)
{
Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
}
}
else
{
ms.Seek(0, SeekOrigin.Begin);
var buffer = ms.GetBuffer();
var assembly = Assembly.Load(buffer);
return assembly;
}
return null;
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
最后,执行脚本:
public async Task<object> Execute(string method, object[] params)
{
var compilation = await GetCompilations(_documents);
var a = GetAssembly(compilation);
try
{
Type t = a.GetTypes().First();
var res = t.GetMethod(method)?.Invoke(null, params);
return res;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
到现在为止还好。这使得用户可以定义可以彼此都可以使用的脚本。
为了编辑,我想提供代码完成,目前正在这样做:
public async Task<CompletionList> GetCompletionList(Document doc, string code, int offset)
{
var newDoc = doc.WithText(SourceText.From(code));
_workspace.TryApplyChanges(newDoc.Project.Solution);
var completionService = CompletionService.GetService(newDoc);
return await completionService.GetCompletionsAsync(newDoc, offset);
}
注意:上面的代码片段被更新以修复Jason在回答中提到的关于使用doc
和document
的错误。实际上,这是因为这里显示的代码是从我的实际应用程序代码中提取(从而修改)的。您可以找到我在他的答案中贴出的原始错误片段,也可以在新版本下面找到,它解决了导致我的问题的实际问题。
现在的问题是,GetCompletionsAsync
只知道同一个Document
中的定义以及在创建工作区和项目时使用的引用,但是它显然没有对同一个项目中的其他文档的任何引用。因此,CompletionList
不包含其他用户脚本的符号。
这似乎很奇怪,因为在“活动”Visual项目中,当然,项目中的所有文件都相互了解。
我遗漏了什么?项目和/或工作区设置是否不正确?还有其他方法叫CompletionService
吗?生成的文档代码是否遗漏了一些东西,比如公共名称空间?
我的最后一招是将用户脚本定义生成的所有方法合并到一个文件中--还有其他方法吗?
FYI,这里有几个有用的链接帮助我走到这一步:
https://www.strathweb.com/2018/12/using-roslyn-c-completion-service-programmatically/
Roslyn throws The language 'C#' is not supported
Updating AdHocWorkspace is slow
Roslyn: is it possible to pass variables to documents (with SourceCodeKind.Script)
更新1:感谢杰森的回答,我更新了GetCompletionList
方法如下:
public async Task<CompletionList> GetCompletionList(Document doc, string code, int offset)
{
var docId = doc.Id;
var newDoc = doc.WithText(SourceText.From(code));
_workspace.TryApplyChanges(newDoc.Project.Solution);
var currentDoc = _workspace.CurrentSolution.GetDocument(docId);
var completionService = CompletionService.GetService(currentDoc);
return await completionService.GetCompletionsAsync(currentDoc, offset);
}
正如Jason指出的,最大的错误是没有充分考虑到项目及其文档的不可变性。调用CompletionService.GetService(doc)
所需的CompletionService.GetService(doc)
实例必须是当前解决方案中包含的实际实例,而不是由doc.WithText(...)
创建的实例,因为该实例不知道任何事情。
通过存储原始实例的DocumentId
并使用它在解决方案currentDoc
中检索更新的实例,在应用更改之后,完成服务可以(如“活动”解决方案)引用其他文档。
更新2:在我最初的问题中,代码片段使用了SourceCodeKind.Regular
,但是--至少在本例中--它必须是SourceCodeKind.Script
,因为否则编译器会抱怨不允许顶级静态方法(当使用C# 7.3时)。我现在更新了这篇文章。
发布于 2022-02-15 19:52:02
有一件事看上去有点可疑:
public async Task<CompletionList> GetCompletionList(Document doc, string code, int offset)
{
var newDoc = document.WithText(SourceText.From(code));
_workspace.TryApplyChanges(newDoc.Project.Solution);
var completionService = CompletionService.GetService(newDoc);
return await completionService.GetCompletionsAsync(document, offset);
}
(注意:您的参数名是"doc“,但是您使用的是”文档“,所以我猜这段代码是从完整示例中删减的。但我只是想把它叫停,因为你可能在做这件事时引入了错误。)
因此,主要的鱼位: Roslyn文档是快照;文档是整个解决方案的整个快照中的指针。您的"newDoc“是一个新文档,它包含您所替换的文本,您正在更新w工作区以包含它。然而,您仍然要将原始文档提交给GetCompletionsAsync,这意味着在这种情况下仍然需要旧文档,这可能是陈旧的代码。此外,由于这都是一个快照,通过调用TryApplyChanges对主工作区所做的更改不会以任何方式反映在新的文档对象中。因此,我猜想这里可能发生的是,您传入的文档对象实际上并没有立即更新所有的文本文档,但它们中的大多数仍然是空的或类似的。
https://stackoverflow.com/questions/71127654
复制相似问题