A basic example of how to use MSBuild files in NuGet packages. This sample is geared toward use by .NET MAUI developers specifically but the overall concepts are applicable to .NET in general.
There are situations where, as a .NET MAUI NuGet package author, you may want to automate project configuration or perform custom build logic to simplify the setup process for users and reduce opportunity for errors and inconsistencies. For example, configuring some project settings, including package files with the requisite build actions, or performing other necessary operations through MSBuild tasks.
The MSBuild system is highly customizable and extensible. It enables us to split build configuration across multiple standalone project files, typically with .props and .targets extensions, for use in multiple projects.
In MSBuild, .props and .targets files serve different purposes and are used at different stages of the build process. Typically, .props
files are used to define properties for use throughout the build process and are imported early on. The .targets
files, on the other hand, can contain item definitions, properties, targets, and tasks which get imported later in the build process.
NuGet packages can optionally include MSBuild .props and .targets files within its build folders. When projects consume packages containing build files in the form of <package_id>.<extension>
, those files effectively get imported automatically. Files in the root build folders are considered suitable for all frameworks whereas framework specific build folders are used where applicable.
Packages like Microsoft.Maui.Controls.Build.Tasks use this technique to apply TFM (target framework moniker) specific settings. For example, the Microsoft.Maui.Controls.Build.Tasks.targets
file (within the buildTransitive > net6.0-ios10.0 folder) defines a PropertyGroup
that conditionally sets MtouchLink
to None
.
<Project>
<PropertyGroup>
<MtouchLink Condition="'$(MtouchLink)' == '' and '$(Configuration)' == 'Debug' and '$(UseInterpreter)' == 'true'">None</MtouchLink>
</PropertyGroup>
</Project>
This first-principles example shows how to add a .targets
file to a package, created from a .NET MAUI class library project, alongside the resulting assembly and other sample content via csproj configuration. In this case, the example SamplePackage.targets file just includes some assets from the package with the MauiFont and MauiImage build actions as a means to demonstrate the concept.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<MauiFont Include="$(MSBuildThisFileDirectory)..\fonts\Font Awesome 6 Free-Regular-400.otf" Visible="False">
<Link>%(Filename)%(Extension)</Link>
</MauiFont>
<MauiImage Include="$(MSBuildThisFileDirectory)..\images\sample_bot.svg" Visible="False">
<Link>%(Filename)%(Extension)</Link>
</MauiImage>
</ItemGroup>
</Project>
The package is then consumed by a .NET MAUI application within the same solution to demonstrate the .targets
configuration has been imported successfully.
The MauiImage items can be used in MainPage.xaml with no additional setup.
<Image
Source="sample_bot.png"
HeightRequest="185"
Aspect="AspectFit" />
The MauiFont must get registered in MauiProgram.cs as an additional step.
builder.ConfigureFonts(fonts =>
{
fonts.AddFont("Font Awesome 6 Free-Regular-400.otf", "FontAwesome");
});
Once registered, the font alias and glyphs can be used by MainPage.xaml.
<Label
Text=""
FontFamily="FontAwesome" />
To embellish the sample a little, the SamplePackage project includes some additional components:
- AppBuilderExtensions.cs: Extension method for use with
MauiAppBuilder
encapsulating the font registration - BrandColors.xaml: ResourceDictionary defining some color resources for use by the test app
- FontAwesomeGlyphs.cs: Constant strings for the
FontAlias
andCode
glyph - PackageImages.cs: Constant strings for resolving the images
The UseSamplePackage()
method from AppBuilderExtensions.cs gets called from MauiProgram.cs.
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseSamplePackage()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
return builder.Build();
}
}
The FontAwesomeGlyphs and PackageImages classes can be optionally used in MainPage.xaml instead of raw string values.
<Image
Source="{x:Static sp:PackageImages.DotnetBot}"
HeightRequest="185"
Aspect="AspectFit" />
<Label
Text="{x:Static sp:FontAwesomeGlyphs.Code}"
FontFamily="{x:Static sp:FontAwesomeGlyphs.FontAlias}"
TextColor="{StaticResource PrimaryBrandColor}" />
<Label
Text="Welcome to .NET Multi-platform App UI"
TextColor="{StaticResource PrimaryBrandColor}" />
If you have freshly cloned the repository, you must perform the following steps before loading the SamplePackage solution:
-
In Terminal, change the directory to the root of this git repository
-
Use
dotnet build
to create the initial NuGet package for the SamplePackage project.dotnet build src/SamplePackage/SamplePackage.csproj
Important
Building the SamplePackage project results in the NuGet package getting created automatically. The package gets output to a local git ignored artifacts
folder under src
. The .NET MAUI test app resolves the package from this directory instead of using a project reference. See notable details for more information.
- Open the SamplePackage solution in Visual Studio / Visual Studio Code
- Set the PackageConsumerApp as the startup project
- Build and run the app.
Important
After making changes to the SamplePackage project, to test it using the PackageConsumerApp project:
- Build SamplePackage
- Rebuild and run the PackageConsumerApp project
The SamplePackage solution is comprised of two projects. The SamplePackage class library and a .NET MAUI test app, PackageConsumerApp. To demonstrate the relevant NuGet and MSBuild concepts, the test app references the generated NuGet package rather than the class library project itself.
The solution has been configured to simplify updating the test app to use new versions of the library project NuGet package. In this case, a new package gets generated automatically when building the class library project. Changes to the package are then observed after you rebuild the test app or when you start debugging it. The approach being used is described in more detail in the NuGet DevLoop Sample repo.
The SampleProject csproj file defines some typical package metadata and some other properties relating to the pack operation. Notably, PackageVersion
, PackageOutputPath
, and GeneratePackageOnBuild
.
<PropertyGroup>
...
<PackageVersion>0.0.99999-sample</PackageVersion>
<PackageOutputPath>../artifacts/</PackageOutputPath>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
In addition to the standard packaging properties, an <ItemGroup>
includes the files from the Content folder so they are packed into the appropriate folders within the resulting package.
<ItemGroup>
...
<None Include="Content\font_awesome\*" Pack="true" PackagePath="\fonts" />
<None Include="Content\images\*" Pack="true" PackagePath="\images" />
<None Include="Content\resources\*" Pack="true" PackagePath="\resources" />
<None Include="Content\SamplePackage.targets" Pack="true" PackagePath="\build\SamplePackage.targets" />
<None Include="Content\SamplePackage.targets" Pack="true" PackagePath="\buildTransitive\SamplePackage.targets" />
</ItemGroup>
The None
build action is used so these files are not included in the build process. The Pack
attribute determines that the items should be packed. Their paths within the package have been specififed using the PackagePath
attribute.
Lastly, the Microsoft.SourceLink.GitHub
and System.Management
packages are referenced. The former links the source code to the corresponding files in the repo for easier debugging and the latter is specific to Windows builds providing access to system management information.
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" IsImplicitlyDefined="true" />
<PackageReference Include="System.Management" Version="7.0.0" Condition="$(TargetFramework.Contains('-windows')) == true" />
</ItemGroup>
The PackageConsumerApp test app references the SamplePackage
NuGet package built in the same solution.
<ItemGroup>
<PackageReference Include="SamplePackage" Version="0.0.99999-sample" />
</ItemGroup>
This sample uses the Font Awesome 6 Free-Regular-400
otf provided under MIT license by Font Awesome.