<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Madusanka Bandara</title>
    <description>The latest articles on DEV Community by Madusanka Bandara (@madusanka_bandara).</description>
    <link>https://dev.to/madusanka_bandara</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3437209%2Fe60bf1b6-d871-4187-98b7-1441c19cc0cf.jpg</url>
      <title>DEV Community: Madusanka Bandara</title>
      <link>https://dev.to/madusanka_bandara</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/madusanka_bandara"/>
    <language>en</language>
    <item>
      <title>Mastering Database Interceptors in .NET Core Web API (Beginner to Hero)</title>
      <dc:creator>Madusanka Bandara</dc:creator>
      <pubDate>Tue, 02 Sep 2025 15:19:14 +0000</pubDate>
      <link>https://dev.to/madusanka_bandara/mastering-database-interceptors-in-net-core-web-api-beginner-to-hero-18g8</link>
      <guid>https://dev.to/madusanka_bandara/mastering-database-interceptors-in-net-core-web-api-beginner-to-hero-18g8</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;When building modern .NET Web API applications, database operations are at the heart of everything. But have you ever wondered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What SQL queries are being executed?&lt;/li&gt;
&lt;li&gt;How can I log or measure query performance?&lt;/li&gt;
&lt;li&gt;Can I stop dangerous commands before they hit the database?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answer lies in database interceptors.&lt;/p&gt;

&lt;p&gt;In this article, we’ll explore database interception in Entity Framework Core (EF Core) across .NET 6, 7, 8, and 9.&lt;br&gt;
We’ll start at a beginner level (just logging queries) and work our way up to hero-level scenarios like performance monitoring, auditing, and blocking harmful SQL.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Prerequisites
&lt;/h2&gt;

&lt;p&gt;A working .NET 6, 7, 8, or 9 Web API project&lt;br&gt;
Basic knowledge of Entity Framework Core&lt;/p&gt;

&lt;p&gt;Installed EF Core NuGet packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Beginner Level: Setting Up a Web API with EF Core
&lt;/h2&gt;

&lt;p&gt;Let’s start with a simple User entity and DbContext.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions&amp;lt;AppDbContext&amp;gt; options) : base(options) { }

    public DbSet&amp;lt;User&amp;gt; Users { get; set; }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;Program.cs&lt;/code&gt;, register the context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext&amp;lt;AppDbContext&amp;gt;(options =&amp;gt;
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

app.MapGet("/users", async (AppDbContext db) =&amp;gt; await db.Users.ToListAsync());

app.Run();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Creating a Simple Logging Interceptor
&lt;/h2&gt;

&lt;p&gt;An interceptor lets us hook into SQL commands before they are sent to the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;

public class LoggingInterceptor : DbCommandInterceptor
{
    public override InterceptionResult&amp;lt;DbDataReader&amp;gt; ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult&amp;lt;DbDataReader&amp;gt; result)
    {
        Console.WriteLine($"[SQL LOG] ReaderExecuting: {command.CommandText}");
        return base.ReaderExecuting(command, eventData, result);
    }

    public override InterceptionResult&amp;lt;int&amp;gt; NonQueryExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult&amp;lt;int&amp;gt; result)
    {
        Console.WriteLine($"[SQL LOG] NonQueryExecuting: {command.CommandText}");
        return base.NonQueryExecuting(command, eventData, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Intermediate Level: Measuring Performance
&lt;/h2&gt;

&lt;p&gt;We can go beyond logging and measure query execution time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;

public class PerformanceInterceptor: DbCommandInterceptor
{
    public override DbDataReader ReaderExecuted(
        DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
    {
        var elapsed = eventData?.Duration.TotalMilliseconds ?? 0;
        Console.WriteLine($"[PERF] Query took {elapsed} ms. SQL: {Truncate(command.CommandText)}");
        return base.ReaderExecuted(command, eventData, result);
    }

    public override int NonQueryExecuted(
        DbCommand command, CommandExecutedEventData eventData, int result)
    {
        var elapsed = eventData?.Duration.TotalMilliseconds ?? 0;
        Console.WriteLine($"[PERF] NonQuery took {elapsed} ms. SQL: {Truncate(command.CommandText)}");
        return base.NonQueryExecuted(command, eventData, result);
    }

    private string Truncate(string s, int len = 200) =&amp;gt;
        string.IsNullOrEmpty(s) ? s : (s.Length &amp;lt;= len ? s : s.Substring(0, len) + "...");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Advanced Level: Blocking Dangerous SQL
&lt;/h2&gt;

&lt;p&gt;What if someone accidentally writes a DELETE without WHERE? We can block it!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;

public class SecurityInterceptor : DbCommandInterceptor
{
    public override InterceptionResult&amp;lt;int&amp;gt; NonQueryExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult&amp;lt;int&amp;gt; result)
    {
        var sql = (command.CommandText ?? string.Empty).TrimStart();

        // Case-insensitive check and ignore leading comments/whitespace
        if (sql.StartsWith("DELETE", StringComparison.OrdinalIgnoreCase)
            &amp;amp;&amp;amp; !sql.IndexOf("WHERE", StringComparison.OrdinalIgnoreCase).Equals(-1) == false) // no WHERE present
        {
            // Block dangerous DELETE without WHERE
            Console.WriteLine("[SECURITY] Blocked dangerous DELETE without WHERE.");
            throw new InvalidOperationException("Blocked dangerous DELETE without WHERE clause.");
        }

        return base.NonQueryExecuting(command, eventData, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Hero Level: Auditing User Actions
&lt;/h2&gt;

&lt;p&gt;We can use interceptors for auditing who ran which query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.AspNetCore.Http;

public class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public AuditSaveChangesInterceptor(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public override InterceptionResult&amp;lt;int&amp;gt; SavingChanges(DbContextEventData eventData, InterceptionResult&amp;lt;int&amp;gt; result)
    {
        var userName = _httpContextAccessor?.HttpContext?.User?.Identity?.Name ?? "anonymous";
        Console.WriteLine($"[AUDIT] User '{userName}' is calling SaveChanges at {DateTime.UtcNow:O}");
        return base.SavingChanges(eventData, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  8. Registering the Interceptor in &lt;code&gt;Program.cs&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Program.cs&lt;/code&gt; (minimal hosting; ensure interceptors are registered as IInterceptor)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();

// Register interceptors as IInterceptor so GetServices&amp;lt;IInterceptor&amp;gt;() returns them
builder.Services.AddScoped&amp;lt;IInterceptor, LoggingInterceptor&amp;gt;();
builder.Services.AddScoped&amp;lt;IInterceptor, PerformanceInterceptor&amp;gt;();
builder.Services.AddScoped&amp;lt;IInterceptor, SecurityInterceptor&amp;gt;();
builder.Services.AddScoped&amp;lt;IInterceptor, AuditSaveChangesInterceptor&amp;gt;();

// Add DbContext and inject resolved interceptors into EF Core options
builder.Services.AddDbContext&amp;lt;AppDbContext&amp;gt;((serviceProvider, options) =&amp;gt;
{
    var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
                           "Server=(localdb)\\mssqllocaldb;Database=EfInterceptorsDemo;Trusted_Connection=True;";

    options.UseSqlServer(connectionString);

    // Resolve all registered IInterceptor implementations
    var interceptors = serviceProvider.GetServices&amp;lt;IInterceptor&amp;gt;().ToArray();
    if (interceptors.Any())
    {
        options.AddInterceptors(interceptors);
    }

    // Helpful for debugging — optional in production
    options.EnableSensitiveDataLogging();
    options.LogTo(Console.WriteLine, LogLevel.Information);
});

builder.Services.AddControllers();
var app = builder.Build();

app.MapControllers();
app.Run();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  9. UsersController (controller + simple business logic)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly AppDbContext _db;

    public UsersController(AppDbContext db) =&amp;gt; _db = db;

    [HttpGet]
    public async Task&amp;lt;IActionResult&amp;gt; GetUsers()
    {
        var users = await _db.Users.ToListAsync(); // triggers ReaderExecuting / ReaderExecuted
        return Ok(users);
    }

    [HttpPost]
    public async Task&amp;lt;IActionResult&amp;gt; AddUser([FromQuery] string name)
    {
        if (string.IsNullOrWhiteSpace(name)) return BadRequest("Name required");
        _db.Users.Add(new User { Name = name });
        await _db.SaveChangesAsync(); // triggers SaveChangesInterceptor + NonQueryExecuting/NonQueryExecuted
        return Ok();
    }

    [HttpDelete("dangerous")]
    public IActionResult AttemptDangerousDelete()
    {
        // This intentionally sends 'DELETE FROM Users' without WHERE to trigger the security interceptor
        _db.Database.ExecuteSqlRaw("DELETE FROM Users");
        return Ok("Attempted dangerous delete");
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  10. How to test (step-by-step)
&lt;/h2&gt;

&lt;p&gt;i. Run the app (F5 or &lt;code&gt;dotnet run&lt;/code&gt;). Watch console.&lt;/p&gt;

&lt;p&gt;ii. Create DB / Apply migrations (or ensure DB exists). Example quick seed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use EF migrations or ensure &lt;code&gt;Users&lt;/code&gt; table exists. For quick test you can create with SQL Server LocalDB or &lt;code&gt;dotnet ef migrations add Init &amp;amp;&amp;amp; dotnet ef database update&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;u&gt;iii. Test Logging + Performance&lt;/u&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET http://localhost:5000/api/users&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Expected console output (example):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[SQL LOG] ReaderExecuting: SELECT [u].[Id], [u].[Name] FROM [Users] AS [u]
[PERF] Query took 12 ms. SQL: SELECT [u].[Id], [u].[Name] FROM [Users] AS [u]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dncqepjimihn30vpopg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dncqepjimihn30vpopg.png" alt=" " width="800" height="140"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;u&gt;iv. Test Audit&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;POST &lt;code&gt;http://localhost:5000/api/users?name=Alice&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Expected console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[AUDIT] User 'anonymous' is calling SaveChanges at 2025-09-01T...
[SQL LOG] NonQueryExecuting: INSERT INTO ...
[PERF] NonQuery took 15 ms. SQL: INSERT INTO ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8q9og1yw0b6yfk07tak.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8q9og1yw0b6yfk07tak.png" alt=" " width="800" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;u&gt;v. Test Security (blocked DELETE)&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;DELETE &lt;code&gt;http://localhost:5000/api/users/dangerous&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Expected API response: 500 Internal Server Error&lt;/p&gt;

&lt;p&gt;Console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[SECURITY] Blocked dangerous DELETE without WHERE.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fci034b4w2d71noyrh71q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fci034b4w2d71noyrh71q.png" alt=" " width="800" height="127"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The exception thrown by the interceptor will be visible in logs and will prevent row deletion.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep interceptors lightweight (avoid long-running logic).&lt;/li&gt;
&lt;li&gt;Use structured logging (Serilog, Seq, Application Insights) instead of &lt;code&gt;Console.WriteLine&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Separate concerns into multiple interceptors (logging, auditing, security).&lt;/li&gt;
&lt;li&gt;Test performance impact before deploying to production.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  12. Conclusion
&lt;/h2&gt;

&lt;p&gt;Database Interceptors in Entity Framework Core (EF Core) are an underused but powerful feature that can transform how you interact with your database layer. They allow you to observe, manipulate, and enhance database operations seamlessly — all without altering your application logic directly.&lt;/p&gt;

&lt;p&gt;Here’s why interceptors are so valuable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log SQL Queries&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Capture every executed SQL statement.&lt;/li&gt;
&lt;li&gt;Helps with debugging, troubleshooting, and understanding query patterns.&lt;/li&gt;
&lt;li&gt;Ensures visibility into how EF Core translates LINQ into SQL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Monitor Performance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measure query execution time in real-time.&lt;/li&gt;
&lt;li&gt;Detect slow or inefficient queries.&lt;/li&gt;
&lt;li&gt;Optimize database performance with minimal overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Block Risky Commands&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Intercept and prevent accidental destructive operations like TRUNCATE or DROP.&lt;/li&gt;
&lt;li&gt;Enforce business rules and security policies at the database level.&lt;/li&gt;
&lt;li&gt;Reduce risks of data corruption or loss.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Add Auditing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track user activity, including inserts, updates, and deletes.&lt;/li&gt;
&lt;li&gt;Automatically store metadata such as timestamps, user IDs, and IP addresses.&lt;/li&gt;
&lt;li&gt;Ensure compliance with auditing and regulatory requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, database interceptors elevate you from beginner-level logging to hero-level auditing, monitoring, and security. They give developers deep insight, precise control, and safer interactions with the database — all while keeping the application code clean and maintainable.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Code Download
&lt;/h2&gt;

&lt;p&gt;The code developed during this article can be found &lt;a href="https://github.com/Madusanka98/Database-Interceptors" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>webapi</category>
      <category>efcore</category>
      <category>database</category>
    </item>
    <item>
      <title>Mastering Hangfire in .NET 9: A Complete Guide to Background Jobs</title>
      <dc:creator>Madusanka Bandara</dc:creator>
      <pubDate>Sun, 31 Aug 2025 15:11:10 +0000</pubDate>
      <link>https://dev.to/madusanka_bandara/mastering-hangfire-in-net-9-a-complete-guide-to-background-jobs-2bje</link>
      <guid>https://dev.to/madusanka_bandara/mastering-hangfire-in-net-9-a-complete-guide-to-background-jobs-2bje</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;1. Introduction&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In modern web applications, not all tasks should run during the main user request. Some processes—like sending emails, generating reports, cleaning up logs, or syncing data—can take seconds or even minutes to complete. Running these tasks inline would slow down the application and create a poor user experience. This is where background jobs come into play.&lt;/p&gt;

&lt;p&gt;A background job is any task that runs outside of the main request/response cycle. Instead of making users wait for time-consuming operations, the application delegates these tasks to run in the background. This improves performance, keeps the application responsive, and allows developers to handle recurring or long-running work reliably.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending a confirmation email after user registration.&lt;/li&gt;
&lt;li&gt;Generating invoices or reports at scheduled intervals.&lt;/li&gt;
&lt;li&gt;Processing uploaded files (e.g., resizing images, parsing CSVs).&lt;/li&gt;
&lt;li&gt;Performing database cleanups or backups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the release of .NET 9, Microsoft continues to provide strong support for background processing. Developers can use built-in approaches like IHostedService or BackgroundService for simple scenarios. However, for more advanced requirements—such as retries, job persistence, and monitoring—frameworks like Hangfire offer a much more robust solution.&lt;/p&gt;

&lt;p&gt;In this article, we’ll explore how to leverage Hangfire in .NET 9 to implement reliable, scalable, and maintainable background jobs.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. What is Hangfire?
&lt;/h2&gt;

&lt;p&gt;Hangfire is an open-source framework for managing background jobs in .NET applications. It allows developers to run fire-and-forget, delayed, recurring, or continuation jobs without writing complex infrastructure code. Hangfire handles job execution, persistence, retries, and monitoring out of the box, making it one of the most popular choices for background processing in the .NET ecosystem.&lt;/p&gt;

&lt;p&gt;At its core, Hangfire uses a persistent storage (SQL Server, PostgreSQL, Redis, etc.) to queue and track jobs. This ensures that even if your application restarts or crashes, pending jobs will still be processed reliably.&lt;/p&gt;

&lt;p&gt;Here’s why Hangfire often makes a better choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persistence&lt;/strong&gt;: Jobs survive application restarts and crashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries &amp;amp; Error Handling&lt;/strong&gt;: Built-in retry logic prevents job loss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: The Dashboard provides visibility into job execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Can run multiple servers to process jobs concurrently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility&lt;/strong&gt;: Supports different storage providers (SQL, Redis, etc.).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Setting Up Hangfire in .NET 9
&lt;/h2&gt;

&lt;p&gt;Now that we understand what Hangfire is and why it’s powerful, let’s walk through the setup in a .NET 9 Web API or MVC project. Currently I'm choosing to web api project to implement Hangfire background job.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;3.1 Installing Required NuGet Packages&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;Add the below NuGet packages to the project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hangfire.AspNetCore
Hangfire.SqlServer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hangfire.AspNetCore&lt;/strong&gt; → Integrates Hangfire with ASP.NET Core apps&lt;br&gt;
&lt;strong&gt;Hangfire.SqlServer&lt;/strong&gt; → Enables SQL Server as persistent storage for jobs&lt;/p&gt;

&lt;p&gt;If you’re using a different storage provider (e.g., PostgreSQL, Redis), install the respective package.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;3.2 Configuring Hangfire in Program.cs&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;In .NET 9, most configurations go inside &lt;code&gt;Program.cs&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Hangfire;

var builder = WebApplication.CreateBuilder(args);

// 1. Add Hangfire services and configure SQL Server storage
builder.Services.AddHangfire(config =&amp;gt;
    config.UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection")));

// 2. Add Hangfire background processing server
builder.Services.AddHangfireServer();

var app = builder.Build();

// 3. Enable Hangfire Dashboard
app.UseHangfireDashboard("/hangfire");

// Example: register a sample job
app.MapGet("/", (IBackgroundJobClient backgroundJobs) =&amp;gt;
{
    backgroundJobs.Enqueue(() =&amp;gt; Console.WriteLine("Hello from Hangfire!"));
    return "Background job has been queued.";
});

app.Run();

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;u&gt;3.3 Connecting with SQL Server for Storage&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;In your appsettings.json, define the Hangfire connection string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "ConnectionStrings": {
    "HangfireConnection": "Server=localhost;Database=HangfireDB;User Id=sa;Password=YourPassword;TrustServerCertificate=True;"
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Types of Background Jobs in Hangfire
&lt;/h2&gt;

&lt;p&gt;One of Hangfire’s biggest strengths is the variety of job types it supports. Depending on your scenario, you can run jobs immediately, after a delay, on a schedule, or chained together. Let’s go through each type with examples.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;4.1 Fire-and-Forget Jobs&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;These are executed immediately and only once. They are useful for tasks that don’t need to block the main user request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
using Hangfire;

public class EmailService
{
    public void SendWelcomeEmail(string email)
    {
        Console.WriteLine($"Welcome email sent to {email}");
    }
}

// Usage
BackgroundJob.Enqueue&amp;lt;EmailService&amp;gt;(service =&amp;gt; service.SendWelcomeEmail("user@example.com"));

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;u&gt;4.2 Delayed Jobs&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;These jobs are scheduled to run after a specified delay.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BackgroundJob.Schedule(() =&amp;gt; Console.WriteLine("This job runs after 2 minutes"), 
                       TimeSpan.FromMinutes(2));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;u&gt;4.3 Recurring Jobs (with Cron)&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;Recurring jobs run on a schedule, similar to CRON jobs in Linux.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RecurringJob.AddOrUpdate("daily-report", 
    () =&amp;gt; Console.WriteLine("Generating daily report..."), 
    Cron.Daily);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other Cron expressions provided by Hangfire:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Cron.Minutely&lt;/code&gt; → every minute&lt;br&gt;
&lt;code&gt;Cron.Hourly&lt;/code&gt; → every hour&lt;br&gt;
&lt;code&gt;Cron.Daily&lt;/code&gt; → once a day&lt;br&gt;
&lt;code&gt;Cron.Weekly&lt;/code&gt; → once a week&lt;br&gt;
&lt;code&gt;Cron.Monthly&lt;/code&gt; → once a month&lt;/p&gt;

&lt;p&gt;&lt;u&gt;4.4 Continuation Jobs&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;These jobs run only after a parent job has finished.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var parentJobId = BackgroundJob.Enqueue(() =&amp;gt; Console.WriteLine("Step 1: First job executed"));

BackgroundJob.ContinueJobWith(parentJobId, 
    () =&amp;gt; Console.WriteLine("Step 2: Continuation job executed"));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Exploring the Hangfire Dashboard
&lt;/h2&gt;

&lt;p&gt;One of Hangfire’s most powerful features is its interactive dashboard. It provides real-time visibility into background jobs, queues, and processing servers. With just a browser, you can monitor the health of y&lt;/p&gt;

&lt;p&gt;&lt;u&gt;5.1 Navigating the UI&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;Once enabled in your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.UseHangfireDashboard("/hangfire");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can open &lt;code&gt;http://localhost:5000/hangfire&lt;/code&gt; in the browser.&lt;/p&gt;

&lt;p&gt;The dashboard is divided into several sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jobs&lt;/strong&gt; → View jobs by state (Succeeded, Failed, Scheduled, Processing, Deleted).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recurring Jobs&lt;/strong&gt; → See all scheduled recurring tasks with their CRON expressions.&lt;/li&gt;
&lt;li&gt;*&lt;em&gt;Queues *&lt;/em&gt;→ Monitor job queues and check how many workers are processing them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Servers&lt;/strong&gt; → Displays active Hangfire servers running your jobs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries&lt;/strong&gt; → Track jobs that have failed and are waiting for a retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;u&gt;5.2 Monitoring Running &amp;amp; Completed Jobs&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;From the Jobs tab, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check which jobs are currently running.&lt;/li&gt;
&lt;li&gt;See detailed logs for completed jobs.&lt;/li&gt;
&lt;li&gt;Inspect job parameters and execution history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;u&gt;5.3 Retrying Failed Jobs&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;If a job fails (due to a network error, database timeout, etc.), Hangfire automatically retries it a few times based on the retry policy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can manually retry a failed job from the dashboard.&lt;/li&gt;
&lt;li&gt;Failed jobs are highlighted with detailed exception logs.&lt;/li&gt;
&lt;li&gt;This makes Hangfire extremely reliable for critical tasks like sending invoices or payments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;u&gt;5.4 Securing the Dashboard with Authentication&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;By default, the dashboard is open to anyone who knows the URL. This is fine for local development but must be secured in production.&lt;/p&gt;

&lt;p&gt;You can add authentication using DashboardOptions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Hangfire.Dashboard;

app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = new[] { new MyDashboardAuthorizationFilter() }
});

public class MyDashboardAuthorizationFilter : IDashboardAuthorizationFilter
{
    public bool Authorize(DashboardContext context)
    {
        // Example: allow only authenticated users
        var httpContext = context.GetHttpContext();
        return httpContext.User.Identity?.IsAuthenticated ?? false;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Advanced Usage Patterns
&lt;/h2&gt;

&lt;p&gt;Once you’re comfortable with basic Hangfire jobs, you can unlock its advanced features to build more scalable and maintainable systems. Let’s explore some common patterns.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;6.1 Using Dependency Injection in Hangfire Jobs&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;Hangfire integrates with ASP.NET Core’s built-in Dependency Injection (DI) container. This allows you to inject services directly into your job methods instead of writing static helpers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public interface IEmailService
{
    void SendWelcomeEmail(string email);
}

public class EmailService : IEmailService
{
    public void SendWelcomeEmail(string email)
    {
        Console.WriteLine($"Email sent to {email}");
    }
}

// Register service in Program.cs
builder.Services.AddScoped&amp;lt;IEmailService, EmailService&amp;gt;();

// Enqueue job with DI
BackgroundJob.Enqueue&amp;lt;IEmailService&amp;gt;(service =&amp;gt; service.SendWelcomeEmail("user@example.com"));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;u&gt;6.2 Creating a Custom Job Scheduler with Cron&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;For advanced scheduling, you can define custom CRON expressions.&lt;/p&gt;

&lt;p&gt;Example: Run a cleanup job every Monday at midnight.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RecurringJob.AddOrUpdate("weekly-cleanup",
    () =&amp;gt; Console.WriteLine("Performing weekly cleanup..."),
    "0 0 * * 1"); // Cron format: minute hour day month weekday
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;u&gt;6.3 Handling Concurrency &amp;amp; Queue Management&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;By default, all jobs run in the default queue. For more control, you can assign jobs to custom queues and configure workers accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Enqueue job into a custom "emails" queue
BackgroundJob.Enqueue(() =&amp;gt; Console.WriteLine("Email Job"), new EnqueuedState("emails"));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Program.cs, configure Hangfire Server to listen to multiple queues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.Services.AddHangfireServer(options =&amp;gt;
{
    options.Queues = new[] { "default", "emails", "reports" };
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;u&gt;6.4 Scaling Hangfire with Multiple Servers&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;Hangfire is designed for horizontal scaling. You can run multiple instances of your application (on different servers or containers), and they will all share the same job storage (e.g., SQL Server or Redis).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each server pulls jobs from the shared storage.&lt;/li&gt;
&lt;li&gt;Jobs are distributed across workers automatically.&lt;/li&gt;
&lt;li&gt;Failed jobs are retried by any available server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes Hangfire suitable for high-traffic, production workloads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.Services.AddHangfireServer(options =&amp;gt;
{
    options.WorkerCount = Environment.ProcessorCount * 5; // Scale with CPU cores
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Best Practices for Production
&lt;/h2&gt;

&lt;p&gt;Running Hangfire in production requires careful planning to ensure reliability, security, and performance. Below are some best practices you should follow:&lt;/p&gt;

&lt;p&gt;&lt;u&gt;7.1. Logging &amp;amp; Monitoring Background Jobs&lt;/u&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integrate structured logging (e.g., with Serilog or NLog) to track job execution.&lt;/li&gt;
&lt;li&gt;Use Hangfire’s built-in job history tracking for insights.&lt;/li&gt;
&lt;li&gt;Consider external monitoring tools like Application Insights, ELK stack, or Prometheus/Grafana for deeper visibility.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class EmailService
{
    private readonly ILogger&amp;lt;EmailService&amp;gt; _logger;

    public EmailService(ILogger&amp;lt;EmailService&amp;gt; logger)
    {
        _logger = logger;
    }

    public void SendWelcomeEmail(string userEmail)
    {
        _logger.LogInformation("Sending welcome email to {User}", userEmail);
        // email sending logic
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;u&gt;7.2 Retry Policies &amp;amp; Error Handling&lt;/u&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hangfire automatically retries failed jobs (default: 10 attempts).&lt;/li&gt;
&lt;li&gt;Configure retry policies based on job criticality.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Fail)]&lt;/code&gt; to fine-tune.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class ReportGenerator
{
    [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
    public void GenerateDailyReport()
    {
        // Report generation logic
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;u&gt;7.3. Securing Hangfire Dashboard in Production&lt;/u&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = new[] { new HangfireCustomAuthorizationFilter() }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Custom filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class HangfireCustomAuthorizationFilter : IDashboardAuthorizationFilter
{
    public bool Authorize(DashboardContext context)
    {
        var httpContext = context.GetHttpContext();
        return httpContext.User.Identity?.IsAuthenticated == true &amp;amp;&amp;amp;
               httpContext.User.IsInRole("Admin");
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7.4. Database Performance Considerations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hangfire stores jobs in SQL Server; optimize DB for high insert/update throughput.&lt;/li&gt;
&lt;li&gt;Use separate DB schema or instance if you have high job volume.&lt;/li&gt;
&lt;li&gt;Clean up old job history with Hangfire’s built-in housekeeping jobs.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.UseHangfireServer(new BackgroundJobServerOptions
{
    Queues = new[] { "default" },
    WorkerCount = Environment.ProcessorCount * 5 // Tune based on load
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  8. Conclusion
&lt;/h2&gt;

&lt;p&gt;Background job processing is a critical part of building modern, scalable applications, and Hangfire makes it both simple and powerful in the .NET 9 ecosystem. With its persistence, retry mechanisms, flexible job types, and intuitive dashboard, Hangfire provides a production-ready framework for handling tasks that don’t belong in the main request pipeline.&lt;/p&gt;

&lt;p&gt;While BackgroundService may be sufficient for lightweight or one-off background tasks, Hangfire shines when you need reliability, visibility, and scalability. By following best practices—such as securing the dashboard, tuning retry policies, and monitoring system performance—you can ensure your background jobs run smoothly in production environments.&lt;/p&gt;

&lt;p&gt;As .NET 9 continues to improve performance and developer experience, pairing it with Hangfire allows you to build robust, fault-tolerant, and scalable background processing systems that enhance both your application’s performance and your users’ experience.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>hangfire</category>
      <category>backgroundjob</category>
      <category>webapi</category>
    </item>
    <item>
      <title>Real-Time Email Notifications in .NET Using Microsoft Graph API</title>
      <dc:creator>Madusanka Bandara</dc:creator>
      <pubDate>Wed, 20 Aug 2025 08:09:56 +0000</pubDate>
      <link>https://dev.to/madusanka_bandara/real-time-email-notifications-in-net-using-microsoft-graph-api-3f71</link>
      <guid>https://dev.to/madusanka_bandara/real-time-email-notifications-in-net-using-microsoft-graph-api-3f71</guid>
      <description>&lt;p&gt;Monitoring emails in real-time is a common requirement for businesses. With Microsoft Graph API, you can automatically log email details, download attachments, and maintain daily logs. In this article, we’ll create a .NET Web API to handle email notifications.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;Microsoft Graph API provides access to Office 365 services like emails, calendars, and users.&lt;/p&gt;

&lt;p&gt;With this setup, we can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subscribe to a user’s inbox&lt;/li&gt;
&lt;li&gt;Receive webhook notifications for new emails&lt;/li&gt;
&lt;li&gt;Log email details to a file in JSON format&lt;/li&gt;
&lt;li&gt;Download attachments automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is useful for automated email monitoring and reporting.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, ensure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;.NET 6/7/8 project&lt;/li&gt;
&lt;li&gt;Visual Studio or VS Code&lt;/li&gt;
&lt;li&gt;Office 365 account&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NuGet packages:&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microsoft.Graph  
Azure.Identity
Newtonsoft.Json
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Setting Up Microsoft Graph API
&lt;/h2&gt;

&lt;p&gt;&lt;u&gt;3.1 Register an Application in Azure&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;i. Go to Azure Portal (&lt;a href="https://portal.azure.com/" rel="noopener noreferrer"&gt;https://portal.azure.com/&lt;/a&gt;) → App Registrations → New Registration&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffkzfzj07w5nz4chrq6bg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffkzfzj07w5nz4chrq6bg.png" alt=" " width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ii. Provide a name (GraphNotificationDemo), Supported account types (Accounts in any organizational directory and personal Microsoft accounts) and redirect URI (&lt;a href="https://localhost" rel="noopener noreferrer"&gt;https://localhost&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8mmettom3xjz1ztupydq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8mmettom3xjz1ztupydq.png" alt=" " width="800" height="622"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Register.&lt;/p&gt;

&lt;p&gt;iii. Note down the Application (client) ID and Directory (tenant) ID for later use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2upaw6nx3hgh3ppi5fof.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2upaw6nx3hgh3ppi5fof.png" alt=" " width="800" height="162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;iv. Add a client secret (Manage → Certificates &amp;amp; secrets → New client secret → Enter a "Description", and select the "Expires" duration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fizjh3kyejnroh6nvsw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fizjh3kyejnroh6nvsw.png" alt=" " width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click Add.&lt;/p&gt;

&lt;p&gt;v. Note down the client secret value for future reference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6j36wvjp6pc20xovhdar.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6j36wvjp6pc20xovhdar.png" alt=" " width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;vi. API Permissions (Manage → API Permissions → Add a permission  → Microsoft Graph)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0lqwmocvup3zcd01i7cy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0lqwmocvup3zcd01i7cy.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;vii. Add API permissions:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mail.Read
Mail.ReadWrite
Mail.ReadBasic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqrlp07xkkb2352qxr1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqrlp07xkkb2352qxr1o.png" alt=" " width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Ngrok
&lt;/h2&gt;

&lt;p&gt;Ngrok allows calls from the internet to be directed to your application running locally without needing to create firewall rules.&lt;/p&gt;

&lt;p&gt;Install Ngrok from &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;https://ngrok.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the command prompt, Run below command:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ngrok http 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5lg7lai62mxm0z0ei8fm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5lg7lai62mxm0z0ei8fm.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Create an ASP.NET Core web API project
&lt;/h2&gt;

&lt;p&gt;i. Open Visual Studio 2022 and create a new project. From the available templates, select ASP.NET Core Web API.&lt;/p&gt;

&lt;p&gt;ii. Give your project a specific name and choose the location where you want to save it. After that, click the Next button.&lt;/p&gt;

&lt;p&gt;iii. In the next step, select the framework as .NET 8 (LTS), since it is the latest long-term support version. Finally, click Create to generate your new Web API project.&lt;/p&gt;

&lt;p&gt;iv. Add the below NuGet packages to the project.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Azure.Identity
Microsoft.Graph
Newtonsoft.Json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;v. Create a configuration class:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class MyConfig
{
    public string AppId { get; set; }
    public string TenantId { get; set; }
    public string AppSecret { get; set; }
    public string Ngrok { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;vi. Add your credentials in appsettings.json.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"MyConfig": {
  "AppId": &amp;lt;&amp;lt;--YOUR CLIENT ID--&amp;gt;&amp;gt;,
  "AppSecret": &amp;lt;&amp;lt;--YOUR CLIENT Secret--&amp;gt;&amp;gt;,
  "TenantId": &amp;lt;&amp;lt;--YOUR Tenant Id--&amp;gt;&amp;gt;,
  "Ngrok": &amp;lt;&amp;lt;--YOUR Ngrok URL--&amp;gt;&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;vii. Update Program.cs file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using changenotifications.Models;

var config = new MyConfig();
builder.Configuration.Bind("MyConfig", config);
builder.Services.AddSingleton(config);

//app.UseHttpsRedirection(); -- comment out the following line to disable SSL redirection.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;viii. Subscribing to a User Inbox:&lt;/p&gt;

&lt;p&gt;What is a subscription?&lt;/p&gt;

&lt;p&gt;A subscription tells Graph API to send POST requests to your server whenever a new email arrives. Subscriptions expire, so we’ll set up auto-renew.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var subscription = new Subscription
{
    ChangeType = "created",
    NotificationUrl = $"{config.Ngrok}/api/notifications",
    Resource = $"/users/{userId}/mailFolders('Inbox')/messages",
    ExpirationDateTime = DateTime.UtcNow.AddMinutes(5),
    ClientState = "SecretClientState"
};

var newSubscription = await graphClient.Subscriptions.PostAsync(subscription);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;ix. Webhook Endpoint&lt;/p&gt;

&lt;p&gt;Create a [HttpPost] endpoint to receive notifications:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[HttpPost]
public async Task&amp;lt;ActionResult&amp;gt; Post([FromQuery] string validationToken)
{
    if (!string.IsNullOrEmpty(validationToken))
    return Ok(validationToken); // Graph validation
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;x. Parsing Notifications&lt;/p&gt;

&lt;p&gt;After receiving a POST, extract userId and messageId to fetch the full email:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var message = await graphClient.Users[userId].Messages[messageId].GetAsync();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;xi. Logging Email Details&lt;/p&gt;

&lt;p&gt;We log key email details such as the subject, sender, recipients (To &amp;amp; CC), received and sent dates, body preview, and attachments.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var logFile = Path.Combine(BaseDir, "logs", $"EmailLog_{DateTime.UtcNow:yyyyMMdd}.txt");
await File.AppendAllTextAsync(logFile, JsonSerializer.Serialize(logEntry, new JsonSerializerOptions { WriteIndented = true }));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;xii. Downloading Attachments&lt;/p&gt;

&lt;p&gt;If the email has attachments, save them in a folder:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (attachment is FileAttachment fileAtt)
{
    var savePath = Path.Combine(msgFolder, fileAtt.Name);
    await File.WriteAllBytesAsync(savePath, fileAtt.ContentBytes);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Folder structure:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\EmailAttachments\20250819\&amp;lt;MessageId&amp;gt;\
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;xiii. Auto-Renew Subscriptions&lt;/p&gt;

&lt;p&gt;Subscriptions expire. Use a Timer to renew:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (subscription.ExpirationDateTime &amp;lt; DateTime.UtcNow.AddMinutes(2))
RenewSubscription(subscription);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;h2&gt;
  
  
  6. Test the application
&lt;/h2&gt;

&lt;p&gt;i. Open Swagger in the browser, trigger the subscribe-inbox GET endpoint from the Notifications controller, and you will receive a response like below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpx2ovayj4bxdejg296fm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpx2ovayj4bxdejg296fm.png" alt=" " width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After subscribing, the system sends an email to the user, logs all email details, and saves any email attachments in the file path C:\EmailAttachments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6d04f9kzg2p4a93kr6e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6d04f9kzg2p4a93kr6e.png" alt=" " width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Summary
&lt;/h2&gt;

&lt;p&gt;In this article, we learned how to use Microsoft Graph API with .NET to read emails, create inbox subscriptions, handle real-time notifications, log email details into daily text files, and download attachments. We also covered how to set up auto-renewal for subscriptions to keep the system running continuously. With this setup, you can build a reliable email monitoring and processing system for business or personal applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Code Download
&lt;/h2&gt;

&lt;p&gt;The code developed during this article can be found &lt;a href="https://github.com/Madusanka98/RealTime-Email-Notifications" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>api</category>
      <category>dotnet</category>
      <category>azure</category>
      <category>microsoftgraph</category>
    </item>
  </channel>
</rss>
