Over the years, I've relied on Meziantou.DotNet.CodingStandard, a NuGet package, to establish a consistent baseline across all my .NET projects. This approach includes style enforcement, essential analyzers, sensible build defaults, and additional tooling. This technique has been adopted by multiple comipanies and has significantly improved code quality and developer productivity, particularly when developers switch between different codebases. Moreover, it eliminates hundreds of lines of boilerplate code and configuration from each project, enabling developers to focus on delivering business value.
One of the main benefit of removing boilerplate is that you can easily update the baseline. When a new analyzer version is released, or a new best practice emerges, I can update the coding standard package, and all projects automatically benefit from the improvements without any manual intervention.
Despite its success, this NuGet package approach has inherent limitations. Most notably, it cannot dynamically add packages based on project characteristics. For instance, test projects would benefit from specialized test extensions and loggers, while web projects require default services such as health checks and OpenTelemetry instrumentation. The static nature of NuGet references prevents this level of intelligent configuration.
MSBuild SDKs offer a compelling solution to these constraints. Unlike traditional NuGet packages, SDKs enable dynamic project configuration and can intelligently add packages or files based on project type. This capability allows me to create tailored setups for different scenarios while preserving the consistent baseline that has proven so valuable.
#What are MSBuild SDKs?
MSBuild SDKs are a way to extend the capabilities of MSBuild projects. They are referenced differently than regular NuGet packages and can execute code before the main build process. This allows for more dynamic configurations, such as adding packages or files based on the project type.
An MSBuild SDK is essentially a collection of .props and .targets files that are automatically imported into your project at specific points during the build process. When you reference an SDK, MSBuild imports Sdk.props at the very beginning of your project file and Sdk.targets at the very end. This positioning gives SDKs significant control over the build process, allowing them to:
- Set default properties before they can be overridden by the project
- Add items and modify the build pipeline after the project has been processed
- Conditionally include additional MSBuild files based on project properties
- Import other SDKs or NuGet packages programmatically
.NET SDK includes multiple built-in SDKs, such as Microsoft.NET.Sdk, Microsoft.NET.Sdk.Web, and Microsoft.NET.Sdk.Worker. These SDKs are provided as part of the .NET SDK installation. You can also create your own custom SDKs by packaging them as NuGet packages with a specific structure: your package must contain a Sdk folder at the root with Sdk.props and/or Sdk.targets files inside it.
The key advantage of SDKs over traditional NuGet packages is their execution timing and scope. While NuGet packages are restored and referenced during the build, SDKs are resolved and imported before the project is fully evaluated. This early execution enables them to influence fundamental aspects of the build, such as determining which files to compile, what target framework to use, or which additional packages to include based on project characteristics.
#What features does Meziantou.NET.Sdk provide?
My custom SDK, Meziantou.NET.Sdk, wraps existing Microsoft SDKs and applies common settings and packages to .NET projects based on their type, eliminating repetitive configuration while enforcing best practices.
Everything can be overridden in the project file if needed. The goal is to provide sensible defaults that work for most scenarios, reducing boilerplate and allowing developers to focus on writing code. Howvever, if a project has specific requirements, it can easily override the defaults set by the SDK.
##Features for all projects
- Naming conventions and coding style: An embedded
.editorconfig ensures consistent formatting for C# and VB.NET across the codebase - Analyzers with preconfigured rules:
- MSBuild properties: Automatically sets properties like
TreatWarningsAsErrors in CI environments, enabling strict mode, or AccelerateBuildsInVisualStudio for faster builds in Visual Studio - NuGet audit: Enables security vulnerability scanning for dependencies
- RollForward behavior: Sets
RollForward to Major by default for .NET tools, ensuring compatibility with newer runtimes - npm integration: Automatically restores npm packages when
package.json is present - Intelligent TargetFramework: Sets
TargetFramework based on the SDK version if not explicitly specified - SourceLink configuration: Enables source debugging for NuGet packages
- Enhanced global usings: Adds commonly used namespaces based on project patterns
- Improved binlog diagnostics: Embeds additional files (
.editorconfig, BannedSymbols.txt) for easier troubleshooting - NuGet package metadata: Automatically populates author, icon, readme, and license information when missing
##Features for test projects
The Sdk detects test projects based on the presence of common properties and applies additional configurations for VSTest and Microsoft Testing Platform:
- Test loggers: Automatically configures appropriate test output formats
- GitHub Actions integration: Adds
GitHubActionsTestLogger when running in GitHub Actions - Code coverage: Enables coverage collection with sensible defaults
- Test extensions: Includes crash dump collection and timeout detection using blame
- xUnit integration: Adds xUnit global usings when the package is detected
In this end, running dotnet test is sufficient. There is no need for additional --blame or --logger arguments as there are added by the SDK.
##Features for web projects
The Sdk adds dependencies and configures common services for ASP.NET Core projects (without requiring any code changes):
- Health checks: Adds default health check endpoints
- OpenTelemetry: Configures common instrumentations (ASP.NET Core, HTTP Client, SQL Client, custom activity sources) and exports traces/metrics/logs to an OTEL collector if
OTEL_EXPORTER_OTLP_ENDPOINT is set - Resilient HTTP clients: Includes retry policies
- Set
user-agent headers for all HttpClient created via dependency injection - Standard middleware: Automatically adds HTTPS redirection, HSTS, static file serving, and more
- JSON conventions: Configures naming conventions, null value handling, etc.
Note that this one is very opinionated. You may also want to add authentication, CORS, rate limiting, http logging, or other middleware according to your needs.
##Example: A complete web application
Here's a fully functional web project using the SDK:
Sample.csproj (csproj (MSBuild project file))
<Project Sdk="Meziantou.NET.Sdk.Web/1.0.31" />
<!-- Target framework, nullability, and many dependencies is automatically set -->
Program.cs (C#)
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
This minimal example automatically includes health checks at /health, OpenTelemetry traces/metrics/logs export to an OTEL collector if OTEL_EXPORTER_OTLP_ENDPOINT is set, resilient HTTP clients, and all the standard middleware—without any explicit configuration.
#How to reference a custom MSBuild SDK
There are several ways to reference custom MSBuild SDKs in your projects. Each method has different implications for versioning and project structure.
##Direct reference with version
The simplest approach embeds the version directly in the project file:
XML
<Project Sdk="Meziantou.NET.Sdk/1.0.42">
</Project>
This method is straightforward and makes the SDK version explicit in each project. However, updating the SDK across multiple projects requires modifying each file individually.
##Centralized version management with global.json
For better version control across a solution, define SDKs in a global.json file at the repository root:
JSON
{
"msbuild-sdks": {
"Meziantou.NET.Sdk": "1.0.42"
}
}
Then reference the SDK without a version in your project files:
XML
<Project Sdk="Meziantou.NET.Sdk">
</Project>
This approach centralizes SDK version management, making it easier to update all projects simultaneously by modifying a single file.
##Using the Sdk element
Alternatively, you can use the Sdk element to combine multiple SDKs:
XML
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Meziantou.NET.Sdk" Version="1.0.42" />
</Project>
This method differs from the previous approaches because the SDK specified in the Sdk element is resolved after the main SDK. This changes the order in which properties are evaluated, which may affect build behavior. This method is particularly useful in Directory.Build.props files to apply the SDK to all projects in a repository without modifying individual project files.
#Should you create your own SDK?
Creating a custom MSBuild SDK can be a powerful way to enforce coding standards, add packages dynamically, and configure .NET projects consistently. However, it's essential to weigh the benefits against the complexity it introduces. Please don't use Meziantou.NET.Sdk as-is; instead, consider forking it and adapting it to your specific needs.
#Additional resources
Do you have a question or a suggestion about this post? Contact me!