You've just pulled up a terminal, made a quick change to a service, and now you need to test it with a specific environment variable. In .NET 10, that meant either exporting a shell variable, editing launchSettings.json, or reaching for a .env file loader. In .NET 11 Preview 3, you type dotnet run -e MY_VAR=value and move on with your day.
That's the theme running through the SDK improvements in this preview: small, well-placed quality-of-life changes that remove friction from the daily development loop. Multi-file support for file-based apps, inline environment variables, smarter dotnet watch behaviour, and CLI-editable solution filters all landed in the same release. None of them is a headline-grabbing language feature, but collectively they reshape how you interact with the CLI.
Inline environment variables with dotnet run -e
The new -e flag on dotnet run lets you pass environment variables directly from the command line:
dotnet run -e ASPNETCORE_ENVIRONMENT=Development -e LOG_LEVEL=Debug
Variables passed this way take precedence over ambient environment variables, System.CommandLine env directives, and any environmentVariables block in the active launch profile. That precedence order matters — it means you can override a launch profile's defaults without touching the file.
Under the hood, these values are also available to MSBuild as RuntimeEnvironmentVariable items. That opens the door to conditional build logic based on runtime intent, though most developers will simply use it to toggle configuration during local testing.
# Quick test against a staging database
dotnet run -e ConnectionStrings__Default="Server=staging;Database=Orders;..." -e ASPNETCORE_ENVIRONMENT=Staging
// TIP
The -e flag works with file-based apps too. dotnet run app.cs -e MY_VAR=value passes the variable through without any project file ceremony.
Before this feature, the common workarounds were:
- Shell exports — pollute the session and are easy to forget about
- Launch profiles — require editing JSON and are tied to the project, not the developer
- Third-party
.envloaders — add a dependency for something the SDK should handle
The -e flag is the cleanest solution for ad-hoc overrides, and it's one of those features that, once you've used it, you'll wonder how you managed without.
File-based apps go multi-file with #:include
File-based apps — the dotnet run app.cs workflow introduced in .NET 10 — were intentionally limited to a single file in their first release. The team wanted to nail the single-file experience before tackling the complexity of multiple files. With Preview 3, that limitation is gone.
The new #:include directive lets you pull in additional source files:
#:include helpers.cs
#:include models/customer.cs
var customer = new Customer("Acme Corp", "GB");
Console.WriteLine(Helpers.FormatSummary(customer));
public record Customer(string Name, string CountryCode);
public static class Helpers
{
public static string FormatSummary(Customer c) =>
$"{c.Name} ({c.CountryCode})";
}
Run it as before:
dotnet run app.cs
The directive accepts relative paths from the file containing it. You can include files from subdirectories, and each included file can itself contain #: directives like #:package or #:property.
Why this matters
The single-file constraint pushed developers toward an awkward choice: either cram everything into one file or graduate to a full .csproj project. Multi-file support fills the gap between throwaway scripts and proper solutions. Realistic use cases include:
- CLI tools and utilities that need a few model types alongside the main logic
- Prototypes that span a service class and a data model
- Automation scripts that share helper functions across entry points
You still get native AOT by default, the #:package directive for NuGet references, and the ability to convert to a full project later with dotnet project convert app.cs.
// WARNING
File-based apps still respect Directory.Build.props and Directory.Build.targets from parent directories. If your file-based app lives inside a repository with these files, their settings will affect your build. Keep file-based apps in isolated directories to avoid surprises.
Combining with launch profiles
File-based apps support a flat launch settings format using [appname].run.json:
{
"profiles": {
"dev": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Run with a specific profile:
dotnet run app.cs --launch-profile dev
When multiple file-based apps share a directory, each gets its own .run.json file — foo.run.json for foo.cs, bar.run.json for bar.cs.
Solution filters from the CLI
Large repositories often contain dozens of projects, but individual developers typically work on a subset. Solution filters (.slnf files) solve this by defining a view over a solution that only loads specific projects. Until now, creating and editing them required Visual Studio or hand-editing JSON.
Preview 3 brings solution filter management to the dotnet sln command:
# Create a new solution filter
dotnet new slnf --name MyApp.slnf
# Add projects to the filter
dotnet sln MyApp.slnf add src/Api/Api.csproj
dotnet sln MyApp.slnf add src/Core/Core.csproj
# List projects in the filter
dotnet sln MyApp.slnf list
# Remove a project
dotnet sln MyApp.slnf remove src/Core/Core.csproj
The commands mirror the existing dotnet sln operations you already know for .sln files. This is particularly useful in CI/CD pipelines where you want to build or test a subset of a solution without maintaining a separate .sln file.
// NOTE
Solution filters reference an underlying .sln file. The dotnet sln commands operate on the filter's project list, not the parent solution's.
A smarter dotnet watch
dotnet watch has been the backbone of the .NET inner development loop for years, but it had a few rough edges that Preview 3 addresses.
Crash recovery
Previously, when your application crashed during a dotnet watch session, you'd see the error and then... nothing. You had to manually restart the watcher or save a file to trigger a rebuild. Now, dotnet watch automatically relaunches the application on the next relevant file change after a crash.
This sounds minor, but it changes the workflow significantly. You see the crash, fix the code, save the file, and the app restarts. No context switch to the terminal to press Ctrl+C and re-run the command.
Aspire integration
dotnet watch can now orchestrate Aspire AppHost projects. This was a notable gap — developers using Aspire for local development had to choose between hot reload and the Aspire dashboard. With this integration, you get both: file changes trigger hot reload across the full Aspire application model, including dependent services.
dotnet watch --project src/AppHost/AppHost.csproj
Windows desktop improvements
Ctrl+C handling is improved for Windows Forms and WPF applications. Previously, dotnet watch could leave orphaned processes when terminating desktop apps. The shutdown sequence is now cleaner, properly signalling the application before forcefully terminating it.
Smaller SDK installers
This one won't change your daily workflow, but it's worth noting: the .NET 11 SDK installer is meaningfully smaller on Linux and macOS. The team found that 35% of the SDK directory consisted of duplicate files — 816 files totalling 140 MB on Linux x64. By replacing duplicates with symbolic links, the installers shrink considerably:
| Platform | Artifact | .NET 10 | .NET 11 Preview 2 | Reduction |
|---|---|---|---|---|
| linux-x64 | tarball | 230 MB | 189 MB | 17.8% |
| linux-x64 | deb | 164 MB | 122 MB | 25.6% |
| linux-x64 | rpm | 165 MB | 122 MB | 26.0% |
| linux-x64 | containers | varies | varies | 8-17% |
Windows deduplication is planned for a future preview. For container-based deployments and CI runners where the SDK is downloaded frequently, this is a meaningful improvement.
Improved CA1873 diagnostics
The CA1873 analyser (which warns about potentially expensive arguments in logging calls) received two improvements that make it more practical to leave enabled.
Reduced false positives. Property accesses, GetType(), GetHashCode(), and GetTimestamp() calls are no longer flagged. The analyser now only fires for Information-level logging and below by default, since warning, error, and critical paths are rarely hot.
Specific diagnostic messages. Instead of a generic "evaluation of this argument may be expensive" message, the diagnostic now tells you why it flagged the call:
// Before
warning CA1873: Evaluation of this argument may be expensive and unnecessary if logging is disabled
// After
warning CA1873: Evaluation of this argument may be expensive and unnecessary if logging is disabled (method invocation)
The nine specific reasons include method invocation, object creation, array creation, boxing conversion, string interpolation, collection expression, anonymous object creation, await expression, and with expression. This specificity makes it much easier to decide which warnings warrant attention.
Other CLI changes worth knowing
A few smaller improvements round out the SDK update:
dotnet format --framework— you can now target a specific framework when formatting multi-targeted projectsdotnet testin MTP mode — supports--artifacts-pathfor controlling where test results landdotnet tool exec— no longer prompts for extra approval when running tools, removing a friction point in automated scripts
Common pitfalls
Mixing -e with launch profiles. Variables passed via -e override launch profile values, which is usually what you want. But if you're debugging a "why is my config wrong" issue, remember to check both sources.
File-based app cache staleness. The build cache for file-based apps is based on source content and directives, but changes to Directory.Build.props in parent directories don't always invalidate the cache. If your file-based app behaves unexpectedly after changing implicit build files, run dotnet clean file-based-apps to clear the cache.
Solution filter project paths. Solution filter entries are relative to the .sln file, not the .slnf file. If your filter lives in a different directory from the solution, the paths in the filter still reference the solution's location.
Aspire watch and port conflicts. When using dotnet watch with Aspire, ensure your AppHost isn't configured with fixed ports that conflict with the restarted instances. Dashboard and service ports should use dynamic allocation during development.
Summary
dotnet run -epasses environment variables inline, overriding launch profiles and shell state#:includeenables multi-file file-based apps, bridging the gap between scripts and full projectsdotnet slnfor.slnfbrings solution filter management to the CLIdotnet watchgains crash recovery, Aspire integration, and better Windows desktop cleanup- SDK installers are up to 26% smaller on Linux through file deduplication
- CA1873 is less noisy and more specific about why it flagged a logging argument