Using the Roslyn API you can parse and compile a source programatically. The question linked by vasil oreshenski gives you working examples. The missing piece is to get your desired value (somehow) and then embed it into the syntax tree before compilation.
First, to make our lives easier, create a custom attribute and mark the key field with it.
using System;
public class BuildTimeParameterAttribute : Attribute
{
}
namespace MyNamespace
{
public class Myclass
{
[BuildTimeParameter]
private const string key1 = "", key2 = "";
[BuildTimeParameter]
private const string key3 = "";
....
}
}
I also changed the field to be a const
and added additional fields to test the behaviour. Now when you have a syntax tree from the RoslynAPI you can do this:
var parsedSyntaxTree = Parse(
source,
"",
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp8));
// Get those values from commandline arguments or however you like.
var replacementValues = new Dictionary<string, string>
{{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}};
var root = (CompilationUnitSyntax) parsedSyntaxTree.GetRoot();
// Retrieve all fields marked with the BuildTimeParameterAttribute.
var fields = root.DescendantNodes().OfType<FieldDeclarationSyntax>().Where(
s => s.AttributeLists.SelectMany(a => a.Attributes)
.Any(a => a.Name.ToString() == "BuildTimeParameter"));
var newRoot = root.ReplaceNodes(
fields,
(_, field) =>
{
var variables = field.Declaration.Variables;
var newVariables = from variable in variables
let newValue = replacementValues[variable.Identifier.ValueText]
let newInitializer = variable.Initializer.WithValue(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(newValue)))
select variable.WithInitializer(newInitializer);
return field.WithDeclaration(
field.Declaration.WithVariables(
SyntaxFactory.SeparatedList(newVariables)));
});
The procedure is a bit complicated, because a single field declaration can contain many variables (in our case the first field declaration contains key1
and key2
).
Now you can create a compilation using the newRoot.SyntaxTree
. Decompiling the resulting dll yields:
namespace MyNamespace
{
public class MyClass
{
[BuildTimeParameter]
private const string key1 = "value1";
[BuildTimeParameter]
private const string key2 = "value2";
[BuildTimeParameter]
private const string key3 = "value3";
}
}
The above code doesn't handle errors and will fail with an NRE if there is no initializer, so for example if you write private string key;
. It will work with const
, as they are required to have an initializer. Making this code production-worthy I leave for you.
Note
With .NET 5 this could be much more easily done using source generators. You could declare your class as partial
and then generate a file with the key
constant field declaration using a source generator.