The Expense Manager API is a .NET 9-based application designed to track expenses efficiently. It follows Clean Architecture and adheres to SOLID principles for maintainability and scalability.
β
CRUD Operations (Add, Update, Retrieve, Delete Expenses)
β
Expense Categories
β
SQLite Persistence
β
Resilient API with Rate Limiting
β
CORS Policy for Cross-Origin Requests
β
Logging with Serilog
β
Performance Optimizations
β
Centralized Logging with SEQ (Optional)
β
SAAS Enablement (all 3 strategies)
- .NET 9 (Minimal API + Controllers)
- SQLite (Persistent Storage)
- Entity Framework Core (Database ORM)
- Serilog (Logging)
- Rate Limiting (Resiliency)
git clone <repository-url>
cd ExpenseManager
dotnet restore
dotnet ef migrations add InitialCreate
dotnet ef database update
dotnet run
The API will be available at http://localhost:5000
make dockerrun
or
docker run -d -p 8080:8080 --name expense-api-container expense-api
The API will now be accessible at: http://localhost:8080
The API uses SQLite for data persistence. The database file (expense.db
) is automatically created upon running the migrations.
If needed, delete expense.db
and reapply migrations:
rm expense.db
dotnet ef database update
Serilog is configured to log messages to console, files, and SEQ.
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/api-log.txt", rollingInterval: RollingInterval.Day)
.WriteTo.Seq("http://localhost:5341")
.Enrich.FromLogContext()
.MinimumLevel.Information()
.CreateLogger();
builder.Host.UseSerilog();
If you want to enable centralized logging, start SEQ using Docker:
docker run --name seq -d -e ACCEPT_EULA=Y -p 5341:80 datalust/seq
Then, open http://localhost:5341 to view logs.
Method | Endpoint | Description |
---|---|---|
GET | /api/expenses |
Retrieve expenses |
POST | /api/expenses |
Add a new expense |
PUT | /api/expenses/{id} |
Update an expense |
DELETE | /api/expenses/{id} |
Delete an expense |
To prevent API abuse, the following rate limits are applied:
- Max 100 requests per 10 minutes per IP
Implemented in Program.cs
:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", policy => policy
.PermitLimit(100)
.Window(TimeSpan.FromMinutes(10)));
});
To allow cross-origin requests, CORS is enabled:
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy => policy
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
Activate in the middleware:
app.UseCors("AllowAll");
Enable Gzip/Brotli compression for faster API responses:
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
});
app.UseResponseCompression();
Optimize database access using connection pooling:
builder.Services.AddDbContextPool<ExpenseDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
β
SOLID Principles - Clean Architecture & Decoupled Design
β
Dependency Injection - Proper Service Layer Usage
β
Rate Limiting - API Protection
β
Structured Logging - Serilog + SEQ for Monitoring
β
CORS Policy - Secure Cross-Origin Access
β
Performance Tweaks - Response Compression, DB Connection Pooling
read
For your Expense Manager API, here are some design patterns that can be implemented to improve maintainability, scalability, and testability while adhering to SOLID principles:
- Use Case: Abstracts database operations, making the API decoupled from the persistence logic.
- Implementation: The
IExpenseRepository
interface ensures loose coupling, and concrete implementations interact with SQLite via EF Core.
public interface IExpenseRepository
{
Task<IEnumerable<Expense>> GetAllAsync();
Task<Expense> GetByIdAsync(int id);
Task AddAsync(Expense expense);
Task UpdateAsync(Expense expense);
Task DeleteAsync(int id);
}
β
Benefits:
β Encapsulates database logic
β Makes the app easier to switch databases (e.g., SQL Server, PostgreSQL)
- Use Case: When multiple database operations need to be committed together (e.g., adding an expense + logging the action).
- Implementation: Wrap repository operations in a single transaction.
public interface IUnitOfWork
{
IExpenseRepository Expenses { get; }
Task<int> SaveChangesAsync();
}
β
Benefits:
β Ensures consistency across multiple database operations
β Reduces unnecessary database calls
- Use Case: If there are different types of expenses (e.g., Personal, Business, Investment) and creation logic varies.
- Implementation: Use a factory to instantiate different expense objects.
public static class ExpenseFactory
{
public static Expense CreateExpense(string type, string title, decimal amount)
{
return type switch
{
"Personal" => new PersonalExpense(title, amount),
"Business" => new BusinessExpense(title, amount),
_ => throw new ArgumentException("Invalid Expense Type")
};
}
}
β
Benefits:
β Encapsulates object creation logic
β Easier to introduce new expense types
- Use Case: If expense categorization rules change frequently.
- Implementation: Define multiple categorization strategies dynamically.
public interface IExpenseCategorizationStrategy
{
string Categorize(Expense expense);
}
public class AmountBasedCategorization : IExpenseCategorizationStrategy
{
public string Categorize(Expense expense)
{
return expense.Amount > 1000 ? "High" : "Low";
}
}
β
Benefits:
β Makes the categorization logic flexible & interchangeable
β Avoids if-else clutter in the business logic
- Use Case: If the application needs to scale by separating read and write operations (e.g., expensive analytics queries).
- Implementation: Define separate commands (writes) and queries (reads).
public record AddExpenseCommand(string Title, decimal Amount);
public record GetExpenseQuery(int Id);
β
Benefits:
β Improves scalability when using read replicas
β Optimizes performance for complex queries
- Use Case: If you need to add logging, caching, or validation without modifying the core repository logic.
- Implementation: Decorate
IExpenseRepository
with logging functionality.
public class ExpenseRepositoryLoggingDecorator : IExpenseRepository
{
private readonly IExpenseRepository _inner;
private readonly ILogger<ExpenseRepositoryLoggingDecorator> _logger;
public ExpenseRepositoryLoggingDecorator(IExpenseRepository inner, ILogger<ExpenseRepositoryLoggingDecorator> logger)
{
_inner = inner;
_logger = logger;
}
public async Task<IEnumerable<Expense>> GetAllAsync()
{
_logger.LogInformation("Fetching all expenses.");
return await _inner.GetAllAsync();
}
}
β
Benefits:
β Adds cross-cutting concerns (e.g., logging) without modifying existing code
β Follows Open/Closed Principle (OCP)
- Use Case: If multiple validation steps need to be executed sequentially.
- Implementation: Define linked handlers for different validation steps.
public abstract class ExpenseValidationHandler
{
protected ExpenseValidationHandler? Next;
public void SetNext(ExpenseValidationHandler next) => Next = next;
public abstract void Handle(Expense expense);
}
public class AmountValidationHandler : ExpenseValidationHandler
{
public override void Handle(Expense expense)
{
if (expense.Amount <= 0)
throw new Exception("Amount must be greater than zero.");
Next?.Handle(expense);
}
}
β
Benefits:
β Makes the validation modular and extensible
β Avoids a massive if-else block
- Use Case: If other services need to react when an expense is added (e.g., send email notification).
- Implementation: Use an event-driven approach.
public class ExpenseNotifier
{
private readonly List<IObserver> _observers = new();
public void Subscribe(IObserver observer) => _observers.Add(observer);
public void Notify(Expense expense) => _observers.ForEach(o => o.Update(expense));
}
β
Benefits:
β Allows event-driven behavior (e.g., notifying services)
β Enhances scalability without modifying the core logic
Pattern | Use Case | Benefit |
---|---|---|
Repository | Encapsulate database operations | Decouples persistence from business logic |
Unit of Work | Handle transactions | Ensures atomicity & consistency |
Factory | Create different types of expenses | Centralizes object creation logic |
Strategy | Dynamic expense categorization | Avoids complex if-else logic |
CQRS | Separate read & write operations | Enhances scalability |
Decorator | Add logging, caching | Extends behavior without modifying core logic |
Chain of Responsibility | Expense validation | Modular validation steps |
Observer | Notify other services | Enables event-driven architecture |
read
The Decorator Pattern is beneficial in the Expense Manager API when you need to extend the behavior of existing services without modifying them directly. In this use case, it can help with:
- Logging Decorator β Log every expense-related operation.
- Caching Decorator β Cache frequent read operations (e.g., fetching expenses).
- Validation Decorator β Add validation rules dynamically.
- Security/Authorization Decorator β Check user roles before executing a request.
- Transaction Decorator β Ensure database consistency.
Instead of adding logging directly into ExpenseRepository
, we wrap it in a decorator.
public class ExpenseRepositoryLoggingDecorator : IExpenseRepository
{
private readonly IExpenseRepository _inner;
private readonly ILogger<ExpenseRepositoryLoggingDecorator> _logger;
public ExpenseRepositoryLoggingDecorator(IExpenseRepository inner, ILogger<ExpenseRepositoryLoggingDecorator> logger)
{
_inner = inner;
_logger = logger;
}
public async Task<IEnumerable<Expense>> GetAllAsync()
{
_logger.LogInformation("Fetching all expenses.");
var result = await _inner.GetAllAsync();
_logger.LogInformation($"Retrieved {result.Count()} expenses.");
return result;
}
public async Task<Expense> GetByIdAsync(int id)
{
_logger.LogInformation($"Fetching expense with ID {id}.");
return await _inner.GetByIdAsync(id);
}
public async Task AddAsync(Expense expense)
{
_logger.LogInformation($"Adding expense: {expense.Title}, Amount: {expense.Amount}");
await _inner.AddAsync(expense);
_logger.LogInformation("Expense added successfully.");
}
public async Task UpdateAsync(Expense expense)
{
_logger.LogInformation($"Updating expense ID {expense.Id}.");
await _inner.UpdateAsync(expense);
_logger.LogInformation("Expense updated successfully.");
}
public async Task DeleteAsync(int id)
{
_logger.LogInformation($"Deleting expense ID {id}.");
await _inner.DeleteAsync(id);
_logger.LogInformation("Expense deleted successfully.");
}
}
Register the decorated repository in DI container in Program.cs
:
builder.Services.AddScoped<IExpenseRepository, ExpenseRepository>();
builder.Services.Decorate<IExpenseRepository, ExpenseRepositoryLoggingDecorator>();
π Benefits:
β Non-intrusive logging (no need to modify ExpenseRepository
)
β Extensible (easily add more behaviors)
β Follows Open/Closed Principle (OCP)
Reduces database queries by caching expenses.
public class ExpenseRepositoryCachingDecorator : IExpenseRepository
{
private readonly IExpenseRepository _inner;
private readonly IMemoryCache _cache;
public ExpenseRepositoryCachingDecorator(IExpenseRepository inner, IMemoryCache cache)
{
_inner = inner;
_cache = cache;
}
public async Task<IEnumerable<Expense>> GetAllAsync()
{
return await _cache.GetOrCreateAsync("expenses_cache", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return await _inner.GetAllAsync();
});
}
public async Task<Expense> GetByIdAsync(int id)
{
return await _cache.GetOrCreateAsync($"expense_{id}", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return await _inner.GetByIdAsync(id);
});
}
public async Task AddAsync(Expense expense)
{
await _inner.AddAsync(expense);
_cache.Remove("expenses_cache");
}
public async Task UpdateAsync(Expense expense)
{
await _inner.UpdateAsync(expense);
_cache.Remove($"expense_{expense.Id}");
}
public async Task DeleteAsync(int id)
{
await _inner.DeleteAsync(id);
_cache.Remove($"expense_{id}");
}
}
builder.Services.AddScoped<IExpenseRepository, ExpenseRepository>();
builder.Services.Decorate<IExpenseRepository, ExpenseRepositoryCachingDecorator>();
π Benefits:
β Reduces database load
β Improves API performance
β Follows OCP (Open/Closed Principle)
Ensures expenses have valid data before persisting.
public class ExpenseRepositoryValidationDecorator : IExpenseRepository
{
private readonly IExpenseRepository _inner;
public ExpenseRepositoryValidationDecorator(IExpenseRepository inner)
{
_inner = inner;
}
public async Task AddAsync(Expense expense)
{
if (string.IsNullOrEmpty(expense.Title))
throw new ArgumentException("Title is required.");
if (expense.Amount <= 0)
throw new ArgumentException("Amount must be greater than zero.");
await _inner.AddAsync(expense);
}
public async Task<IEnumerable<Expense>> GetAllAsync() => await _inner.GetAllAsync();
public async Task<Expense> GetByIdAsync(int id) => await _inner.GetByIdAsync(id);
public async Task UpdateAsync(Expense expense) => await _inner.UpdateAsync(expense);
public async Task DeleteAsync(int id) => await _inner.DeleteAsync(id);
}
builder.Services.AddScoped<IExpenseRepository, ExpenseRepository>();
builder.Services.Decorate<IExpenseRepository, ExpenseRepositoryValidationDecorator>();
π Benefits:
β Keeps validation separate from repository logic
β Prevents invalid data from entering the database
Pattern | Use Case | Benefit |
---|---|---|
Logging | Log repository actions | Non-intrusive, structured logs |
Caching | Reduce DB calls for reads | Improves performance |
Validation | Validate expenses before saving | Keeps concerns separate |
π Best Part? β
You can stack multiple decorators together!
Example:
builder.Services.AddScoped<IExpenseRepository, ExpenseRepository>();
builder.Services.Decorate<IExpenseRepository, ExpenseRepositoryValidationDecorator>();
builder.Services.Decorate<IExpenseRepository, ExpenseRepositoryCachingDecorator>();
builder.Services.Decorate<IExpenseRepository, ExpenseRepositoryLoggingDecorator>();
β Validation β Caching β Logging in order π
πΉ The Decorator Pattern helps in adding cross-cutting concerns dynamically.
πΉ No need to modify core repository code β just wrap and extend!
πΉ Follows OCP (Open/Closed Principle) β Code is open for extension, closed for modification.
πΉ Improves maintainability & testability.
Would you like help implementing unit tests for these decorators? π
This project is open-source and available under the MIT License.
This API is production-ready, resilient, and scalable. Feel free to customize it for your needs!
π¬ Need Help? Reach out for support! π