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:
- Organize files by feature or layer
- Implement repository pattern
- Add dependency injection
- Configure logging
- Add error handling middleware
- Implement configuration management