Project Structure Best Practices

Intermediate 25 min read Lesson 2 of 4

Why Project Structure Matters

A well-organized project structure is crucial for maintainability, scalability, and team collaboration. It makes the codebase easier to understand, navigate, and extend over time.

Key Principle

"Separation of Concerns" - Each component should have a single, well-defined responsibility.

Recommended Project Structure

ASP.NET Core Project Structure

ProjectName/
├── Properties/
│   └── launchSettings.json          // Development environment settings
├── wwwroot/                          // Static files
│   ├── css/                          // Stylesheets
│   │   └── site.css
│   ├── js/                           // JavaScript files
│   │   └── site.js
│   ├── images/                       // Images
│   ├── lib/                          // Third-party libraries
│   └── favicon.ico                   // Favicon
├── Pages/                            // Razor Pages
│   ├── Shared/                       // Shared pages and partials
│   │   ├── _Layout.cshtml            // Main layout
│   │   ├── _ValidationScriptsPartial.cshtml
│   │   └── _SidebarNav.cshtml
│   ├── Index.cshtml                  // Home page
│   ├── Index.cshtml.cs               // Home page model
│   ├── About.cshtml                  // About page
│   ├── Contact.cshtml                // Contact page
│   ├── Error.cshtml                  // Error page
│   └── _ViewImports.cshtml           // Common directives
│   └── _ViewStart.cshtml             // Layout selection
├── Models/                           // Data models
│   ├── Entities/                     // Database entities
│   ├── ViewModels/                   // View models for pages
│   └── DTOs/                         // Data transfer objects
├── Services/                         // Business logic
│   ├── Interfaces/                   // Service interfaces
│   └── Implementations/              // Service implementations
├── Data/                             // Database context
│   ├── ApplicationDbContext.cs
│   └── Migrations/                   // EF Core migrations
├── Middleware/                       // Custom middleware
├── Filters/                          // Action filters
├── Extensions/                       // Extension methods
├── Helpers/                          // Helper classes
├── Configuration/                    // Configuration classes
├── appsettings.json                  // Application settings
├── appsettings.Development.json      // Development settings
├── appsettings.Production.json       // Production settings
├── Program.cs                        // Application entry point
└── ProjectName.csproj                // Project file

Separation of Concerns

1. Presentation Layer (UI)

// Pages/ - Contains all UI pages
// Pages/Shared/ - Shared UI components
// wwwroot/ - Static assets
// Views/ - If using MVC pattern

2. Business Logic Layer (Services)

// Services/ - Contains business logic
// Services/Interfaces/ - Service contracts
// Services/Implementations/ - Service implementations

3. Data Access Layer (Repositories)

// Data/ - Database context
// Models/Entities/ - Database entities
// Models/DTOs/ - Data transfer objects

Naming Conventions

Type Convention Example
Classes PascalCase UserService
Interfaces I + PascalCase IUserService
Methods PascalCase GetUserById
Properties PascalCase UserName
Fields _camelCase _userRepository
Parameters camelCase userId
Constants UPPER_CASE MAX_RETRY_COUNT
Razor Pages PascalCase UserProfile.cshtml
View Models PascalCase + ViewModel UserProfileViewModel

Best Practices

1. Dependency Injection

// Services/Interfaces/IUserService.cs
public interface IUserService
{
    Task GetUserByIdAsync(int id);
    Task> GetAllUsersAsync();
    Task CreateUserAsync(User user);
}

// Services/Implementations/UserService.cs
public class UserService : IUserService
{
    private readonly IUserRepository _userRepository;
    private readonly ILogger _logger;

public UserService(IUserRepository userRepository, ILogger logger)
    {
        _userRepository = userRepository;
        _logger = logger;
    }

public async Task GetUserByIdAsync(int id)
    {
        _logger.LogInformation($"Getting user with ID: {id}");
        return await _userRepository.GetByIdAsync(id);
    }
}

// Program.cs - Register services
builder.Services.AddScoped();
builder.Services.AddScoped();

2. Repository Pattern

// Data/Repositories/IUserRepository.cs
public interface IUserRepository
{
    Task GetByIdAsync(int id);
    Task> GetAllAsync();
    Task AddAsync(User user);
    Task UpdateAsync(User user);
    Task DeleteAsync(int id);
}

// Data/Repositories/UserRepository.cs
public class UserRepository : IUserRepository
{
    private readonly ApplicationDbContext _context;

public UserRepository(ApplicationDbContext context)
    {
        _context = context;
    }

public async Task GetByIdAsync(int id)
    {
        return await _context.Users.FindAsync(id);
    }

public async Task> GetAllAsync()
    {
        return await _context.Users.ToListAsync();
    }

public async Task AddAsync(User user)
    {
        await _context.Users.AddAsync(user);
        await _context.SaveChangesAsync();
    }
}

3. Configuration Management

// Configuration/AppSettings.cs
public class AppSettings
{
    public string ApiUrl { get; set; }
    public int TimeoutSeconds { get; set; }
    public EmailSettings Email { get; set; }
}

public class EmailSettings
{
    public string SmtpServer { get; set; }
    public int SmtpPort { get; set; }
    public string SenderEmail { get; set; }
}

// Program.cs - Bind configuration
var appSettings = builder.Configuration.GetSection("AppSettings").Get();
builder.Services.Configure(builder.Configuration.GetSection("AppSettings"));

4. Error Handling

// Middleware/ExceptionHandlingMiddleware.cs
public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    
    public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger)
    {
        _next = next;
        _logger = logger;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred");
            await HandleExceptionAsync(context, ex);
        }
    }
    
    private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.StatusCode = exception switch
        {
            NotFoundException => StatusCodes.Status404NotFound,
            UnauthorizedException => StatusCodes.Status401Unauthorized,
            _ => StatusCodes.Status500InternalServerError
        };
        
        await context.Response.WriteAsJsonAsync(new { error = exception.Message });
    }
}

5. Logging

// Program.cs - Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
builder.Logging.AddEventLog();

// Using Serilog
Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
    .WriteTo.ApplicationInsights("your-instrumentation-key", TelemetryConverter.Traces)
    .CreateLogger();

builder.Host.UseSerilog();

Common Anti-Patterns to Avoid

  • God Object: A single class with too many responsibilities
  • Hardcoding: Hard-coded values instead of configuration
  • Magic Numbers: Using unexplained numeric literals
  • Spaghetti Code: Poorly structured, tangled code
  • Copy-Paste: Duplicating code instead of reusing
  • Over-Engineering: Building for requirements that don't exist
Key Takeaway
  • Organize your project with clear separation of concerns
  • Follow consistent naming conventions
  • Use dependency injection for loose coupling
  • Implement proper error handling and logging
  • Keep your codebase clean and maintainable
Exercise

Restructure a project with best practices:

  1. Organize files by feature or layer
  2. Implement repository pattern
  3. Add dependency injection
  4. Configure logging
  5. Add error handling middleware
  6. Implement configuration management
Test Your Knowledge - Take Quiz