Are you sure “Hello, World!” is trivial? Most beginners (and a few seniors) trip over at least three basics: where Main
lives, what a namespace really is, and why fields ≠ properties. In this post, you’ll ship a tiny console app and finally connect the dots between using directives, namespaces, classes, constructors, methods, and the entry point.
Why this matters
If C# were a city, using directives are street signs, namespaces are neighborhoods, classes are buildings, fields/properties are rooms, methods are the doors, and constructors are the keys. Once you see the map, everything else is just… taking a walk.
Structure of a C# Program
At its core, a C# program is a set of source files compiled into an assembly (EXE or DLL). A minimal, modern console app can be as short as one file with top‑level statements – or the classic structure with an explicit Main
method inside a class.
// File: Program.cs (modern: top‑level statements)
using System;
Console.WriteLine("Hello, World!");
Or the classic form:
// File: Program.cs (classic entry point)
using System;
namespace GettingStarted
{
internal static class Program
{
private static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
When to choose which?
- Top‑level statements: perfect for demos, small utilities, or minimal APIs.
- Explicit
Main
in a class: clearer for larger apps, multiple entry points, or when you want full control over startup.
Tip: In SDK-style projects, implicit usings may be on by default. That’s why your one-file app compiles even if you don’t import everything manually.
Using Directives
using
tells the compiler which namespaces to search for type names so you don’t have to fully qualify them.
Standard using
using System;
using System.Collections.Generic;
Static using (import static members)
using static System.Console;
class Demo
{
public void Print() => WriteLine("No need for Console.");
}
Alias using (rename a type/namespace)
using IO = System.IO;
class Logs
{
public string ReadFirstLine(string path)
{
using var reader = new IO.StreamReader(path);
return reader.ReadLine();
}
}
Global using (applies to the whole project)
Create GlobalUsings.cs
once and forget repetitive imports:
// File: GlobalUsings.cs
global using System;
global using System.Collections.Generic;
Gotcha: Overusing global using
can hide dependencies. Keep the list small and generic (e.g., System
, System.Linq
).
Namespace Declaration
A namespace groups related types and avoids name collisions. There are two styles:
Block-scoped
namespace Company.Project.Module
{
public class Greeter { }
}
File-scoped (modern, cleaner)
namespace Company.Project.Module;
public class Greeter { }
Guideline: Prefer file‑scoped namespaces for brevity. Use company/product/module patterns for clarity, e.g., Contoso.Inventory.Api
.
Class Declaration
A class defines the shape and behavior of objects.
namespace Basics;
public class Person
{
// Fields (internal state)
private readonly Guid _id = Guid.NewGuid();
// Auto-property with public get/set
public string FirstName { get; set; }
// Property with a private setter (encapsulation)
public string LastName { get; private set; }
// Computed (read-only) property
public string FullName => $"{FirstName} {LastName}".Trim();
// Constructor (runs when creating an instance)
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
// Method (behavior)
public string Introduce() => $"Hi, I’m {FullName}!";
}
Access modifiers: public
, internal
, protected
, private
, protected internal
, private protected
. Start strict (private
) and open up only as needed.
Other modifiers: static
(no instance), sealed
(cannot be inherited), abstract
(incomplete; must be derived), partial
(split across files), record
(for value-like, immutable-by-default models).
Attributes and Behavior (Fields, Properties, Methods)
Think of fields as the raw data, properties as controlled access, and methods as actions.
Fields
- Store state directly.
- Usually
private
. - Prefer read-only (
readonly
) if set once.
Properties
- Use auto-properties for simple storage:
public int Age { get; set; }
- Add logic in accessors when you need validation or derived values:
private int _age; public int Age { get => _age; set => _age = value < 0 ? 0 : value; }
init
accessor lets you set a property during object creation only:public string Email { get; init; } = "unknown@example.com";
Methods
A method has a signature: name + parameters + return type.
public int Add(int a, int b) => a + b;
public string Format(string name, int? age = null)
=> age is null ? name : $"{name} ({age})";
public void Log(string message, bool important = false)
{
if (important) Console.Error.WriteLine(message);
else Console.WriteLine(message);
}
Named/optional args improve readability:
Log(message: "Disk space low", important: true);
Constructors
Constructors initialize objects. You can overload them and chain with this(...)
.
public class TimerOptions
{
public int IntervalMs { get; }
public bool AutoStart { get; }
public TimerOptions(int intervalMs, bool autoStart)
{
IntervalMs = intervalMs;
AutoStart = autoStart;
}
// Convenience overload (defaults)
public TimerOptions(int intervalMs) : this(intervalMs, autoStart: true) { }
}
Best practices
- Validate arguments early; throw informative exceptions.
- Keep constructors thin; defer heavy work to methods (e.g.,
Start()
).
Main Method – The Program’s Entry Point
The CLR starts your app by calling Main
.
Valid signatures
static void Main()
static int Main()
static Task Main()
static Task<int> Main()
static void Main(string[] args)
Async Main
is great for I/O-bound startups:
using System.Net.Http;
internal static class Program
{
private static async Task Main()
{
using var http = new HttpClient();
var ping = await http.GetStringAsync("https://example.com/ping");
Console.WriteLine($"Service says: {ping}");
}
}
Top‑level statements compile down to a generated Main
for you – identical idea, less ceremony.
Bringing It All Together
Let’s model a tiny domain and wire it to a console app.
// File: Models/TaskItem.cs
namespace Tasks.Model;
public class TaskItem
{
public Guid Id { get; } = Guid.NewGuid();
public string Title { get; }
public bool IsDone { get; private set; }
public TaskItem(string title)
{
Title = string.IsNullOrWhiteSpace(title)
? throw new ArgumentException("Title is required", nameof(title))
: title.Trim();
}
public void Complete() => IsDone = true;
}
// File: Services/TaskService.cs
using Tasks.Model;
namespace Tasks.Services;
public class TaskService
{
private readonly List<TaskItem> _items = new();
public TaskItem Add(string title)
{
var item = new TaskItem(title);
_items.Add(item);
return item;
}
public IEnumerable<TaskItem> All() => _items;
}
// File: Program.cs (top‑level)
using Tasks.Services;
var service = new TaskService();
service.Add("Learn basic C# syntax");
service.Add("Write first program");
foreach (var item in service.All())
{
Console.WriteLine($"- [{(item.IsDone ? 'x' : ' ')}] {item.Title}");
}
Project layout (ASCII):
.
├─ Models
│ └─ TaskItem.cs
├─ Services
│ └─ TaskService.cs
└─ Program.cs
This tiny app showcases using directives, namespaces, classes, fields/properties/methods, constructors, and the entry point.
Writing a Simple C# Program (Step‑by‑Step)
- Create a folder and a console project:
mkdir BasicsApp && cd BasicsApp dotnet new console -n BasicsApp cd BasicsApp
- Run it:
dotnet run
- Replace
Program.cs
with the Task list sample above. - Add folders/files
Models/TaskItem.cs
andServices/TaskService.cs
. - Run again and observe the output.
Tip: Turn on nullable reference types for safer code by adding
<Nullable>enable</Nullable>
to your.csproj
.
Understanding Namespaces, Classes, and Methods
- Namespace: a logical group for related types (prevents naming collisions). Think: folders.
- Class: a blueprint for objects that hold state and behavior. Think: a house plan.
- Method: an action or question you can ask an object. Think: a doorbell you ring to do something.
If you fully qualify types, you can skip using
:
var now = System.DateTime.UtcNow;
System.Console.WriteLine(now);
…but life is short – use using
sensibly.
Basic C# Syntax of Namespaces
// File-scoped (preferred)
namespace Company.Product.Feature;
public class Engine { }
// Block-scoped (legacy style)
namespace Company.Product.Feature
{
public class Engine { }
}
Naming Tips
- Use PascalCase for namespaces and classes:
Contoso.Payments.Api
. - Keep them stable; changing a namespace is a breaking change for library consumers.
Basic C# Syntax of Classes
public class Car
{
// Field
private int _odometer;
// Auto-property
public string Model { get; init; }
// Property with logic
public int Odometer
{
get => _odometer;
private set => _odometer = value < 0 ? 0 : value;
}
// Constructor
public Car(string model)
{
Model = model ?? throw new ArgumentNullException(nameof(model));
Odometer = 0;
}
// Method
public void Drive(int kilometers)
{
if (kilometers <= 0) return;
Odometer += kilometers;
}
}
Rule of thumb: Start with auto-properties; move to backing fields only when you need logic or performance tweaks.
Basic C# Syntax of Methods
public class MathOps
{
// 1) Simple method
public int Add(int a, int b) => a + b;
// 2) Overload with different params
public double Add(double a, double b) => a + b;
// 3) Optional + named parameters
public string Pad(string text, int totalWidth = 10, char padChar = ' ')
=> text.PadLeft(totalWidth, padChar);
// 4) Out parameters (multiple results)
public bool TryParseInt(string input, out int number)
=> int.TryParse(input, out number);
// 5) Async method
public async Task<string> DownloadAsync(HttpClient http, string uri)
=> await http.GetStringAsync(uri);
}
Don’t overuse out
– prefer clear return types or small structs/records to carry multiple values.
Common Pitfalls (and quick fixes)
- Forgetting access modifiers:
class
defaults tointernal
in top-level scope. Be explicit (public
) for libraries. - Using fields instead of properties in public APIs: prefer properties for binary compatibility and binding.
- Business logic in constructors: move I/O or heavy work to methods (e.g.,
InitializeAsync
). - Ignoring nullability: enable
<Nullable>enable</Nullable>
and act on warnings. - God classes: if a class exceeds ~200–300 lines and does many things, split responsibilities.
Mini Checklist
- Namespace is file-scoped and meaningful.
- Public types/members use PascalCase; locals/params use camelCase.
- Constructors validate inputs.
- Methods are short, do one thing, and have clear names.
- Properties encapsulate state; fields stay private.
Main
(or top‑level) is minimal – delegate to services.
FAQ: Your First C# Program
Main
?For small apps and samples, top‑level is faster to read. For bigger apps, an explicit Main
keeps startup organized.
A field is raw storage; a property is a method-backed accessor (even if auto-generated) that can add logic, validation, and is friendlier to tooling and binding.
Main
be async?Yes – use static async Task Main()
(or Task<int>
). Await I/O cleanly without blocking.
No, but add one for anything you plan to reuse. Namespaces keep code organized and conflict-free.
If the type is mostly data and equality/value semantics matter, record
(or record class
) is great. For behavior-rich entities, stick with classes.
using
directives go?At the top of the file, before the namespace. Keep them sorted and minimal. Use global using
for widely shared imports.
Conclusion: From syntax to a working app
You just mapped the city of C#: using → namespace → class → fields/properties → methods → constructors → Main. With these parts clear, you can navigate any codebase and ship working programs with confidence. Now it’s your turn – fork the sample, add a command to complete a task, and post your output in the comments. What’s the first feature you’ll build?