Set a class private string parameter with some value at build time in .net core 3.1 class library project

Viewed 355

I have a class in my .NET Core 3.1 library project, where I want to initialize the field key at build time:

using System.Text;

namespace MyNamespace
{
    public class Myclass
    {
        private string key;

        ....
    }
}

I am publishing my library via simple publish command:

dotnet publish -c release -r linux-x64

I am exploring an option where I could pass some attribute during build which will be set in key property during compilation/build process.

I am also obfuscating my DLL as well after this process via some third party tool.

This DLL will then be embedded in an application where this class one method is being called via reflection (reflection and calling method is already done).

4 Answers

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.

I can think of two options:

1. Using external file for the key - You are trying to have inversion of control at build time.
Why hard coding it in the dll when you can use text file with the key in it and your class will read the key from the file. As part of the build process you can define command to create the file with the specified key in the expected location where your dll will read it.

The down side - you need to have the file everywhere you are going to use the dll.
If this is a problem you can add the file as embeded resource to the dll and replace the value as part of the build script - before the actual build of the csproject, this way you will produce dll with embbeded resource which will be synced with the physical file.

2. You can use the roslyn API to create dlls at runtime - this way you can create console application which will read the key from a file or as CMD argument and this application will produce a new dll file with the required class with the key embbeded in the class as property / field.
More info on Roslyn API - you can check this SO answer for complete example how to use the API to produce dll file

Just some thoughts - If this key is some sensitive information you will need different approach (hard coded in the dll is not an option because it can be extracted pretty easy with programs like .NET Reflector - obfuscation won't do much)

This solution is not sophisticated as the other presented, but it probably can achieve your goal with a minimal effort.

I'd use preprocessor directives:

using System.Text;

namespace MyNamespace
{
    public class Myclass
    {
#if MY_DEBUG_KEY
    private string key = "debug";
#elif MY_RELEASE_KEY
    private string key = "release";
#else
#error key value has not been set
#endif
    }
}

Then you can edit your csproj adding thw following PropertyGroups:

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
  <DefineConstants>TRACE;MY_DEBUG_KEY</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
  <DefineConstants>TRACE;MY_RELEASE_KEY</DefineConstants>
</PropertyGroup>

So if you'll compile using Debug configuration the key value will be "debug", if you'll compile with Release it will be "release" and if you are going to use a new configuration it won't compile remembering you that you need to set a value for your key.

And you will be able to do it by adding another #elif directive and a PropertyGroup setting in your csproj:

.cs:

#if MY_DEBUG_KEY
    private const string key = "debug";
#elif MY_RELEASE_KEY
        private const string key = "release";
#elif MY_OTHERCFG_KEY
    private const string key = "another value";
#else
#error key value has not been set
#endif

.csproj:

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='OtherCfg|AnyCPU'">
  <DefineConstants>TRACE;MY_OTHERCFG_KEY</DefineConstants>
</PropertyGroup>

The dead easiest way to do this would be to declare the class you want the variable in as partial. This ads a little overhead on the build management side, but the C# compiler does not allow or defining terms with values like a c++ style compiler

Then in the build system swap in a cs file with the defined value you want.

swap.cs
namespace MyNamespace
{
  partial class MyClass 
  {
     const string myspecialstring = "Super duper value";
  }
}

then

MyProgramSource.cs
namespace MyNamespace
{
  partial class MyClass 
  {
    // Lots of awsome code 
    System.Diagnostics.Trace.Writeline($"{myspecialstring}");  //works no problem
  }
}
Related