While I prefer testing the public API of an assembly, it is sometimes useful to test implementation details. One attribute I often use is [assembly: InternalsVisibleTo("MyAssembly.Tests")] to allow the test assembly to access internal classes or methods. The conventional place to declare assembly attributes is AssemblyInfo.cs, but SDK-based projects no longer include this file by default. You can still create it yourself, but why not take advantage of the SDK-based project format to declare the attribute directly? It is even possible to add it automatically to all your projects!
In a previous post about Getting the date of build of a .NET assembly at runtime, I showed how to add an assembly attribute via the csproj file. You can use the same approach 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 take this further by leveraging MSBuild to make it more generic. Test projects often follow a naming convention such as {AssemblyName}.Tests. Since MSBuild exposes the assembly name, we can use it to derive the attribute value:
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, while also supporting custom assembly names. 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())' > 0">
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>%(InternalsVisibleTo.Identity)</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<!-- Handle InternalsVisibleToSuffix -->
<ItemGroup Condition="@(InternalsVisibleToSuffix->Count()) > 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 now 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 across multiple projects, you can wrap it in a NuGet package. I knew this was possible since some packages already do it, but I had no idea how, so I searched online. I found that Thomas Levesque had built something very similar in his GitHub project. His package uses a different approach to declaring the attributes, but it is very similar. I started from his code and adapted it to fit my needs. Here are 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!