newsletter

Building File-Based Apps in .NET: A Complete Guide With Multi-File Support

6 min read

Newsletter Sponsors

Copied

Azure Copilot Migration Agent is Here (Sponsored)

Microsoft's Azure Copilot Migration Agent can turn complex migration data into clear answers. With natural language prompts, you can evaluate readiness, risk, ROI, and automate landing zone requirements to make confident decisions for you and your team. Streamline planning and analysis, so your migrations are better scoped, better justified, and far less error-prone.

πŸ‘‰ Download the playbook to get started

Copied

What Azure Copilot Agents Can Actually Do – Learning Module (Sponsored)

Microsoft has made an Introduction to Azure Copilot Agents learning module available.

The six specialized agents are embedded throughout the lifecycle to automate and accelerate specific tasks, cutting down on cloud operations tasks like deployments, monitoring, optimization, and troubleshooting.

  • Deployment agent: Helps automate infrastructure setup and application deployments by generating configurations and guiding execution within Azure environments.
  • Observability agent: Monitors system health by analyzing metrics, logs, and signals to provide actionable insights and improve visibility.
  • Optimization agent: Identifies opportunities to reduce costs and improve performance across resources using intelligent recommendations.
  • Resiliency agent: Strengthens system reliability by suggesting improvements for fault tolerance, availability, and recovery strategies.
  • Troubleshooting agent: Assists in diagnosing issues by correlating signals across services and guiding root cause analysis.
  • Migration agent: Simplifies moving workloads to Azure by assessing environments, recommending strategies, and orchestrating migration steps.

The learning module is a great way to get started with all six agents.

πŸ‘‰ Check it out

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!

Copied

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:

bash
dotnet 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:

csharp
Console.WriteLine("Hello from a file-based app!"); Console.WriteLine($"Today is {DateTime.Now:dddd, MMMM d, yyyy}");

Then run it:

bash
dotnet 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
Copied

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.

Copied

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:

bash
dotnet 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.

Copied

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:

csharp
using 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:

bash
dotnet 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)
Copied

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 endpoints
  • OrdersDbContext.cs - the EF Core DbContext with mappings
  • Order.cs - the Order entity

Create Order.cs with the entity model:

csharp
public 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:

csharp
using 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.Web switches to the web SDK so we can use WebApplication
  • #:package [email protected] pulls in the SQLite provider
  • #:include Order.cs brings the entity into the compilation
  • #:include OrdersDbContext.cs brings the DbContext into the compilation

Everything below is standard ASP.NET Core code.

Run the API:

bash
dotnet 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:

bash
curl -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.

Copied

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:

bash
dotnet 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 csproj is 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.

Copied

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.

Whenever you're ready, here's how I can help you:

The .NET Senior Playbook is built to:

  • Fast-track you from junior or mid-level to senior
  • Keep you growing as a senior
  • Help you beat any .NET interview

Covers everything: C#, ASP.NET Core, EF Core, system design β€” answer each question first, reveal the solution, and a test after every chapter proves it stuck. Finish, and you earn a verifiable certificate for your LinkedIn.

The .NET Senior Playbook
View the Playbook

Enjoyed this article? Share it with your network

Improve Your .NET and Architecture Skills

Join my community of 25,000+ developers and architects.

Each week you will get 1 practical tip with best practices and real-world examples.

Learn how to craft better software with source code available for my newsletter.

Join 25,000+ developers already reading
No spam. Unsubscribe any time.