Declaring InternalsVisibleTo in the csproj

 
 
  • Gérald Barré

While I prefer testing the public API of an assembly, it's sometimes useful to test the implementation details. So, an attribute that I often use is [assembly: InternalsVisibleTo("MyAssembly.Tests")] to allow the test assembly to use internal classes or methods. A convention is to declare assembly attribute in the file AssemblyInfo.cs. With the new SDK-based project, there is no AssemblyInfo.cs by default to add assembly attributes. Of course, you can still create this file by your own, but why don't take advantage of the new SDK-based project to add this attribute. It's even possible to add it automatically to all your project!

If you remember the post I've written about Getting the date of build of a .NET assembly at runtime a few months ago where I also use the csproj file to add an assembly attribute. You can use the same way here:

csproj (MSBuild project file)
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
    <_Parameter1>ClassLibrary1.Tests</_Parameter1>
    </AssemblyAttribute>
  </ItemGroup>
</Project>

When compiling the project, a file will be generated by MSBuild in the obj folder:

C#
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System;
using System.Reflection;

// 👇 The attribute is here!
[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("ClassLibrary1.Tests")]
[assembly: System.Reflection.AssemblyCompanyAttribute("ClassLibrary1")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("ClassLibrary1")]
[assembly: System.Reflection.AssemblyTitleAttribute("ClassLibrary1")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

// Generated by the MSBuild WriteCodeFragment class.

We can leverage the possibilities of MSBuild to do something more generics. The test projects often have the same naming convention such as {AssemblyName}.Tests. MSBuild exposes the name of the assembly so we can reuse it to declare the value of the attribute:

csproj (MSBuild project file)
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
    <_Parameter1>$(AssemblyName).Tests</_Parameter1> <!-- We use the value of AssemblyName to declare the value of the attribute -->
    </AssemblyAttribute>
  </ItemGroup>
</Project>

Let's go a step further and add it automatically to all projects. Also, we want to provide a way to add custom assembly names when adding the attribute. Create a file named Directory.Build.targets at the root of the repository:

ClassLibrary1.sln
Directory.Build.targets
📁ClassLibrary1
 ┗ ClassLibrary1.csproj
📁ClassLibrary1.Tests
 ┗ ClassLibrary1.Tests.csproj

Add the following content to the file:

csproj (MSBuild project file)
<Project>
  <Target Name="AddInternalsVisibleTo" BeforeTargets="BeforeCompile">
    <!-- Add default suffix if there is no InternalsVisibleTo or InternalsVisibleToSuffix defined -->
    <ItemGroup Condition="@(InternalsVisibleToSuffix->Count()) == 0 AND @(InternalsVisibleTo->Count()) == 0">
      <InternalsVisibleToSuffix Include=".Tests" />
    </ItemGroup>

    <!-- Handle InternalsVisibleTo -->
    <ItemGroup Condition="'@(InternalsVisibleTo->Count())' &gt; 0">
      <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
        <_Parameter1>%(InternalsVisibleTo.Identity)</_Parameter1>
      </AssemblyAttribute>
    </ItemGroup>

    <!-- Handle InternalsVisibleToSuffix -->
    <ItemGroup Condition="@(InternalsVisibleToSuffix->Count()) &gt; 0">
      <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
        <_Parameter1>$(AssemblyName)%(InternalsVisibleToSuffix.Identity)</_Parameter1>
      </AssemblyAttribute>
    </ItemGroup>
  </Target>
</Project>

This file is more generic and allows more ways to declare which attributes to generate. You can control it from your csproj:

csproj (MSBuild project file)
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <InternalsVisibleTo Include="CustomTest1" /> <!-- [assembly: InternalsVisibleTo("CustomTest1")] -->
    <InternalsVisibleTo Include="CustomTest2, PublicKey=abc" /> <!-- [assembly: InternalsVisibleTo("CustomTest2, PublicKey=abc")] -->
    <InternalsVisibleTo Include="$(AssemblyName).Custom" /> <!-- [assembly: InternalsVisibleTo("ClassLibrary1.Custom")] -->

    <InternalsVisibleToSuffix Include=".Tests" /> <!-- [assembly: InternalsVisibleTo("ClassLibrary1.Tests")] -->
    <InternalsVisibleToSuffix Include=".FunctionalTests" /> <!-- [assembly: InternalsVisibleTo("ClassLibrary1.FunctionalTests")] -->
  </ItemGroup>

</Project>

If you don't set anything in your csproj, it will automatically add <InternalsVisibleToSuffix Include=".Tests" />.

You know have a generic way to add InternalsVisibleTo attributes in your projects.

#Package the target file as a NuGet package

If you want to use the targets file in multiple projects, you can wrap it in a NuGet package. I knew it is possible as some packages do that. However, I had no idea how to do it, so I've searched on Google. I found that Thomas Levesque made something very similar in his GitHub project. His package doesn't have the same approach to declaring the attributes, but it is very similar. So, I started from his code and made some adjustments to fit my needs. Here're the code and the package:

csproj (MSBuild project file)
<Project Sdk="Microsoft.NET.Sdk">

  <!-- Add the NuGet package -->
  <ItemGroup>
    <PackageReference Include="Meziantou.MSBuild.InternalsVisibleTo" Version="1.0.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <!-- Declare the InternalVisibleTo attributes to generate -->
  <ItemGroup>
    <InternalsVisibleTo Include="CustomTest1" />
    <InternalsVisibleTo Include="CustomTest2" />

    <InternalsVisibleToSuffix Include=".Tests" />
    <InternalsVisibleToSuffix Include=".FunctionalTests" />
  </ItemGroup>

</Project>

#Update: .NET 5

Starting with .NET 5, you can use the <InternalsVisibleTo> without adding any NuGet package:

csproj (MSBuild project file)
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <InternalsVisibleTo Include="$(AssemblyName).Tests" />
  </ItemGroup>
</Project>

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub