VSLive! Blog

Industry Insights, Information, and Developer News

Blog archive

Have You Tried to Teach Someone C# Lately?

Have you tried to teach someone C# lately?

If you're like me, you learned C# the hard way. You installed the full-blown Visual Studio -- the giant, go-get-a-coffee-while-it-installs version. Then you created a solution. Then a project inside that solution. Probably a console app. And after all that clicking and waiting and scaffolding, you finally arrived at the point where you could type the one line of code you actually wanted to write.

That spot? That was the starting line. And getting there was a surprising amount of work.

Well, I've got good news. If you had to teach someone C# today -- or if you wanted to pick it up yourself from scratch -- it's so much easier. You get to the starting line almost immediately.

And here's the thing: this is the stuff that experienced, long-time .NET devs kinda never notice. Because we already know C#. We're not walking a beginner up to the starting line, so we never see how far away that line used to be. We just fire up Visual Studio out of muscle memory and go.

So let me walk you up to the modern starting line. We'll start with the absolute bare minimum and then climb, one rung at a time, until we're doing real work. .NET 10, C# 14.

(Quick scoping note: this is not a groundbreaking "you're doing it all wrong" article. It's more of a "huh, well that's kinda cool" article. Fair warning.)

Rung 1: The New Starting Line Is One File
Create a file. Call it app.cs. Put one line in it:

Console.WriteLine("Hello, World!");

Now run it:

dotnet run app.cs

That's it. That's the whole thing.

No solution. No project. No .csproj. No static void Main. No namespace. No class. Just a file with a line of code in it, and it runs.

If you came up through the Visual Studio "File -> New Project" world, stop for a second and appreciate how genuinely weird this is. This is .NET 10's file-based apps feature, and it's built right into the SDK. You write C# like it's Python. One file. Run it. Done.

Two things are quietly happening here that a beginner would ask about and a veteran would blow right past.

First, that one-line Hello World works because of top-level statements. If you've ever wondered what the compiler is actually doing when it lets you skip the class and the Main method, I wrote a whole thing about how that works under the hood. Short version: the compiler quietly stuffs all the ceremony back in for you. You just don't have to type it anymore.

Second -- and did you catch this? -- you didn't write using System; either. Console just... worked. Hold that thought. We'll come back to it in Rung 3.

Rung 2: Your App Outgrows One File
One file is great until it isn't. Eventually you want a second type, and you'd like it in its own file so your brain can find it later.

In .NET 10, when you outgrow the single-file life, you run one command:

dotnet project convert app.cs

That turns your loose .cs file into a real project -- it generates the .csproj, wires up the SDK, and you're now in "normal project" land. (Handy detail: a file-based app is basically a real project wearing a disguise, so the conversion is clean.)

Now that we've got real files, let's add a type. Here's something I want you to look at that you've typed 10,000 times without thinking about it. The old way to declare a namespace:

namespace Benday.Demo
{
    public class Greeter
    {
        public string GetGreeting() => "Hello, World!";
    }
}

See the braces? See how everything inside is indented one level for no reason other than "that's where the namespace put me"? That indentation is pure tax. It buys you nothing.

Here's the same thing with a file-scoped namespace:

namespace Benday.Demo;

public class Greeter
{
    public string GetGreeting() => "Hello, World!";
}

One line. A semicolon instead of a set of braces. Everything shifts left. The whole file gets to breathe.

Quick precision note so nobody says, "Well, actually," in the comments: the language feature is the file-scoped namespace -- the namespace Foo; one-liner. The thing where your namespace automatically matches your folder structure is a tooling convention (VS Code and the C# Dev Kit will default the namespace to the folder path when you add a file). Two different things that feel like one.

And here's the question I always trip over, so I'll bet you're wondering it too: can't you just skip the declaration entirely and have C# figure out the namespace from the folder? Nope. Not a thing. Feels like it should be a thing. It isn't a thing. If you leave the namespace off, your type lands in the global namespace -- the folder has zero effect at the compiler level. What makes it feel automatic is a combination of <RootNamespace> in your project file (the base) plus your editor auto-writing the one-line declaration to match the folder and keeping it in sync (that's the IDE0130 analyzer, if you want to look it up). The "just infer it from the folder and let me delete the line" version? That's a long-standing language feature request, not a real thing yet. So: your editor writes and maintains that one line for you, but the line is still there.

Rung 3: You've Been Ignoring 10 Using Statements
Remember back in Rung 1 when Console.WriteLine worked without using System;? Time to explain that.

Modern .NET projects turn on implicit usings. It's one line in your .csproj:

<ImplicitUsings>enable</ImplicitUsings>

With that flipped on, the SDK pre-imports a whole pile of the namespaces you use in basically every file -- System, System.Linq, System.Collections.Generic, and a bunch more -- so you never have to type them. They're just there.

And if you want to add your own to the always-on list, you make a GlobalUsings.cs file and use the global using keyword:

global using Benday.Demo;
global using Benday.Demo.Services;

Declare it once. It's available in every file in the project. No more copy-pasting the same six using lines at the top of every new file.

Here's the point that ties this rung back to the whole article: you never noticed the using statements were missing for the same reason you never questioned static void Main -- it was already handled, so you stopped seeing it. The ceremony didn't go away. It just moved somewhere you don't have to look at it.

The Fork in the Road: Stop Here, or Go Big
Okay. Deep breath. This is the most important paragraph in the article.

If you're writing script-ish stuff -- a quick utility, a one-off automation, glue code, a thing you'll run twice and delete -- stop climbing. You're done. Everything up to here gets you a clean, fast, minimal app with almost zero ceremony. Ship it and move on. Seriously. Not every program needs to be an enterprise cathedral.

But if you want to start building big-kid apps -- the kind that grow, that other people maintain, that need logging and configuration and a real structure -- there's one more rung. And it's the rung that unlocks all the others.

Notice what happened to the distance between "hello world" and "real app." It collapsed at both ends. Beginners hit the starting line instantly (Rungs 1 through 3). And the starting line is now sitting right next to production-shaped code (Rung 4, coming up). It's the same idea measured from two directions: the gap between nothing and doing real work got small.

Rung 4: Dependency Injection, a.k.a. The Socket Everything Plugs Into
Here's the move that turns a toy into an app.

You can pull the whole .NET generic host -- with dependency injection, configuration and logging baked in -- into even a file-based app. Add one package directive at the top of your file:

#:package Microsoft.Extensions.Hosting

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<IGreeter, Greeter>();

using var host = builder.Build();

host.Services.GetRequiredService<IGreeter>().SayHello();

interface IGreeter
{
    void SayHello();
}

class Greeter(ILogger<Greeter> logger) : IGreeter
{
    public void SayHello() => logger.LogInformation("Hello from DI!");
}

Look at what you got for two lines of registration.

You built a host. You registered a service. You asked the container for it. And that Greeter class? It's getting a fully configured ILogger handed to it -- through a primary constructor, no less (Greeter(ILogger<Greeter> logger)), which shrinks the class to almost nothing. You didn't new up a logger. You didn't configure it. It just showed up because the host wired it in.

That's the whole reason DI is the payoff and not just "rung 4 of 4." Dependency injection isn't one feature among many. It's the socket everything else plugs into. Once you've got a container, the rest of the modern .NET toolbox lights up:

  • ILogger<T> -- structured logging, for free, injected wherever you want it.
  • IConfiguration -- read settings from all over (more on this in a second).
  • IOptions<T> -- strongly typed config bound to your own classes.
  • HttpClientFactory, IHostedService, background workers... all of it.

None of that shows up until you've got DI. That's why this is the rung that matters. It's the on-ramp to "real."

And when your single file eventually gets too big for its britches? Same as before -- dotnet project convert app.cs, and you're in a full project with room to grow. You climbed the whole ladder without ever scaffolding a solution by hand.

The Cherry on Top: Go Count the Config Providers
Before I let you go, a little going-away present. Whipped cream, meet cherry.

Once you've got that host from Rung 4, you've also got IConfiguration -- and I'd bet money you've never actually looked at how many places .NET can pull configuration from these days. Here's the in-the-box lineup:

  • JSON, INI and XML files (the file-based trio)
  • Environment variables
  • Command-line arguments
  • In-memory collections
  • User secrets -- the one nobody remembers exists until they leak a key into Git
  • Key-per-file -- reads a directory where each file is one setting, which is exactly how Docker and Kubernetes mount secrets
  • Azure Key Vault and Azure App Configuration -- separate packages, still Microsoft-shipped

That's roughly eight in the box plus the Azure two. And underneath all of them is a custom-provider extension point, so a database-backed config provider is about 40 lines of code. Config lives wherever you want it to live.

But here's the actual .NET 10 nugget, the one nobody's noticed yet. The environment-variable provider got smarter about connection strings: starting in .NET 10, it recognizes seven additional connection-string prefixes, for a total of 11. It used to be a short list. Now a whole batch of ConnectionStrings__-style prefixes gets auto-mapped into your configuration for you.

It's a tiny thing. But it's this entire article in miniature: the platform quietly got more capable at the starting line, and the veterans didn't look up from their boilerplate long enough to notice.

Which is the whole point, isn't it? You never counted the config providers for the same reason you never questioned static void Main, never noticed the missing using System;, never wondered why the namespace made you indent. It was already handled. So you stopped seeing it.

Every so often, it's worth looking again.

About the Author

Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 07/31/2026


Keep Up-to-Date with Visual Studio Live!

Email Address*Country*
Please type the letters/numbers you see above.