I have this line inside my csproj
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TextTemplating\Microsoft.TextTemplating.targets" />
But when I run dotnet test it seems to resolve MSBuildExtensionsPath with
C:\Program Files\dotnet\sdk\8.0.204\
I tried to set an environment MSBuildExtensionsPath and set it to the expected path which is
C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\
Nothing worked so far. I'm running this as TeamCity build step. Also tried to use environment variables in TeamCity.
The error which I receive is
error MSB4019: The imported project "C:\Program Files\dotnet\sdk\8.0.204\Microsoft\VisualStudio\v17.0\TextTemplating\Microsoft.TextTemplating.targets" was not found. Confirm that the expression in the Import declaration "C:\Program Files\dotnet\sdk\8.0.204\Microsoft\VisualStudio\v17.0\TextTemplating\Microsoft.TextTemplating.targets" is correct, and that the file exists on disk.
Build with MSBuild
From the comments a solution is being used with a mix of .Net 8, .Net Framework 4.8, and .Net Standard projects.
The dotnet
CLI can't build .Net Framework.
To build the mixed solution at the command line (or via the Team City Agent), a "Developer Command Prompt for VS" or its equivalent, and MSBuild are needed.
The Dev Cmd Prompt shortcut runs the VsDevCmd.bat
batch file which adds to the PATH
and adds some environment variables. If VsDevCmd.bat
has been run, then msbuild
can be found via the PATH
and msbuild
can find the Visual Studio install directory. $(MSBuildExtensionsPath)
will be set appropriately, e.g. C:\Program Files\Microsoft Visual Studio\2022\BuildTools\MSBuild
.
The solution can be built with the command:
msbuild mixed.sln
Test the Built Test Assemblies
The dotnet
CLI embeds its own copies of several tools including MSBuild. The MSBuild embedded in dotnet
will use the SDK path for $(MSBuildExtensionsPath)
, e.g. C:\Program Files\dotnet\sdk\8.0.204\
.
The command dotnet test mixed.sln
will use the embedded MSBuild to attempt to evaluate the projects and determine if the projects need to be built. Evaluating the projects will fail because $(MSBuildExtensionsPath)
will be wrong.
However, dotnet test
forwards to MSBuild for projects, solutions, directories, and when no argument is supplied. When an assembly (.dll or .exe) is the supplied argument, dotnet test
forwards to the test runner and doesn't evaluate the project. In other words, run dotnet test
against the built test assemblies.
If there are circumstances that interfere with the above approach, there are other options.
Override MSBuildExtensionsPath
You can override MSBuildExtensionsPath
, but that may have unintended side effects.
The dotnet test
command when forwarding to MSBuild, will accept MSBuild arguments. An override may look like the following:
dotnet test mixed.sln -p:MSBuildExtensionsPath="C:\Program Files\Microsoft Visual Studio\2022\BuildTools\MSBuild"
The -p
option is defining a property.
Note that projects that expect extensions from the SDK directory will break. This may include standard SDK style project support so this may not be a viable approach.
Edit the Projects to Conditionalize the T4 Import
An Import
can have a Condition
:
<Import Project="example.targets" Condition="Exists('example.targets')" />
In the Microsoft.TextTemplating.targets
file, a $(T4BuildTasksAssemblyFile)
property is defined which can be expected to be reasonbably unique to the file. You can test this property to determine if an Import
was performed.
As an example:
<PropertyGroup>
<T4TargetsFile>Microsoft.TextTemplating.targets</T4TargetsFile>
<T4StdPath>$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TextTemplating\$(T4TargetsFile)</T4StdPath>
<T4AltPath>C:\Program Files\Microsoft Visual Studio\2022\BuildTools\Msbuild\Microsoft\VisualStudio\v17.0\TextTemplating\$(T4TargetsFile)</T4AltPath>
</PropertyGroup>
<Import Project="$(T4StdPath)" Condition="Exists('$(T4StdPath)')" />
<Import Project="$(T4AltPath)" Condition="'$(T4BuildTasksAssemblyFile)' == ''" />
If the Import
of $(T4StdPath)
was performed, then $(T4BuildTasksAssemblyFile)
willl not be empty and the Import
of $(T4AltPath)
will not be done.
If $(T4StdPath)
doesn't exist, then $(T4BuildTasksAssemblyFile)
will be empty and the Import
of $(T4AltPath)
will be attempted. If $(T4AltPath)
also doesn't exist, then there will be an MSB4019 error.
The pattern can be extended to test three or more paths:
<Import Project="$(T4Path0)" Condition="Exists('$(T4Path0)') and '$(T4BuildTasksAssemblyFile)' == ''" />
<Import Project="$(T4Path1)" Condition="Exists('$(T4Path1)') and '$(T4BuildTasksAssemblyFile)' == ''" />
<Import Project="$(T4Path2)" Condition="'$(T4BuildTasksAssemblyFile)' == ''" />
Instead of maintaining a copy of this MSBuild code in every project that uses Microsoft.TextTemplating.targets
, place this conditional logic code in its own .targets file and change the Import
of Microsoft.TextTemplating.targets
to an Import
of the conditional logic file.