Attendance App: Real-Time Use of DI Lifetimes
Dependency Injection (DI) in ASP.NET Core is more than just a design pattern — it’s a powerful tool for managing service lifetimes and dependencies in real-world applications. In this guide, we'll explore how an Attendance App uses different DI lifetimes (Singleton, Scoped, Transient) to handle services like logging, attendance tracking, OTP generation, and email notifications.
π― App Features
- Students check-in/out
- Admins manage students
- Operations are logged (Singleton)
- Attendance tracked per request (Scoped)
- Email/OTP as lightweight utilities (Transient)
π Quick Overview: DI Lifetimes in ASP.NET Core
Before diving into the app, here’s a quick refresher on DI lifetimes:
- Singleton: One instance shared across the entire application lifetime. Good for stateless or shared resources (e.g., logging).
- Scoped: One instance per client request. Ideal for services that maintain request-specific state or database transactions.
- Transient: New instance every time the service is requested. Best for lightweight, stateless services.
Note on Transient Services: Transient services should ideally be stateless. Because a new instance is created every time they're requested, keeping them stateless helps avoid unexpected side effects and makes them lightweight and easy to manage.
π 1. Service Lifetimes in Real Context
| Service | Lifetime | Why? |
|---|---|---|
ILoggingService |
Singleton | One instance logs everything app-wide |
AttendanceService |
Scoped | Per HTTP request to avoid stale or shared state |
EmailNotificationService |
Transient | Lightweight utility, recreated each time |
OtpGeneratorService |
Transient | Stateless; new instance per use |
DbContext |
Scoped | Scoped per request to safely manage DB transactions |
π§Ύ 2. Interface Definitions
public interface ILoggingService {
void Log(string message);
}
public interface IAttendanceService {
string MarkAttendance(int studentId);
}
public interface IEmailNotificationService {
void Send(string to, string subject, string body);
}
public interface IOtpGeneratorService {
string GenerateOtp();
}
⚙️ 3. Implementations
public class LoggingService : ILoggingService {
private readonly string _id = Guid.NewGuid().ToString();
public void Log(string message) {
Console.WriteLine($"[{_id}] {DateTime.Now}: {message}");
}
}
public class AttendanceService : IAttendanceService {
private readonly ILoggingService _logger;
public AttendanceService(ILoggingService logger) {
_logger = logger;
}
public string MarkAttendance(int studentId) {
_logger.Log($"Attendance marked for student ID: {studentId}");
return $"Attendance marked for student {studentId}";
}
}
public class EmailNotificationService : IEmailNotificationService {
private readonly ILoggingService _logger;
public EmailNotificationService(ILoggingService logger) {
_logger = logger;
}
public void Send(string to, string subject, string body) {
_logger.Log($"Email sent to {to} with subject '{subject}'");
}
}
public class OtpGeneratorService : IOtpGeneratorService {
public string GenerateOtp() {
return new Random().Next(100000, 999999).ToString();
}
}
π¦ 4. Register Services in Program.cs
builder.Services.AddSingleton<ILoggingService, LoggingService>(); // Singleton
builder.Services.AddScoped<IAttendanceService, AttendanceService>(); // Scoped
builder.Services.AddTransient<IEmailNotificationService, EmailNotificationService>(); // Transient
builder.Services.AddTransient<IOtpGeneratorService, OtpGeneratorService>(); // Transient
// EF Core DbContext registration (Scoped by default)
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
π§π« 5. Controller Example
[ApiController]
[Route("api/[controller]")]
public class AttendanceController : ControllerBase {
private readonly IAttendanceService _attendanceService;
private readonly IEmailNotificationService _emailService;
private readonly IOtpGeneratorService _otpService;
private readonly ILoggingService _logger;
public AttendanceController(IAttendanceService attendanceService, IEmailNotificationService emailService, IOtpGeneratorService otpService, ILoggingService logger) {
_attendanceService = attendanceService;
_emailService = emailService;
_otpService = otpService;
_logger = logger;
}
[HttpPost("{studentId}")]
public IActionResult MarkAttendance(int studentId) {
var attendanceResult = _attendanceService.MarkAttendance(studentId);
string otp = _otpService.GenerateOtp();
_emailService.Send("student@school.com", "Attendance Marked", $"Your OTP: {otp}");
_logger.Log("Attendance workflow completed.");
return Ok(new {
Attendance = attendanceResult,
OtpSent = otp
});
}
}
π€ Final Output Example
{
"Attendance": "Attendance marked for student 101",
"OtpSent": "732819"
}
Note: OTP is dynamically generated; this is just a sample output.
π§ Summary of DI Usage
| Service | Real Usage | Lifetime Reason |
|---|---|---|
ILoggingService |
Log across layers | Singleton = single instance |
AttendanceService |
Track each HTTP request | Scoped = prevents state leaks |
EmailNotificationService |
Send email per event | Transient = lightweight, short-lived |
OtpGeneratorService |
Generate new OTP each time | Transient = stateless |
π Why This Matters
Choosing the correct lifetimes for your services not only ensures the right behavior but also directly impacts application performance and resource management. For example, using a Singleton for a service that maintains user-specific state could lead to data leaks and hard-to-debug bugs. On the other hand, using Scoped or Transient lifetimes unnecessarily might cause excessive object creation and memory overhead. Thoughtful lifetime management keeps your app efficient, reliable, and easier to maintain.
Comments
Post a Comment