2

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).

sɐunıɔןɐqɐp
  • 3,332
  • 15
  • 36
  • 40
Kamran Shahid
  • 3,954
  • 5
  • 48
  • 93
  • may help the url https://dzone.com/articles/using-startup-command-to-pass-command-line-arguments-to-azure-app-service-for-linux – LDS Jul 26 '20 at 08:36
  • I think it is not possible, you can pass parameter at run time but not at compile time. Let the expert answer this – Vivek Nuna Jul 26 '20 at 10:48
  • Let's wait for someone then – Kamran Shahid Jul 26 '20 at 11:24
  • @LDS i doesn't want command line argument to be passed. i want to initialize some string property at build time. that dll will then be plugged in in some other application where a method of that class is called via reflection – Kamran Shahid Jul 27 '20 at 15:03
  • why not just using a t4 template? – ralf.w. Jul 30 '20 at 07:18
  • can you please guide me such t4 template in .net core 3.1 class library project via which i can able to set some string in build process? – Kamran Shahid Jul 30 '20 at 08:39
  • Have you considered generating a `*.cs` file via msbuild? https://stackoverflow.com/a/53605206/507793 – Matthew Aug 04 '20 at 17:57

4 Answers4

4

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.

V0ldek
  • 9,623
  • 1
  • 26
  • 57
  • Thanks @V0ldek. As we know we can create platform specific build by dotnet publish -c release -r linux-x64 or dotnet publish -c release -r win-x64 commands. I am just thinking how i can achieve this during this build process – Kamran Shahid Aug 04 '20 at 09:26
  • @KamranShahid To integrate with MsBuild you would have to [create a custom Task that would run before build](https://learn.microsoft.com/en-us/visualstudio/msbuild/task-writing?view=vs-2019), [parse arguments from commandline](https://stackoverflow.com/questions/6051054/how-do-i-pass-a-property-to-msbuild-via-command-line-that-could-be-parsed-into-a), pass the source files as a parameter to the task and use the above syntax tree manipulations to parse, rewrite and save the file back to disk. – V0ldek Aug 05 '20 at 08:02
  • Thanks . Quite a number of things to achieve this :) – Kamran Shahid Aug 05 '20 at 18:40
1

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)

vasil oreshenski
  • 2,788
  • 1
  • 14
  • 21
  • I were looking for option one where i can somehow embed the key at build time. I have to create separate dll as per my licensing. I am using some advance paid obfuscation tool to obfuscate the dll. – Kamran Shahid Jul 30 '20 at 18:49
0

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>
Matteo Umili
  • 3,412
  • 1
  • 19
  • 31
  • This is a great option for non-sensitive values. However, this requires storing the key in code, which I presume is the motivation for wanting to pass it in at build-time, e.g. from a restricted build server. – Adam Oct 24 '22 at 16:04
0

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
  }
}
yugami
  • 379
  • 1
  • 10
  • I wish this is that easy :) – Kamran Shahid Aug 05 '20 at 18:17
  • @KamranShahid With a pre-build script why isn't it? – yugami Aug 05 '20 at 18:23
  • you have worked with .net core 3.1 and done some parameterized build ever? Please try to read question context – Kamran Shahid Aug 05 '20 at 18:30
  • @KamranShahid Yes I have, in fact I tested the solution I proposed and have a fully functioning sample- please try to put some effort in. – yugami Aug 05 '20 at 18:56
  • Don't need your input about putting effort or not. what's the use of partial class and myspecialstring in it. and where it is being passed at compile time? swapping a cs file is different thing while i was looking for inject a parameter at build time which will be injected in the code, compiled, build created as per required platform from dotnet publish -c release -r linux-x64. your answer have nothing new – Kamran Shahid Aug 05 '20 at 19:19
  • The file is swapped in at compile tile based on the parameters used to build. AKA the pre-build script something like this copy $(SolutionDir)$(ProjectName)\$(PlatformName)\test.cs $(SolutionDir)$(ProjectName)\test.cs – yugami Aug 05 '20 at 19:21