Background
I'm trying to get Edit and Continue to work with a class library I'm compiling at runtime using Roslyn. This is for adding modding support to a game I'm developing.
Breakdown of problem
I have one class library project (A) with source files (.cs)
I have another console application project (B) in another solution that does the following:
Compiles all of project A's source files
Emits a dll and pdb
Loads the emitted dll and pdb via an assembly context
Calls a static method defined within project B
My desire is to be able to attach a debugger to a running process of project B in an instance of VS with project A loaded and be able to break, edit project A's code, and continue with my changes being executed
Currently, I am only able to break and continue
Any edits lead to the following notification:
This source file has changed. It no longer matches the version of the file used to build the application being debugged.
Source
Project A: DebuggableClassLibrary.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
Project A: Test.cs
using System;
namespace DebuggableClassLibrary
{
public class Test
{
public static int Ct = 0;
public static void SayHello()
{
Ct++;
Console.WriteLine("Hello World");
}
}
}
Project B: DynamicLoading.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" />
</ItemGroup>
</Project>
Project B: Program.cs
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
namespace DynamicLoading
{
class Program
{
static void Main(string[] args)
{
var references = new MetadataReference[]
{
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location)
};
var files = Directory.GetFiles(#"C:\Users\mrbri\source\repos\DebuggableClassLibrary\DebuggableClassLibrary", "*.cs");
var assemblyName = "DebuggableClassLibrary.dll";
var debug = true;
var allowUnsafe = false;
var outputDirectory = #"C:\Users\mrbri\Documents\Test";
var preprocessorSymbols = debug ? new string[] { "DEBUG" } : new string[] { };
var parseOptions = new CSharpParseOptions(LanguageVersion.Latest, preprocessorSymbols: preprocessorSymbols);
var compilation = CSharpCompilation.Create(
assemblyName: assemblyName,
syntaxTrees: files.Select(f => SyntaxFactory.ParseSyntaxTree(File.ReadAllText(f), parseOptions, f, Encoding.UTF8)),
references: references,
options: new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default,
optimizationLevel: debug ? OptimizationLevel.Debug : OptimizationLevel.Release,
allowUnsafe: allowUnsafe
));
var pePath = Path.Combine(outputDirectory, assemblyName);
var pdbPath = Path.Combine(outputDirectory, Path.ChangeExtension(assemblyName, ".pdb"));
using (var peStream = new FileStream(pePath, FileMode.Create))
using (var pdbStream = new FileStream(pdbPath, FileMode.Create))
{
var results = compilation.Emit(
peStream: peStream,
pdbStream: pdbStream,
options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb)
);
}
var assemblyLoadContext = new SimpleUnloadableAssemblyLoadContext();
var assembly = assemblyLoadContext.LoadFromStream(File.OpenRead(pePath), File.OpenRead(pdbPath));
var type = assembly.GetTypes().First();
var method = type.GetMethod("SayHello");
while (true)
{
method.Invoke(null, null);
}
}
}
internal class SimpleUnloadableAssemblyLoadContext : AssemblyLoadContext
{
public SimpleUnloadableAssemblyLoadContext(): base(true) { }
protected override Assembly Load(AssemblyName assemblyName) => null;
}
}
Attempts at solutions and observations
Compiling project A manually through VS and loading the generated pdb and dll exactly as I do for the Roslyn compiled one does allow for Edit and Continue
Comparing project A's dlls generated via Roslyn and VS in JetBrains dotPeek did yield some interesting differences that stem from the compilation time generated .NETCoreApp,Version=v5.0.AssemblyAttributes.cs and DebuggableClassLibrary.AssemblyInfo.cs that I do not include when I compile in project B
Going through the trouble of compiling project A via a MSBuildWorkspace Project did not allow Edit and Continue, although did include .NETCoreApp,Version=v5.0.AssemblyAttributes.cs and DebuggableClassLibrary.AssemblyInfo.cs
Alternatives
I am open to Roslyn alternatives/wrappers that do have Edit and Continue support.
Edit and Continue does not support this scenario. The project for the library being edited needs to be loaded in VS (in the current solution) and the program needs to be launched with the debugger attached.
Related
ANSWER for this question thanks to Jeremy C.:
There is no KeePass nuget package for the Net5.0 yet. Thats why there is that error message. Thanks Jeremy C. for the help and answers.
QUESTION:
Im getting this error after starting my solution.
Unhandled exception. System.TypeLoadException: Could not load type 'System.Drawing.Color' from assembly 'Splat, Version=3.0.0.0, Culture=neutral, PublicKeyToken=null'.
Already used google and tried to find a fix for it and also red all articles about similiar errors like "System.Drawing.Font" or "System.Drawing.Image". But theres nothing really helpful and nothing really informative about 'System.Drawing.Color'.
Ive got the code example and package from here:
github.com/wismna/ModernKeePassLib
This is my code:
.csproj
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ModernKeePassLib" Version="2.45.1" />
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
</ItemGroup>
</Project>
And:
using ModernKeePassLib;
using ModernKeePassLib.Interfaces;
using ModernKeePassLib.Keys;
using ModernKeePassLib.Serialization;
using System;
using System.Linq;
using System.Text;
namespace KeePasso
{
class Program
{
static void Main()
{
var dbpath = #"C:\Users\prusinma\Desktop\KeePassDatabase\Database.kdbx";
var keypath = #"C:\Users\prusinma\Desktop\KeePassDatabase\Database.key";
var masterpw = "1234abcd";
Console.WriteLine("init done");
byte[] DBPathBytes = Encoding.ASCII.GetBytes(dbpath);
byte[] KeyPathBytes = Encoding.ASCII.GetBytes(keypath);
var ioConnection = IOConnectionInfo.FromByteArray(DBPathBytes);
var compositeKey = new CompositeKey();
compositeKey.AddUserKey(new KcpPassword(masterpw)); // Password
compositeKey.AddUserKey(new KcpKeyFile(IOConnectionInfo.FromByteArray(KeyPathBytes))); // Keyfile
var db = new PwDatabase();
db.Open(ioConnection, compositeKey, new NullStatusLogger());
var kpdata = from entry in db.RootGroup.GetEntries(true)
select new
{
Group = entry.ParentGroup.Name,
Title = entry.Strings.ReadSafe("Title"),
Username = entry.Strings.ReadSafe("UserName"),
Password = entry.Strings.ReadSafe("Password"),
URL = entry.Strings.ReadSafe("URL"),
Notes = entry.Strings.ReadSafe("Notes")
};
db.Save(new NullStatusLogger());
var contents = db.IOConnectionInfo.Bytes;
string bitString = BitConverter.ToString(contents);
Console.WriteLine(bitString);
Console.WriteLine(kpdata.ToString());
}
}
}
Those classes were moved into their own nuget package. Add it to your project and you should be good to go: https://www.nuget.org/packages/System.Drawing.Common/
From the project directory at the command line:
dotnet add package System.Drawing.Common
Closer inspection reveals ModernKeepPass targets.netstandard1.2 and will not work with 5's System.Drawing nuget package without being upgraded to target the new framework.
https://github.com/wismna/ModernKeePassLib/blob/master/ModernKeePassLib/ModernKeePassLib.csproj
<PropertyGroup>
<TargetFramework>netstandard1.2</TargetFramework>
I cannot understand why I am getting an error (using VS2017) for the code in below related to not finding the class ControlFlowGraph which is supposed to be part of the package Microsoft.CodeAnalysis.FlowAnalysis:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.FlowAnalysis;
namespace CodeAnalysisApp3
{
class Program
{
static async Task Main(string[] args)
{
// Attempt to set the version of MSBuild.
var visualStudioInstances = MSBuildLocator.QueryVisualStudioInstances().ToArray();
var instance = visualStudioInstances[0];
Console.WriteLine($"Using MSBuild at '{instance.MSBuildPath}' to load projects.");
// NOTE: Be sure to register an instance with the MSBuildLocator
// before calling MSBuildWorkspace.Create()
// otherwise, MSBuildWorkspace won't MEF compose.
MSBuildLocator.RegisterInstance(instance);
using (var workspace = MSBuildWorkspace.Create())
{
// Print message for WorkspaceFailed event to help diagnosing project load failures.
workspace.WorkspaceFailed += (o, e) => Console.WriteLine(e.Diagnostic.Message);
var solutionPath = args[0];
Console.WriteLine($"Loading solution '{solutionPath}'");
// Attach progress reporter so we print projects as they are loaded.
var solution = await workspace.OpenSolutionAsync(solutionPath, new ConsoleProgressReporter());
Console.WriteLine($"Finished loading solution '{solutionPath}'");
// TODO: Do analysis on the projects in the loaded solution
CSharpParseOptions options = CSharpParseOptions.Default
.WithFeatures(new[] { new KeyValuePair<string, string>("flow-analysis", "") });
var projIds = solution.ProjectIds;
var project = solution.GetProject(projIds[0]);
Compilation compilation = await project.GetCompilationAsync();
if (compilation != null && !string.IsNullOrEmpty(compilation.AssemblyName))
{
var mySyntaxTree = compilation.SyntaxTrees.First();
// get syntax nodes for methods
var methodNodes = from methodDeclaration in mySyntaxTree.GetRoot().DescendantNodes()
.Where(x => x is MethodDeclarationSyntax)
select methodDeclaration;
foreach (MethodDeclarationSyntax node in methodNodes)
{
var model = compilation.GetSemanticModel(node.SyntaxTree);
node.Identifier.ToString();
if (node.SyntaxTree.Options.Features.Any())
{
var graph = ControlFlowGraph.Create(node, model); // CFG is here
}
}
}
}
}
private class ConsoleProgressReporter : IProgress<ProjectLoadProgress>
{
public void Report(ProjectLoadProgress loadProgress)
{
var projectDisplay = Path.GetFileName(loadProgress.FilePath);
if (loadProgress.TargetFramework != null)
{
projectDisplay += $" ({loadProgress.TargetFramework})";
}
Console.WriteLine($"{loadProgress.Operation,-15} {loadProgress.ElapsedTime,-15:m\\:ss\\.fffffff} {projectDisplay}");
}
}
}
}
However, when I compile the above code I am getting the following error message with VS2017:
1>Program.cs(67,41,67,57): error CS0103: The name 'ControlFlowGraph' does not exist in the current context
1>Done building project "CodeAnalysisApp3.csproj" -- FAILED.
========== Rebuild All: 0 succeeded, 1 failed, 0 skipped ==========
Version used:
Microsoft (R) Visual C# Compiler version 4.8.3761.0
for C# 5
Based on my test, I find I can use class ControlFlowGraph.
I installed the following nugetpackage.
Microsoft.CodeAnalysis
Microsoft.Build.Locator
Then, you will see the following result.
Besides, I used .net framwork 4.6.1.
I was able to solve the problem when I used roslyn CodeAnalysis packages with the proper versions:
CodeAnalysis.CSharp.Workspaces (3.4.0)
CodeAnalysis.FlowAnalysis.Utilities (2.9.6)
CodeAnalysis.Workspaces.MSBuild (3.4.0)
The target framework is .NETFramework 4.7.2
A link to a closed issue created for this question on roslyn Github repo is here
I'm trying to build a dynamic Web interface where I can dynamically point at a folder and serve Web content out of that folder with ASP.NET Core. This works fairly easily by using FileProviders in ASP.NET Core to re-route the Web root folder. This works both for StaticFiles and For RazorPages.
However, for RazorPages the problem is that once you do this you can't dynamically add references for additional types. I'd like to be able to optionally add a folder (PrivateBin) which on startup I can loop through, load the assemblies and then have those assemblies visible in Razor.
Unfortunately it doesn't work as Razor does not appear to see the loaded assemblies even when using runtime compilation.
I use the following during startup to load assemblies. Note the folder that these are loaded from are not in the default ContentRoot or WebRoot but in the new redirected WebRoot.
// WebRoot is a user chosen Path here specified via command line --WebRoot c:\temp\web
private void LoadPrivateBinAssemblies()
{
var binPath = Path.Combine(WebRoot, "PrivateBin");
if (Directory.Exists(binPath))
{
var files = Directory.GetFiles(binPath);
foreach (var file in files)
{
if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
!file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
continue;
try
{
var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
Console.WriteLine("Additional Assembly: " + file);
}
catch (Exception ex)
{
Console.WriteLine("Failed to load private assembly: " + file);
}
}
}
}
The assembly loads into the AssemblyLoadContext() and I can - using Reflection and Type.GetType("namespace.class,assembly") - access the type.
However, when I try to access the type in RazorPages - even with Runtime Compilation enabled - the types are not available. I get the following error:
To make sure that the type is indeed available, I checked that I can do the following inside of Razor:
#{
var md = Type.GetType("Westwind.AspNetCore.Markdown.Markdown,Westwind.AspNetCore.Markdown");
var mdText = md.InvokeMember("Parse", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null,
null, new object[] { "**asdasd**", false, false, false });
}
#mdText
and that works fine. So the assembly is loaded and the type is accessible, but Razor doesn't appear to be aware of it.
So the question is:
Is it possible to load assemblies at runtime and make them available to Razor with Runtime Compilation, and use it like you normally would use a type via direct declarative access?
It turns out the solution to this is via the Razor Runtime Compilation Options which allow adding of extra 'ReferencePaths', and then explicitly loading assemblies.
In ConfigureServices():
services.AddRazorPages(opt => { opt.RootDirectory = "/"; })
.AddRazorRuntimeCompilation(
opt =>
{
opt.FileProviders.Add(new PhysicalFileProvider(WebRoot));
LoadPrivateBinAssemblies(opt);
});
then:
private void LoadPrivateBinAssemblies(MvcRazorRuntimeCompilationOptions opt)
{
var binPath = Path.Combine(WebRoot, "PrivateBin");
if (Directory.Exists(binPath))
{
var files = Directory.GetFiles(binPath);
foreach (var file in files)
{
if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
!file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
continue;
try
{
var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
opt.AdditionalReferencePaths.Add(file);
}
catch (Exception ex)
{
...
}
}
}
}
The key is:
opt.AdditionalReferencePaths.Add(file);
which makes the assembly visible to Razor, but doesn't actually load it. To load it you then have to explicitly load it with:
AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
which loads the assembly from a path. Note that any dependencies that this assembly has have to be available either in the application's startup path or in the same folder you're loading from.
Note: Load order for dependencies may be important here or a not previously added assembly may not be found as a dependency (untested).
A Quick look into the ASP.NET Core source code reveals:
All Razor view Compilations start at:
RuntimeViewCompiler.CreateCompilation(..)
which uses:
CSharpCompiler.Create(.., .., references: ..)
which uses:
RazorReferenceManager.CompilationReferences
which uses: see code on github
// simplyfied
var referencePaths = ApplicationPartManager.ApplicationParts
.OfType<ICompilationReferencesProvider>()
.SelectMany(_ => _.GetReferencePaths())
which uses:
ApplicationPartManager.ApplicationParts
So we need somehow register our own ICompilationReferencesProvider and this is how..
ApplicationPartManager
While it's search for Application parts does the ApplicationPartManager a few things:
it searchs for hidden Assemblies reading attributes like:
[assembly: ApplicationPartAttribute(assemblyName:"..")] // Specifies an assembly to be added as an ApplicationPart
[assembly: RelatedAssemblyAttribute(assemblyFileName:"..")] // Specifies a assembly to load as part of MVC's assembly discovery mechanism.
// plus `Assembly.GetEntryAssembly()` gets added automaticly behind the scenes.
Then it loops throuth all found Assemblies and uses ApplicationPartFactory.GetApplicationPartFactory(assembly) (as seen in line 69) to find types which extend ApplicationPartFactory.
Then it invokes the method GetApplicationParts(assembly) on all found ApplicationPartFactorys.
All Assemblies without ApplicationPartFactory get the DefaultApplicationPartFactory which returns new AssemblyPart(assembly) in GetApplicationParts.
public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
GetApplicationPartFactory
GetApplicationPartFactory searches for [assembly: ProvideApplicationPartFactory(typeof(SomeType))] then it uses SomeType as factory.
public abstract class ApplicationPartFactory {
public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
public static ApplicationPartFactory GetApplicationPartFactory(Assembly assembly)
{
// ...
var provideAttribute = assembly.GetCustomAttribute<ProvideApplicationPartFactoryAttribute>();
if (provideAttribute == null)
{
return DefaultApplicationPartFactory.Instance; // this registers `assembly` as `new AssemblyPart(assembly)`
}
var type = provideAttribute.GetFactoryType();
// ...
return (ApplicationPartFactory)Activator.CreateInstance(type);
}
}
One Solution
This means we can create and register (using ProvideApplicationPartFactoryAttribute) our own ApplicationPartFactory which returns a custom ApplicationPart implementation which implements ICompilationReferencesProvider and then returns our references in GetReferencePaths.
[assembly: ProvideApplicationPartFactory(typeof(MyApplicationPartFactory))]
namespace WebApplication1 {
public class MyApplicationPartFactory : ApplicationPartFactory {
public override IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly)
{
yield return new CompilationReferencesProviderAssemblyPart(assembly);
}
}
public class CompilationReferencesProviderAssemblyPart : AssemblyPart, ICompilationReferencesProvider {
private readonly Assembly _assembly;
public CompilationReferencesProviderAssemblyPart(Assembly assembly) : base(assembly)
{
_assembly = assembly;
}
public IEnumerable<string> GetReferencePaths()
{
// your `LoadPrivateBinAssemblies()` method needs to be called before the next line executes!
// So you should load all private bin's before the first RazorPage gets requested.
return AssemblyLoadContext.GetLoadContext(_assembly).Assemblies
.Where(_ => !_.IsDynamic)
.Select(_ => new Uri(_.CodeBase).LocalPath);
}
}
}
My Working Test Setup:
ASP.NET Core 3 WebApplication
ASP.NET Core 3 ClassLibrary
Both Projects have no reference to each other.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Content Remove="Pages\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.0.0" />
</ItemGroup>
</Project>
services
.AddRazorPages()
.AddRazorRuntimeCompilation();
AssemblyLoadContext.Default.LoadFromAssemblyPath(#"C:\path\to\ClassLibrary1.dll");
// plus the MyApplicationPartFactory and attribute from above.
~/Pages/Index.cshtml
#page
<pre>
output: [
#(
new ClassLibrary1.Class1().Method1()
)
]
</pre>
And it shows the expected output:
output: [
Hallo, World!
]
Have a nice day.
I set the 'Embed Interop Types' property of the Netwonsoft.Json library to true and it returns an error:
Cannot embed interop types from assembly
'c:\path\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll'
because it is missing either the 'ImportedFromTypeLibAttribute' attribute or
the 'PrimaryInteropAssemblyAttribute' attribute
c:\path\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll
It looks like looking for missing references within the Newtonsoft.Json library, but I am not entirely certain. Is it possible for Json.Net to be embeded into the executable?
You didn't say which language you were using but here is how you'd do it for C#
First, turn off "Embed Interop Types"
Then, to the main executable project, unload and edit the .csproj file, and below the following line:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
Add this XML to the project file, save, and load it back up.
<Target Name="AfterResolveReferences">
<ItemGroup>
<EmbeddedResource Include="#(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
<LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
</Target>
You’ll then add a new code file to the main project and add the following code to it (modified to fit how your application is named / structured, in a WPF application, a good place to put it would be App.xaml.cs):
[STAThread]
public static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
App.Main(); // Run WPF startup code.
}
private static Assembly OnResolveAssembly(object sender, ResolveEventArgs e)
{
var thisAssembly = Assembly.GetExecutingAssembly();
// Get the Name of the AssemblyFile
var assemblyName = new AssemblyName(e.Name);
var dllName = assemblyName.Name + ".dll";
// Load from Embedded Resources - This function is not called if the Assembly is already
// in the same folder as the app.
var resources = thisAssembly.GetManifestResourceNames().Where(s => s.EndsWith(dllName));
if (resources.Any())
{
// 99% of cases will only have one matching item, but if you don't,
// you will have to change the logic to handle those cases.
var resourceName = resources.First();
using (var stream = thisAssembly.GetManifestResourceStream(resourceName))
{
if (stream == null) return null;
var block = new byte[stream.Length];
// Safely try to load the assembly.
try
{
stream.Read(block, 0, block.Length);
return Assembly.Load(block);
}
catch (IOException)
{
return null;
}
catch(BadImageFormatException)
{
return null;
}
}
}
// in the case the resource doesn't exist, return null.
return null;
}
Finally, make sure you update the target method for your main application to be the main method for the project you just added
Source: http://www.paulrohde.com/merging-a-wpf-application-into-a-single-exe/
I've been trying to persist a new solution containing a project and a simple cs file with the following code but nothing get saved to the disk. Am I doing something wrong or is Roslyn not the tool to be used to generate solutions project and files?
class Program
{
static void Main(string[] args)
{
var workspace = new AdhocWorkspace();
var solutionInfo = SolutionInfo.Create(SolutionId.CreateNewId(),
VersionStamp.Create(),
#"C:\Seb\SebSol.sln");
var projectInfo = ProjectInfo.Create(ProjectId.CreateNewId(),
VersionStamp.Create(),
"SebProj",
"SebProj.dll",
LanguageNames.CSharp,
#"C:\Seb\SebSol\SebProj\SebProj.csproj");
workspace.AddSolution(solutionInfo);
workspace.AddProject(projectInfo);
var sourceText = SourceText.From("public class A { }");
workspace.CurrentSolution.AddDocument(DocumentId.CreateNewId(projectInfo.Id), "ClassA.cs", sourceText);
workspace.TryApplyChanges(workspace.CurrentSolution);
}
}
You're looking for MsBuildWorkspace, which can actually update sln and csproj files in MSBuild format on disk.
Other than that class, Roslyn APIs are completely agnostic to project formats such as MSBuild.