Code Organization
Proper code organization is essential for maintainability, readability, and collaboration. C# provides several mechanisms to structure and organize your code effectively.
Namespaces
Namespaces organize code into logical groups and prevent naming conflicts.
Declaring Namespaces
// Traditional namespace declaration
namespace MyCompany.MyProduct.Core
{
public class Logger
{
// Class implementation
}
}
// File-scoped namespace declaration (C# 10+)
namespace MyCompany.MyProduct.Services;
public class UserService
{
// Class implementation
}
Nested Namespaces
namespace MyCompany.MyProduct
{
// Types in the MyCompany.MyProduct namespace
public class Product { }
namespace Core
{
// Types in the MyCompany.MyProduct.Core namespace
public class Engine { }
}
namespace UI
{
// Types in the MyCompany.MyProduct.UI namespace
public class Window { }
}
}
Namespace Best Practices
- Use a hierarchical naming scheme (e.g.,
CompanyName.ProductName.ComponentName) - Avoid using the same name for a namespace and a type within that namespace
- Keep namespaces focused on a specific functionality or component
- Use PascalCase for namespace names
Using Directives
Using directives simplify code by eliminating the need to specify fully qualified type names.
Basic Using Directives
// Import namespaces
using System;
using System.Collections.Generic;
using System.Linq;
// Now you can use types from these namespaces without qualification
List<int> numbers = new List<int>();
DateTime now = DateTime.Now;
Static Using Directives
// Import static members
using static System.Math;
using static System.Console;
// Use static members directly
double result = Sqrt(16);
WriteLine("The square root of 16 is " + result);
Using Aliases
// Create aliases for namespaces or types
using Process = System.Diagnostics.Process;
using Text = System.Text;
using WinForm = System.Windows.Forms.Form;
// Use the aliases
Process currentProcess = Process.GetCurrentProcess();
Text.StringBuilder sb = new Text.StringBuilder();
Global Using Directives (C# 10+)
Global using directives apply to all files in the project:
// In a file like GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
You can also enable implicit usings in your project file:
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
File-Scoped Types (C# 11+)
File-scoped types are only visible within the file where they're declared:
// This class is only visible within this file
file class Helper
{
public static void DoSomething() { }
}
// Public class that can use the file-scoped Helper
public class PublicService
{
public void Process()
{
// Can use Helper because it's in the same file
Helper.DoSomething();
}
}
File-scoped types are useful for:
- Implementation details that shouldn't be exposed
- Source generators that need to avoid naming conflicts
- Utility classes used only within a specific file
Partial Types
Partial types allow splitting a class, struct, or interface definition across multiple files.
Basic Partial Classes
// In File1.cs
partial class Customer
{
public string Name { get; set; }
public string Email { get; set; }
public void UpdateProfile() { /* ... */ }
}
// In File2.cs
partial class Customer
{
public string Address { get; set; }
public string Phone { get; set; }
public void PlaceOrder() { /* ... */ }
}
Partial Methods
Partial methods allow one part to declare a method that another part can optionally implement:
// In GeneratedCode.cs (possibly auto-generated)
partial class Order
{
// Declaration only - no implementation required
partial void OnOrderProcessed(DateTime processedTime);
public void Process()
{
// Business logic
// Call the partial method - does nothing if not implemented
OnOrderProcessed(DateTime.Now);
}
}
// In UserCode.cs
partial class Order
{
// Implementation of the partial method
partial void OnOrderProcessed(DateTime processedTime)
{
Console.WriteLine($"Order processed at {processedTime}");
// Custom logic here
}
}
Partial Properties and Indexers (C# 13+)
C# 13 introduced partial properties and indexers:
// In File1.cs
partial class Document
{
// Declare the property
public partial string Title { get; }
// Declare the indexer
public partial string this[int index] { get; }
}
// In File2.cs
partial class Document
{
// Implement the property
public partial string Title => _title;
// Implement the indexer
public partial string this[int index] => _sections[index];
}
Partial Events and Constructors (C# 14+)
C# 14 introduced partial events and partial instance constructors. Each must have exactly one defining declaration and one implementing declaration.
// In File1.cs
partial class DataProcessor
{
// Declare the event
public partial event EventHandler<DataProcessedEventArgs> DataProcessed;
protected virtual void OnDataProcessed(DataProcessedEventArgs e)
{
DataProcessed?.Invoke(this, e);
}
}
// In File2.cs
partial class DataProcessor
{
// Implement the event (must include add/remove accessors)
public partial event EventHandler<DataProcessedEventArgs> DataProcessed
{
add => _handler += value;
remove => _handler -= value;
}
}
Partial constructors follow the same declaring/implementing pattern. Only the implementing declaration may include a this() or base() constructor initializer, and only one partial declaration of the type may use primary constructor syntax:
// In File1.cs - defining declaration
public partial class Widget
{
public partial Widget(string name);
}
// In File2.cs - implementing declaration
public partial class Widget
{
private readonly string _name;
public partial Widget(string name)
{
_name = name;
}
}
Access Modifiers
Access modifiers control the visibility and accessibility of types and members.
Type-Level Access Modifiers
// Public - accessible from any assembly
public class PublicClass { }
// Internal - accessible only within the same assembly
internal class InternalClass { }
// File-scoped (C# 11+) - accessible only within the same file
file class FileClass { }
// Private (nested classes only) - accessible only within the containing type
public class Container
{
private class PrivateNestedClass { }
}
Member-Level Access Modifiers
public class Example
{
// Public - accessible from any code
public int PublicField;
// Private - accessible only within this class
private int _privateField;
// Protected - accessible within this class and derived classes
protected int ProtectedField;
// Internal - accessible within the same assembly
internal int InternalField;
// Protected Internal - accessible within the same assembly OR derived classes
protected internal int ProtectedInternalField;
// Private Protected (C# 7.2+) - accessible within the same assembly AND derived classes
private protected int PrivateProtectedField;
}
Access Modifier Best Practices
- Use the most restrictive access level that works for your needs
- Make fields private and expose them through properties if needed
- Use internal for types that should only be used within your assembly
- Use protected sparingly and only when inheritance is part of the design
Internal vs Public
Choosing between internal and public is an important architectural decision:
- Public: Use for types and members that form your library's API
- Internal: Use for implementation details that should be hidden from consumers
// Public API
public interface IUserService
{
User GetUser(int id);
void UpdateUser(User user);
}
// Internal implementation
internal class UserService : IUserService
{
private readonly IUserRepository _repository;
internal UserService(IUserRepository repository)
{
_repository = repository;
}
public User GetUser(int id) => _repository.GetById(id);
public void UpdateUser(User user) => _repository.Update(user);
// Internal helper methods
internal void ValidateUser(User user) { /* ... */ }
}
Making Internals Visible to Other Assemblies
You can selectively expose internal types to specific assemblies:
// In AssemblyInfo.cs or any .cs file
[assembly: InternalsVisibleTo("MyCompany.MyProduct.Tests")]
[assembly: InternalsVisibleTo("MyCompany.MyProduct.DesignTime")]