For years, even the smallest C# program required a solution file, a project file, and a folder structure.
While Python and JavaScript developers could create a single file and run it in seconds, .NET developers had to create a solution and add a csproj file just to test an idea.
That changed with .NET 10, which introduced file-based apps.
Now you can run a C# file directly with dotnet run.
And in April 2026 in .NET 11 Preview 3 we received a support for using multiple files in file-based apps with the #:include directive.
This finally makes C# a real option for scripts, automation, internal tooling, and quick prototypes - without the overhead of a full project.
In this post, we will explore:
- What Are File-Based Apps in .NET
- Adding NuGet Packages and SDKs with Directives
- Splitting File-Based Apps Across Multiple Files
- Building an HTTP Health-Check Tool
- Building a Minimal API with EF Core and SQLite
- Converting a File-Based App to a Full Project
Let's dive in!
What Are File-Based Apps in .NET
Traditionally, every C# application required three things:
- Solution file (
*.sln) - Project file (
*.csproj) - Source code (
*.cs).
Even for a 10-line script, you had to create a new solution, add a project with dotnet new console, wait for the scaffolding to finish, and only then write your actual code.
Starting with .NET 10, you can skip all of that.
You can create a single .cs file and run it directly:
bashdotnet run main.cs
That's it. No project file. No solution file. Just one file.
This puts C# on equal footing with Python, JavaScript, Node.js, and other scripting languages. For CLI utilities, automation tasks, and one-off tools, this completely changes the workflow.
Let's see the simplest possible example. Create a file called hello.cs:
csharpConsole.WriteLine("Hello from a file-based app!"); Console.WriteLine($"Today is {DateTime.Now:dddd, MMMM d, yyyy}");
Then run it:
bashdotnet run hello.cs
Notice there is no Main method, no class Program, no using statement.
File-based apps build on top of top-level statements (introduced in C# 9) and implicit usings, so you can write code as if you were in a script.
Behind the scenes, the .NET CLI compiles your file in a temporary location and runs the resulting binary. The first run takes a moment, but subsequent runs are cached and fast.
I've used this many times in the last year for small tasks I would have otherwise written in Python (or created a solution project):
- Quick data transformations on JSON or CSV files
- Calling internal APIs to verify a deployment
- Running test scripts
- Parsing log files to extract patterns
Adding NuGet Packages and SDKs with Directives
A real script usually needs more than the BCL. You probably want JSON parsing, HTTP calls, or database access.
File-based apps support special # directives at the top of your file to declare dependencies.
The #:package Directive
Use #:package to reference any NuGet package:
csharp#:package [email protected] using Newtonsoft.Json; var data = new { Name = "Anton", Year = 2026 }; var json = JsonConvert.SerializeObject(data, Formatting.Indented); Console.WriteLine(json);
The format is #:package PackageName@Version.
The .NET CLI restores the package on first run, just like it would for a regular project.
The #:sdk Directive
Use #:sdk when you need a specific SDK, such as the Web SDK for ASP.NET Core:
csharp#:sdk Microsoft.NET.Sdk.Web var builder = WebApplication.CreateBuilder(); var app = builder.Build(); app.MapGet("/", () => "Hello from a single-file API!"); app.Run();
By default, file-based apps use Microsoft.NET.Sdk (the standard SDK).
Switching to Microsoft.NET.Sdk.Web unlocks ASP.NET Core types like WebApplication without any project setup.
The #:project Directive
If you need to call into an existing project (for example, a shared class library), use #:project:
csharp#:project ../MyLibrary/MyLibrary.csproj using MyLibrary; var calculator = new Calculator(); Console.WriteLine(calculator.Add(2, 3));
This is helpful when you have a class library and want to write a quick tool that uses it, without creating yet another console project.
Splitting File-Based Apps Across Multiple Files
In .NET 10, file-based apps had one significant limitation: everything had to fit in a single file. That worked for short scripts, but the file grew too big for anything larger.
.NET 11 Preview 3 fixes this with the #:include directive.
You can now split your code across multiple files while keeping the file-based workflow.
Imagine you are writing a small data processor. In .NET 10, you had to put models, helpers, and the main logic all in one file. Now you can split it cleanly:
csharp// models.cs public record Customer(int Id, string Name, string Email);
csharp// helpers.cs public static class Helpers { public static string Format(Customer c) => $"[{c.Id}] {c.Name} <{c.Email}>"; }
csharp// main.cs #:include models.cs #:include helpers.cs var customer = new Customer(1, "John Doe", "[email protected]"); Console.WriteLine(Helpers.Format(customer));
Run it the same way as before:
bashdotnet run main.cs
The #:include directive tells the compiler to include the referenced file in the compilation.
You can include as many files as you need, and they can live in subfolders:
csharp#:include models/customer.cs #:include models/order.cs #:include services/email-service.cs
Roslyn also adds editor IntelliSense for the directive, so completion works when you type #:include in your main file.
This completes the file-based apps feature. You can start with a single file, grow it into multiple files as your script matures, and convert it to a full project only when you truly need one.
Building an HTTP Health-Check Tool
Let's build something practical: a tool that monitors a list of URLs every 5 seconds and reports their status.
This is the kind of small utility that's perfect for a file-based app.
We'll split it into two files: one for the health-check logic, and one for the entry point.
The Health-Check Logic
Create healthchecker.cs with the class that performs the actual check:
csharpusing System.Diagnostics; public sealed class HealthChecker(HttpClient httpClient) { public async Task<HealthCheckResult> CheckAsync(string url) { var sw = Stopwatch.StartNew(); try { using var response = await httpClient.GetAsync(url); sw.Stop(); return new HealthCheckResult( Url: url, StatusCode: (int)response.StatusCode, IsHealthy: response.IsSuccessStatusCode, ResponseTimeMs: sw.ElapsedMilliseconds); } catch (Exception ex) { sw.Stop(); return new HealthCheckResult( Url: url, StatusCode: 0, IsHealthy: false, ResponseTimeMs: sw.ElapsedMilliseconds, Error: ex.Message); } } } public record HealthCheckResult( string Url, int StatusCode, bool IsHealthy, long ResponseTimeMs, string? Error = null);
The HealthChecker class takes an HttpClient and exposes a single CheckAsync method.
It captures the response time, status code, and any exception that occurs.
The Entry Point
Create main.cs with the URLs to monitor and the polling loop:
csharp#:include healthchecker.cs var urls = new[] { "https://github.com", "https://example.com" }; using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; var checker = new HealthChecker(http); Console.WriteLine($"Monitoring {urls.Length} URLs every 5 seconds. Press Ctrl+C to stop."); Console.WriteLine(); while (true) { foreach (var url in urls) { var result = await checker.CheckAsync(url); var status = result.IsHealthy ? "OK " : "FAIL"; var error = result.Error is null ? "" : $" - {result.Error}"; Console.WriteLine( $"[{DateTime.Now:HH:mm:ss}] {status} {result.StatusCode} " + $"{result.Url} ({result.ResponseTimeMs}ms){error}"); } Console.WriteLine(); await Task.Delay(TimeSpan.FromSeconds(5)); }
The main file pulls in healthchecker.cs with #:include and runs an infinite loop that checks each URL every 5 seconds.
To run the app, execute the following command at the main file:
bashdotnet run main.cs
You'll see output like this:
Monitoring 3 URLs every 5 seconds. Press Ctrl+C to stop. [14:32:01] OK 200 https://github.com (412ms) [14:32:02] OK 200 https://example.com (89ms) [14:32:07] OK 200 https://github.com (398ms) [14:32:07] OK 200 https://example.com (76ms)
Building a Minimal API with EF Core and SQLite
Now let's go further. We'll build an Orders API with EF Core and SQLite, all as a file-based app.
This is a real, runnable web API that you could use for prototyping or internal tooling. But the entire thing is just three files without a project.
We'll structure it like this:
main.cs- DI setup and the Minimal API endpointsOrdersDbContext.cs- the EF CoreDbContextwith mappingsOrder.cs- theOrderentity
Create Order.cs with the entity model:
csharppublic class Order { public int Id { get; set; } public string OrderNumber { get; set; } = string.Empty; public string CustomerName { get; set; } = string.Empty; public decimal Amount { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; }
Create OrdersDbContext.cs with the EF Core context and inline Fluent API mappings:
csharpusing Microsoft.EntityFrameworkCore; public class OrdersDbContext : DbContext { public OrdersDbContext(DbContextOptions<OrdersDbContext> options) : base(options) { } public DbSet<Order> Orders => Set<Order>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Order>(entity => { entity.ToTable("Orders"); entity.HasKey(o => o.Id); entity.Property(o => o.OrderNumber) .HasMaxLength(50) .IsRequired(); entity.Property(o => o.CustomerName) .HasMaxLength(200) .IsRequired(); entity.Property(o => o.Amount) .HasColumnType("decimal(18,2)"); entity.HasIndex(o => o.OrderNumber).IsUnique(); }); } }
The mappings live directly inside OnModelCreating for simplicity.
For larger projects, you would extract them into separate IEntityTypeConfiguration<T> classes - but for a file-based app, inline mappings are perfect.
Now the entry point.
Create main.cs with the SDK directive, package directive, includes, DI setup, and the API endpoints:
csharp#:sdk Microsoft.NET.Sdk.Web #:package [email protected] #:include Order.cs #:include OrdersDbContext.cs using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(); builder.Services.AddDbContext<OrdersDbContext>(options => options.UseSqlite("Data Source=orders.db")); var app = builder.Build(); // Make sure the database and table exist before handling requests using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService<OrdersDbContext>(); await db.Database.MigrateAsync(); } app.MapGet("/orders", async (OrdersDbContext db) => await db.Orders.AsNoTracking().ToListAsync()); app.MapGet("/orders/{id:int}", async (int id, OrdersDbContext db) => await db.Orders.FindAsync(id) is { } order ? Results.Ok(order) : Results.NotFound()); app.MapPost("/orders", async (Order order, OrdersDbContext db) => { db.Orders.Add(order); await db.SaveChangesAsync(); return Results.Created($"/orders/{order.Id}", order); }); app.MapDelete("/orders/{id:int}", async (int id, OrdersDbContext db) => { var order = await db.Orders.FindAsync(id); if (order is null) return Results.NotFound(); db.Orders.Remove(order); await db.SaveChangesAsync(); return Results.NoContent(); }); app.Run();
Look at the top of the file. There's a lot going on in just four lines:
#:sdk Microsoft.NET.Sdk.Webswitches to the web SDK so we can useWebApplication#:package [email protected]pulls in the SQLite provider#:include Order.csbrings the entity into the compilation#:include OrdersDbContext.csbrings the DbContext into the compilation
Everything below is standard ASP.NET Core code.
Run the API:
bashdotnet run main.cs
The first run takes a few seconds while NuGet restores EF Core and the SQLite provider. Subsequent runs are fast.
You can now hit the endpoints with curl:
bashcurl -X POST http://localhost:5000/orders \ -H "Content-Type: application/json" \ -d '{"orderNumber":"ORD-001","customerName":"Alice","amount":99.99}' curl http://localhost:5000/orders
A working web API with a real database, in three files, with no project setup. That's the power of multi-file file-based apps.
Converting a File-Based App to a Full Project
There comes a point when a file-based app is no longer enough. Maybe you need multiple projects, complex build steps, or proper packaging for distribution.
You can use the .NET CLI to convert your file-based app into a regular project with a single command:
bashdotnet project convert main.cs
What You Get
The command creates a new folder next to your file (named after your main file) containing a regular project structure:
main/ ├── main.csproj ├── Order.cs ├── OrdersDbContext.cs └── main.cs
The csproj file includes the SDK and package references declared with #: directives in your script.
The #:include directives are removed because all files are now part of the project naturally.
Your main.cs becomes a standard top-level statements file.
You can then open the folder in your IDE and continue working as if you had created the project from scratch.
When to Convert
File-based apps are great until they're not. Convert to a full project when:
- You need multiple projects in one solution - for example, splitting a Web API and a class library
- You need MSBuild customizations - custom targets, conditional compilation, build properties
- You need proper packaging - publishing as a NuGet package or a self-contained executable
- You need CI/CD with custom steps - pipelines tend to assume a project file exists
- The team grows - working with
csprojis more familiar to most .NET developers - You need test projects - unit tests work best in a multi-project solution structure
The good news is that you don't have to decide upfront. Start as a file-based app, grow it as needed, and convert only when you exceed the file-based approach.
Summary
File-based apps are the most useful productivity feature .NET has shipped in years.
What started in .NET 10 as a single-file workflow becomes a real, scalable approach in .NET 11 Preview 3 with multi-file support.
When to use file-based apps:
- Quick scripts, prototypes, and proof-of-concept work
- CLI tools and automation utilities
- Internal tooling that doesn't need a full project structure
- Demos and learning material where setup gets in the way
- Glue code that calls existing libraries or APIs
When to use full projects:
- Production applications that need long-term maintenance
- Solutions with multiple projects (libraries, APIs, tests)
- Anything that requires custom MSBuild logic or packaging
- Code that needs to be distributed as a NuGet package
I've already replaced several Python utility scripts with file-based C# apps in my own toolbox. The strong typing, IntelliSense, and ability to share code with my main projects make them genuinely better tools.
Are you using file-based apps yet? Stay subscribed to my newsletter for more practical .NET content like this.
Hope you find this newsletter useful. See you next time.


Comments