How to persist a solution with projects and documents using Roslyn's AdhocWorkspace? - c#

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.

Related

Reading txt file and import as a c# code in a class

I wonder to know if it's possible. For example bootstrap has new version and i need to change cdn url and re-build project for RegisterBundles in asp.net web forms. It's simple url that we don't need to re-build project for that issue.
Is it possible that reading a *.txt file and using as c# code in a class. Class will be same as before and we will survive.
Example RegisterBundles code:
public class stylescriptbundle
{
public void RegisterBundles(BundleCollection bundles)
{
BundleTable.Bundles.Add(new ScriptBundle("/mybundle").Include(
"https://code.jquery.com/jquery-3.4.1.slim.min.js",
"https://cdn.jsdelivr.net/npm/popper.js#1.16.0/dist/umd/popper.min.js",
"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
);
BundleTable.Bundles.UseCdn = true;
}
}
First, similar to what robbpriestley said, what you should do is read the strings from a file so you can include them in the bundle. Configuration file is best, but if you're hellbent on a .txt, try below:
var scripts = File.ReadAllLines("someConfigurationFile.txt");
var bundle = new ScriptBundle("/myBundle");
foreach(var script in scripts)
{
bundle.Include(script);
}
BundleTable.Bundles.UseCdn = true;
BundleTable.Bundles.Add(bundle);
(the above is just off the top of my head, you may have to tweak it to get it to work.)
That said, if you still want to compile code at run time, you can use the Microsoft.CSharp and Microsoft.CodeDom.Compiler namespaces, according to this tutorial. I'll try and summarize it here, for archival reasons.
Get your code into a string: var codeStr = File.ReadAllText("runtime compiled.cs");
Create a provider and compiler: var provider = new CSharpCodeProvider(); var parameters = new CodeParameters();
Define parameters of the compiler. It sounds like you'll definitely want to compile it into memory, and you'll need to reference any assemblies you use in that code: parameters.GenerateInMemory = true; parameters.ReferencedAssemblies.Add("necessaryAssembly.dll");
compile: var results = provider.CompileAssemblyFromSource(parameters, code);
Check errors
Get your assembly: var assembly = results.CompiledAssembly; so you can get your type: var program = assembly.GetType("first.program"); so you can get your method: var main = program.GetMethod("Main");
And then you can call your method with Invoke: main.Invoke(null, null); (check out reference on MethodInfo.)
Don't use a TXT file for this. Put the strings as settings into your web.config and then use the provided ASP.NET ConfigurationManager to reference them.
For example, see: https://stackoverflow.com/a/12892083/1348592

Using log4net With Visual Studio Debugger

I'm having trouble getting my log4net.config file to load when using Visual Studio in debug mode for an Excel VSTO Plugin. The config file is in the top level directory of my project. I have the property "Copy to Output Directory" set to "Copy Always". This ensures the file is copied to bin/Debug/log4net.config. I can verify this is the case when I build.
However, the file won't load when I run in Debug mode. I gave up on trying to get the file to load automatically and decided to do it by code, as per the OP's code at the bottom of this question.
However, I realised that I needed to use an absolute path to the config file, as relative paths weren't picking it up. On further investigation, I realised that the executing DLL wasn't actually the DLL in the debug/bin folder. It was in the following location:
C:\Users\cbhandal\AppData\Local\assembly\dl3\MO52QQWP.9ZL\K36XZHGN.1PB\230751e6\d09b7fb2_19f6d401
Also the current working directory, as found by System.IO.Directory.GetCurrentDirectory(); was set to "C:\\Users\\cbhandal\\Documents".
Hard-coding the path as an absolute path works as in the following code:
var log4netConfig = "C:\\" + path + "\\Log4net.config";
var log4netInfo = new FileInfo(log4netConfig);
log4net.Config.XmlConfigurator.ConfigureAndWatch(log4netInfo);
But that's not a solution I can deploy. I'm stuck here. Wondering if there's a way to either force Visual studio to copy the .config file to that appdata/temp location, or if there's a way to programatically reference the folder where the original DLL lay- the one that was built. Or if anyone had any other solution?
For me the easiest solution was to use this:
https://stackoverflow.com/a/6963420/4754981
But there are several other solutions on that link for different approaches, each with their caveats.
So mine looks like this:
using System.Reflection;
using System.IO;
using System;
public static class Extensions {
private static string GetDirectory(this Assembly a) {
string codeBase = a.CodeBase;
UriBuilder uri = new UriBuilder(codeBase);
string path = Uri.UnescapeDataString(uri.Path);
return Path.GetDirectoryName(path);
}
private static void AlterLogPath(this log4net.Repository.ILoggerRepository repo, string newPath, string directory="") {
log4net.Repository.Hierarchy.Hierarchy h = (log4net.Repository.Hierarchy.Hierarchy) repo;
foreach (log4net.Appender.IAppender a in h.Root.Appenders) {
if (a is log4net.Appender.FileAppender) {
var fa = (log4net.Appender.FileAppender)a;
var fileName = Path.GetFileName(fa.File);
fa.File = newPath + (String.IsNullOrEmpty(directory)?"":(directory + Path.DirectorySeparatorChar.ToString())); // edit: filename is attached after next line automatically.
fa.ActivateOptions();
break;
}
}
}
}
and in the bootup (via [assembly: System.Web.PreApplicationStartMethod] or otherwise for asp), or main app..
static void Main() {
var PATH = Assembly.GetExecutingAssembly().GetDirectory() + Path.DirectorySeparatorChar.ToString();
log4net.Config.XmlConfigurator.ConfigureAndWatch(new FileInfo(PATH + "log4net.config"));
log4net.LogManager.GetRepository().AlterLogPath(PATH, "Logs");
}

Teamcity - writing tests that access a separate JSON file

I have TeamCity running for a C# project. The Unit tests are written using MSTest and they include an external JSON file. They are loaded in because they're large and I don't want to have to escape them in C#.
I import them like this:
[TestInitialize]
public void Setup()
{
using (StreamReader r = new StreamReader(#".\currency2.json"))
{
_json = r.ReadToEnd();
}
...
They run fine locally. I have 'Copy always set' but when the tests are ran using Teamcity I get an error saying that it can't find them in a temp folder. They are copied over to the build server but they're not in this temp folder.
Could not find file 'E:\TeamCity\buildAgent\temp\buildTmp\SYSTEM_SERVER 2016-07-18 15_28_19\Out\currency2.json'
I have **\bin\release\*test*.dll setup as my Test File Names in the test build step.
Any help appreciated.
I had a similar problem.
I changed the properties of the test file to this
Build Action = Content
Copy to Output Directory = Copy always
Teamcity will copy the file to the build folder, but it does not seem to maintain the same structure you'd expect.
So I created a file lookup loop. That will step down the expected folder until it finds the text file in question.
var pathLookups = new string[]
{
"2ndFolder\\3rdFolder\\test.json", // folder that normally workes
"3rdFolder\\test.json",
"test.json"
};
foreach (var pathLookup in pathLookups)
{
try
{
jsonFileCollection = JsonFileLoader<TestJsonType>.LoadJson(pathLooksup);
if (jsonFileCollection!= null)
{
break;
}
}
catch (Exception)
{
Console.WriteLine("Attempted to load test json from path:" + pathLooksup);
}
}
It's not the cleanest solution, but it will get the job done. You could refactor this to look a little nicer.
You might pass the full pass by argument to your program (and value defined in TeamCity).
Something like this (this is a pseudo-code example only) :
string[] programArgs;
static void Main(string[] args)
{
programArgs = args
}
[TestInitialize]
public void Setup()
{
using (StreamReader r = new StreamReader(programArgs[1]))
{
_json = r.ReadToEnd();
}
...
}

C# Generating dynamic executable from project

I wanna generate an exe file with some changes in code from another C# exe.
I know that can easy compile .cs single class using CodeDom.Compiler
The thing I want to know is how to compile a project with 'Resources', 'Settings', 'Forms' and other elements.
CSharpCodeProvider.CompileAssemblyFromSource(CompilerParameters, sources[]);
So, the question is where can I add all resources, settings and form (.resx)?
And can I do it with byte[] streams. Without unpacking project's zip.
Sorry for bad English and mby stupid questions. I wish somebody will help me...
For Example: I have byte[] array of resource file 'pic.png' and I wanna attach it to compiled exe as embedded resource.
You should learn about the new compiler service provided by Microsoft in Microsoft.CodeAnalysis code name "Roslyn".
Roslyn provides you the way to compile the code and everything on the fly including creating and compiling complete solution and projects in-memory.
I think what you're looking for can be achieved via Roslyn. See below sample:
class Program
{
static void Main()
{
var syntaxTree = SyntaxTree.ParseCompilationUnit(
#"using System;
using System.Resources;
namespace ResSample
{
class Program
{
static void Main()
{
ResourceManager resMan = new ResourceManager(""ResSample.Res1"", typeof(Program).Assembly);
Console.WriteLine(resMan.GetString(""String1""));
}
}
}");
var comp = Compilation.Create("ResTest.exe")
.AddReferences(new AssemblyNameReference("mscorlib"))
.AddSyntaxTrees(syntaxTree);
var resourcePath = "ResSample.Res1.resources"; //Provide full path to resource file here
var resourceDescription = new ResourceDescription(
resourceName: "ResSample.Res1.resources",
dataProvider: () => File.OpenRead(resourcePath),
isPublic: false);
var emitResult = comp.Emit(
executableStream: File.Create("ResTest.exe"),
manifestResources: new[] { resourceDescription });
Debug.Assert(emitResult.Success);
}
Original Source here
At line dataProvider: () => File.OpenRead(resourcePath), you can provide your own 'FileStream' like () => return _myResourceStream) for your resource file.

Programmatically build solution with filter

After looking all over the Google I found a good way to build a solution. However the solution I want to build also contains unit test projects, which I don't want to include in the build, or if I can't prevent that at least put those binaries in a separate folder. The code is as follows:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Logging;
public class BuildSolution
{
private readonly string _solutionPath;
private readonly string _outputPath = "C:\\Temp\\TestBuild\\";
public BuildSolution(string solutionPath, string outputPath = null)
{
if (!string.IsNullOrEmpty(outputPath))
_outputPath = outputPath;
_solutionPath = solutionPath;
Directory.EnumerateFiles(_outputPath, "*", SearchOption.AllDirectories)
.Select(x => new FileInfo(x))
.ToList()
.ForEach(x => x.Delete());
}
public void Build()
{
var pc = new ProjectCollection();
var globalProps = new Dictionary<string, string>()
{
{ ProjectPropertyNames.Configuration, "Debug" },
{ ProjectPropertyNames.OutputPath, _outputPath },
{ ProjectPropertyNames.EnableNuGetPackageRestore, "true"},
};
var targetsToBuild = new[] { "Build" };
var buildRequest = new BuildRequestData(_solutionPath, globalProps, null, targetsToBuild, null);
var buildParams = new BuildParameters(pc);
buildParams.Loggers = new List<ILogger>() { new ConsoleLogger(LoggerVerbosity.Minimal) };
var buildManager = BuildManager.DefaultBuildManager;
buildManager.BeginBuild(buildParams);
var buildSubmission = buildManager.PendBuildRequest(buildRequest);
buildSubmission.ExecuteAsync(BuildCompleted, null);
while (!done)
{
Thread.Sleep(10);
}
buildManager.EndBuild();
Console.WriteLine("OverallResult:{0}", buildSubmission.BuildResult.OverallResult);
}
bool done = false;
private void BuildCompleted(BuildSubmission submission)
{
done = submission.IsCompleted;
}
/// <summary>
/// Unused, but I tried it and it gives me back the correct projects but the build fails because of dependant nuget packages
/// </summary>
/// <param name="path">path of solution</param>
/// <returns></returns>
private IEnumerable<FileInfo> GetFirstLevelProjects(string path)
{
foreach (var dir in Directory.EnumerateDirectories(path))
{
foreach (var file in Directory.EnumerateFiles(dir, "*.csproj"))
{
if (!file.Contains("Test"))
yield return new FileInfo(file);
}
}
}
}
nothing fancy about it. (I'm playing with the idea of making the build async so I can update status...we'll see about that, I might switch it back to sync). One thing I tried was that instead of putting the solution in the build request, I would build the project collection using the first level projects (I use git with sub-modules, so I don't want to build all the non-relevant sub-modules). The problem with that route was that the build would fail because of nuget packages (not sure why or how to get around that). When I build the solution it builds successfully, but my outputPath also includes the test binaries. My end game is that the output can get copied to a specific folder of mine. I wouldn't mind having the test binaries if I knew I could filter ALL the binaries that are in the test projects... So how? What options do I have?
Why Not MsBuild?
The easiest way that I can think of to do this is to use MsBuild to do it.
msbuild C:\myFolder\mySolution.sln /p:Configuration=Release
As for not building the tests, this could now be easily changed from within visual studio
Right Click on Solution > Properties > Configuration Properties (on left side)
From there you could switch to release mode and uncheck the box next to your test projects. This will tell visual studio that when it does a release build it can skip these projects.
Without MsBuild
However, if you wanted to keep building them in the manner that you showed, I would change the assembly name (the dll name) so that you could identify them easily in your favorite scripting language.
Right Click on Project > Application Tab (on left side) > Assembly Name Box
I would call them something like SolutionNameSpace.Tests.ProjectUnderTest.dll.
Then in your build process you can filter out SolutionNameSpace.Tests.*.dll. Just be carefull, if you reference testing libraries they could get copied to your output folder also.

Categories