8

I want to access a MSBuild variable inside an unit test, which is a .NET 4.5 class library project (classic csproj), but I failed to find any articles discussing a way to pass values from MSBuild into the execution context.

I thought about setting an environment variable during compilation and then reading that environment variable during execution, but that seems to require a custom task to set the environment variable value and I was a bit worried about the scope of the variable (ideally, I only wanted it to be available to the currently executing project, not globally).

Is there a known solution to reading an MSBuild property from inside a DLL project in runtime? Can MSBuild properties be "passed as parameters" during execution somehow?

Martin Ullrich
  • 94,744
  • 25
  • 252
  • 217
julealgon
  • 7,072
  • 3
  • 32
  • 77
  • What is wrong with ConditionalAttribute? – C.J. Jul 01 '19 at 17:20
  • Not sure how `ConditionalAttribute` would help my case @CJohnson. It only allows you to check a constant key (like `DEBUG` or `RELEASE`), and I want an actual value from MSBuild that is dynamic. – julealgon Jul 01 '19 at 18:28
  • While my question was marked as duplicate, the linked answer only provides an answer for .Net Core, not for .Net Framework. I was able to make it work for .Net Framework by [adding some extra logic in my csproj file however](https://stackoverflow.com/a/4306142/1946412). – julealgon Jul 02 '19 at 12:58
  • @julealgon please add such details to questions, it is easy to overlook tags. Do you want (me/us) to remove the duplicate mark and maybe add your own answer? – Martin Ullrich Jul 02 '19 at 13:01
  • @MartinUllrich I've added the .Net4.5 tag now. I think you'd either need to update your answer on the other thread to include the bits necessary to solve this in .Net, or you could unmark this as a duplicate. I'm fine with adding the answer myself. – julealgon Jul 02 '19 at 21:06

3 Answers3

10

I finally made it work by using the same code generation task that is used by default in .Net Core projects. The only difference is that I had to manually add the Target in the csproj file for it to work, as code creation is not standard for framework projects:

<Target Name="BeforeBuild">
  <ItemGroup>
    <AssemblyAttributes Include="MyProject.SolutionFileAttribute">
      <_Parameter1>$(SolutionPath)</_Parameter1>
    </AssemblyAttributes>
  </ItemGroup>
  <WriteCodeFragment AssemblyAttributes="@(AssemblyAttributes)" Language="C#" OutputDirectory="$(IntermediateOutputPath)" OutputFile="SolutionInfo.cs">
    <Output TaskParameter="OutputFile" ItemName="Compile" />
    <Output TaskParameter="OutputFile" ItemName="FileWrites" />
  </WriteCodeFragment>
</Target>

The lines with Compile and FileWrites are there for it to play nicely with clean and such (see linked answers in my comments above). Everything else should be intuitive enough.

When the project compiles, a custom attribute is added to the assembly, that I can then retrieve using normal reflection:

Assembly
    .GetExecutingAssembly()
    .GetCustomAttribute<SolutionFileAttribute>()
    .SolutionFile

This works really well and allows me to avoid any hardcoded searches for the solution file.

julealgon
  • 7,072
  • 3
  • 32
  • 77
1

I think you have a couple of options:

  • Use environment variables, like you already suggested. A custom task maybe required to do that, but it is easy to do, without any extra assemblies on your part. The required global visibility might be an issue tough; consider parallel builds on a CI machine, for example.
  • Write a code fragment during build and include that into your resulting assembly (something akin to what you have already found under the link you suggested in your comments.
  • Write a file (even app.config) during build that contains settings reflecting the MSBuild properties you need to have; read those during test runs.

(BTW, what makes little sense, is to attempt to read the MSBuild project file again during runtime (using the Microsoft.Build framework). For once that is a whole lot of work to begin with, for little gain IMHO. And even more important, you most likely - depending on the complexity and dependencies of your properties - need to make sure you invoke the MSBuild libraries with the same properties that where present during the actual build. Arguably, that might put you back were you started from.)

The last two options are best suited because they share equal traits: they are scoped only to the build/test run you currently have (i.e. you could have parallel running builds without interference).

I might go for the third, because that seems to be the easiest to realize.

In fact I have done so on a larger project I've been working on. Basically, we had different environments (database connection strings, etc.) and would select those as a post build step by basically copying the specific myenv.config to default.config. The tests would only ever look for a file named default.config and pick up whatever settings are set in there.

Christian.K
  • 47,778
  • 10
  • 99
  • 143
1

Another version, compiled from several internet sources, get environment variable when building, then use its value in code

file AssemblyAttribute.cs

namespace MyApp
{
    [AttributeUsage(AttributeTargets.Assembly)]
    public class MyCustomAttribute : Attribute
    {
        public string Value { get; set; }
        public MyCustomAttribute(string value)
        {
            Value = value;
        }
    }
}

file MainForm.cs

var myvalue = Assembly.GetExecutingAssembly().GetCustomAttribute<MyCustomAttribute>().Value;

file MyApp.csproj, at the end (get %USERNAME% environment variable in build, generate SolutionInfo.cs file, automatically include it to build)

  <Target Name="BeforeBuild">
    <ItemGroup>
      <AssemblyAttributes Include="MyApp.MyCustomAttribute">
        <_Parameter1>$(USERNAME)</_Parameter1>
      </AssemblyAttributes>
    </ItemGroup>
    <WriteCodeFragment AssemblyAttributes="@(AssemblyAttributes)" Language="C#" OutputFile="SolutionInfo.cs">
      <Output TaskParameter="OutputFile" ItemName="Compile" />
      <Output TaskParameter="OutputFile" ItemName="FileWrites" />
    </WriteCodeFragment>
  </Target>
Fl0
  • 166
  • 6