BiblioTech has grown, module by module, into a complete domain (LibraryItem, Book,
Magazine, Member, Loan, Library), persistence in four different forms (text, JSON,
SQLite, Entity Framework Core), and five different user interfaces. With this module, the course
takes a step back: instead of adding new functionality, it's time to consolidate and polish
all of that code. This first lesson focuses on the foundation of any polishing process: coding
standards. A coding standard isn't a minor aesthetic matter — it's what lets anyone (including
your own future self, six months from now) read code they didn't write and understand it
effortlessly. You'll review and expand conventions you already know from Module 1, and see some
new ones that apply to the project as a whole, not just to a single line.
Contents
- Why coding standards matter in a growing project
- .NET naming conventions: review and expansion
EditorConfigand code analyzers- Single responsibility principle at the method and class level
- Useful comments versus noise comments
- XML documentation (
///) onLibrary's public API - Consistent nullability across the whole project
- Combined example: before and after a
Librarysnippet
- Why coding standards matter in a growing project
In the course's early lessons, with programs just a few lines long, any writing style "worked":
the code was so short it was equally understandable whether a variable was called x or
pageCount. BiblioTech is no longer like that: it has a domain of several classes, several forms
of persistence, and five user interfaces, spread across a real project that, at a company,
several people would maintain at once. In that context, a consistent coding standard stops being
a personal preference and becomes a necessity:
| Without a consistent standard | With a consistent standard |
|---|---|
| Each class "reads" differently; you have to relearn the style in every file | The code reads uniformly across the whole project |
| Reviewing someone else's code takes longer (Lesson 5 of this module) | Reviews focus on logic, not on arguing about style |
| Nullability mistakes or confusing names slip through more easily | Many bugs are avoided just by following the convention |
| Onboarding someone new to the project is slow | New code "fits" immediately with the rest |
It's not about imposing a style on a whim, but about eliminating repetitive decisions — capital or lowercase initial? where does the comment go? — so attention can go to what really matters: making sure the code does the right thing.
- .NET naming conventions: review and expansion
The Basic Syntax and Structure lesson (Module 1) introduced .NET's two central conventions:
| Convention | Rule | Where it's used |
|---|---|---|
| PascalCase | Every word starts with a capital letter | Classes, methods, properties (Library, RegisterLoan, Title) |
| camelCase | The first word starts lowercase | Local variables and parameters (title, memberId) |
Three additional conventions now join those, already applied in passing throughout BiblioTech but not formally named until now:
Iprefix on interfaces:ILendable,ISearchable(Module 4). TheIprefix signals, just by reading the name, that this is a contract and not a concrete class — a convention you'll see taken further in Lesson 3 of this module, withILibraryRepository._prefix on private fields:_membersById(Module 4, inLibrary). Distinguishes a private field from a public property or a local variable at a glance, with no need to check its declaration.Asyncsuffix on asynchronous methods:LendBookAsync(Module 4),GetMetadataByIsbnAsync(Module 5). Tells whoever calls the method that it must be used withawait, without having to check the full signature.
class Library
{
private Dictionary<int, Member> _membersById = new Dictionary<int, Member>(); // _prefix: private field
public List<LibraryItem> Catalog { get; } = new List<LibraryItem>(); // PascalCase: public property
public async Task LendBookAsync(Book book, Member member) // Async suffix: asynchronous method
{
// ...
}
}These three conventions aren't quirks of the course itself: they're the ones followed by the whole .NET standard library and the vast majority of published C# code, which means following them makes BiblioTech "fit" immediately with any other .NET project someone reads afterward.
EditorConfig and code analyzers
EditorConfig and code analyzersRemembering a convention by heart and applying it by hand on every line is error-prone. Two tools automate much of that work:
EditorConfig(an.editorconfigfile at the project root): a text file that declares formatting rules — indentation, spaces versus tabs, trailing newline, and also C# naming conventions — that editors like Visual Studio or VS Code apply automatically while writing and formatting code.- Code analyzers (Roslyn analyzers): tools that scan code looking for problems — from
style to potential bugs — and show warnings directly in the editor, even before compiling. .NET
ships with a set of analyzers enabled by default in any modern project (
dotnet new), and they can be extended by installing additional NuGet packages (for example,Microsoft.CodeAnalysis.NetAnalyzerswith stricter rules).
# .editorconfig (illustrative excerpt)
root = true
[*.cs]
indent_style = space
indent_size = 4
dotnet_naming_rule.interfaces_should_be_prefixed_with_i.severity = warningThis lesson doesn't go deep into the full .editorconfig syntax or analyzer configuration —
each team usually adopts an already-prepared set — but it's important to know they exist: they
automate exactly the conventions seen in the previous section, so a naming slip gets caught as a
warning in the editor, not in a code review days later.
- Single responsibility principle at the method and class level
The single responsibility principle (one of the central ideas behind design patterns, which the next lesson revisits in more detail) says, simply: every method and every class should have a single reason to change. Applied day to day, it means a method should do one thing, and do it well, instead of mixing several unrelated responsibilities.
// Before: a method with two responsibilities mixed together
public void ProcessLoan(Book book, Member member)
{
if (!book.Available)
{
Console.WriteLine("Not available.");
return;
}
book.Lend();
Console.WriteLine($"'{book.Title}' lent to {member.Name}."); // presentation responsibility
// ... saving, validation, or notification logic could all creep in here too...
}// After: each method has a single responsibility
public bool TryLend(Book book, Member member)
{
if (!book.Available)
{
return false;
}
book.Lend();
return true;
}
// The decision of what to show on the console (or another interface) lives outside, where it belongs
if (library.TryLend(book1, member1))
{
Console.WriteLine($"'{book1.Title}' lent to {member1.Name}.");
}
else
{
Console.WriteLine("Not available.");
}TryLend now only decides whether the loan is possible and updates the book's state; what
to do with that result (show it on the console, in a WPF window, or return it as JSON from an
ASP.NET Core endpoint) is the caller's responsibility, not the method's own. This separation is
exactly the one you already saw, without naming it that way, across Module 7's five interfaces:
the same Library serves Windows Forms, WPF, ASP.NET Core, Blazor, and MAUI precisely because
its logic assumes nothing about how its results are presented.
- Useful comments versus noise comments
Not every comment adds value. A comment that repeats what the code already says clearly is noise: it takes up space, and over time can drift out of date with the actual code, which is worse than having no comment at all.
// Noise: the comment says nothing the code doesn't already say
// Increment i by 1
i++;
// Add the price to the total
total += price;// Useful: explains the "why", not the "what" (the code already says that)
// Availability is re-checked after the simulated delay, because it could have changed
// while waiting for the penalty service's response (Module 4).
if (!book.Available)
{
throw new InvalidOperationException($"'{book.Title}' is not available for loan.");
}| Comment type | When to write it |
|---|---|
| Explains the "what" of an obvious line | Never: if it's needed, the variable or method name is poorly chosen |
| Explains the "why" behind a non-obvious decision | Yes: a business rule, an external constraint, a historical reason |
| Warns of a non-obvious side effect | Yes: for example, that a method modifies an object received as a parameter |
| Has gone out of date relative to the current code | Never: worse than no comment, because it misleads |
Rule of thumb: if you feel the need to comment on what a line of code does, first try renaming variables or extracting a method with a more descriptive name; reserve comments for what the code, by itself, can't express.
- XML documentation (
///) on Library's public API
///) on Library's public APIModule 1 briefly introduced documentation comments (///), based on XML tags, without yet using
them on BiblioTech's model. Now that Library has a consolidated public API, it's time to
document it:
class Library
{
/// <summary>
/// Attempts to register the loan of a book to a member.
/// </summary>
/// <param name="book">The book to be lent.</param>
/// <param name="member">The member requesting the loan.</param>
/// <returns>
/// <c>true</c> if the loan was registered successfully; <c>false</c> if the book
/// was not available.
/// </returns>
public bool TryLend(Book book, Member member)
{
if (!book.Available)
{
return false;
}
book.Lend();
return true;
}
}<summary> briefly describes what the member does; <param> documents each parameter;
<returns> explains the meaning of the returned value. The benefit isn't just for whoever reads
the source code: any modern editor (Visual Studio, VS Code with the C# extension) shows this
text automatically as contextual help while writing a call to TryLend, just as happens with
the .NET standard library's own methods (Console.WriteLine, for example, has its own XML
documentation). Documenting every line of even a small class this way would be excessive;
standard practice is to reserve /// for the public API of the project's central classes —
exactly the case of Library — and to skip it on internal details that already explain
themselves through their name.
- Consistent nullability across the whole project
The Pattern Matching and Modern Features lesson (Module 4) presented #nullable enable and
warned of a common mistake: enabling it halfway through a project and not attending to the
warnings it generates. Now that BiblioTech is a complete project, that recommendation becomes a
coding standard rule: #nullable enable must be applied consistently across every file in the
project, not just the ones touched most recently.
#nullable enable
class Library
{
public LibraryItem? FindByTitle(string title)
{
return Catalog.FirstOrDefault(item => item.Title == title);
// FirstOrDefault can return null; the "?" in the return type makes that explicit
}
public List<LibraryItem> Catalog { get; } = new List<LibraryItem>();
}| Without consistent nullability | With consistent nullability |
|---|---|
Some methods warn that they can return null, others don't, with no clear criterion |
Every reference type that can be null declares it with ?, across the whole project |
The compiler only warns in files where #nullable enable is turned on |
The compiler warns uniformly in any file |
Risk of NullReferenceException in "forgotten" files |
The risk is concentrated exactly where it can actually happen, marked with ? |
In a new project, the simplest way to achieve this consistency is to enable
<Nullable>enable</Nullable> once in the project file (.csproj), which applies
#nullable enable to every .cs file automatically, with no need to repeat the directive in
each one.
- Combined example: before and after a
Library snippet
Library snippetBringing all of the above together, here's what a Library snippet looks like without any of
the standards covered in this lesson, and its revised version:
// Before: unclear names, mixed responsibilities, no documentation, no nullability
class Library
{
public List<LibraryItem> lista = new List<LibraryItem>();
public LibraryItem Get(string t)
{
foreach (var x in lista)
{
if (x.Title == t)
{
return x;
}
}
return null;
}
public void Proc(string t, Member s)
{
var m = Get(t);
if (m != null && m.Available)
{
m.Lend();
Console.WriteLine($"'{m.Title}' lent to {s.Name}.");
}
else
{
Console.WriteLine("Could not lend it.");
}
}
}// After: descriptive names, separated responsibilities, documented, explicit nullability
#nullable enable
class Library
{
public List<LibraryItem> Catalog { get; } = new List<LibraryItem>();
/// <summary>
/// Finds an item in the catalog by its exact title.
/// </summary>
/// <param name="title">The title to search for.</param>
/// <returns>The matching item, or <c>null</c> if none matches.</returns>
public LibraryItem? FindByTitle(string title)
{
return Catalog.FirstOrDefault(item => item.Title == title);
}
/// <summary>
/// Attempts to lend the item with the given title.
/// </summary>
/// <param name="title">The title of the item to lend.</param>
/// <param name="member">The member requesting the loan.</param>
/// <returns><c>true</c> if the loan was registered; <c>false</c> otherwise.</returns>
public bool TryLendByTitle(string title, Member member)
{
LibraryItem? item = FindByTitle(title);
if (item is null || !item.Available)
{
return false;
}
item.Lend();
return true;
}
}FindByTitle and TryLendByTitle are now two methods each with a single responsibility, with
names that say exactly what they do, documented with ///, and with FindByTitle's nullability
explicit in its signature (LibraryItem?). Note that the decision of what to show on the
console no longer lives inside Library: it stays, as in section 4, in the hands of whoever
calls the method, whether that's the console, a form, or an HTTP endpoint.
Common Mistakes and Tips
- Mixing naming conventions within the same project: using
_fieldin one class and plainfieldin another, with no clear criterion, is more confusing than having no convention at all. Apply the same rule across the whole project. - Commenting the "what" instead of renaming: if you need a comment to explain what a simple line does, it's almost always preferable to improve the variable's name or extract a method with a descriptive name.
- Documenting every single line of the project with
///: it's a disproportionate effort that tends to go out of date over time. Reserve///for the public API of the central classes. - Enabling
#nullable enableonly in new files: leaves the project with an inconsistent criterion. Enable it at the project level (.csproj) so it's applied uniformly. - Tip: if you're unsure how to name something, ask yourself what name the .NET standard
library itself would use for an equivalent concept (
List,Dictionary,HttpClient...); that intuition almost always matches the correct convention.
Exercises
-
Given the following
Librarymethod, identify three coding standard problems (naming, single responsibility, nullability) and rewrite it fixing them:public Member Find(int i) { foreach (var s in Members) { if (s.Id == i) return s; } return null; } -
Add XML documentation (
///with<summary>,<param>, and<returns>) to the corrected method from the previous exercise.
Solutions
Problems: (a) the parameter i and the name Find aren't very descriptive; (b) the method
can return null but its signature doesn't reflect that (Member instead of Member?); (c)
the name doesn't distinguish "find by id" from other possible future searches (by name, for
example).
public Member? FindMemberById(int memberId)
{
return Members.FirstOrDefault(member => member.Id == memberId);
}
/// <summary>
/// Finds a member by their identifier.
/// </summary>
/// <param name="memberId">The identifier of the member to find.</param>
/// <returns>The matching member, or <c>null</c> if no member exists with that identifier.</returns>
public Member? FindMemberById(int memberId)
{
return Members.FirstOrDefault(member => member.Id == memberId);
}
Conclusion
In this lesson you've reviewed and expanded .NET's naming conventions (PascalCase/camelCase,
I/_ prefixes, Async suffix), learned about EditorConfig and code analyzers as tools that
automate those conventions, applied the single responsibility principle to separate logic from
presentation, told useful comments apart from noise, documented Library's public API with
///, and established consistent nullability as a standard across the whole project. With this
snippet of Library now cleaner and more readable, the next lesson takes a step beyond style:
design patterns, proven solutions to recurring design problems, which you'll see applied
directly to BiblioTech's own domain.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
