Tuesday, 28 July 2026

Dual-Layer Rate Limiting Middleware for Enterprise LangGraph RAG

Leave a Comment

Large Language Models (LLMs) are already essential infrastructure in the corporate AI world of 2026, not only a curiosity. Nevertheless, LLM inference is computationally demanding and costly by nature. The "noisy neighbor" issue, budget-draining prompt injection loops, and misuse are all possible when a multi-agent Retrieval-Augmented Generation (RAG) system is exposed through an API.

 

A single client spamming your RAG endpoint has the potential to deplete your LLM token quotas, increase your cloud compute expenses, and worsen latency for all other users. An end-to-end tutorial for setting up a Dual-Layer Rate Limiting Middleware in FastAPI is given in this article. In order to enforce restrictions, we will return standard 429 Too Many Requests replies based on both Client IP (for anonymous/internal traffic) and API Key (for authorized partners). Lastly, a production-grade Multi-Agent LangGraph RAG system with permanent memory and state will be integrated with this middleware. 

Part 1: The Enterprise Use Case

Scenario: "Acme Corp" has deployed an internal/external AI Knowledge Assistant. The system uses a multi-agent LangGraph architecture to retrieve data from Confluence, Jira, and internal wikis, and synthesize answers.

The Access Tiers:

  1. Authenticated Partners (API Key): Have a strict limit (e.g., 60 requests per minute) to prevent automated script abuse.

  2. Internal Employees (Corporate IP): Have a higher limit (e.g., 120 requests per minute) as they are trusted internal users.

  3. Anonymous Web Chat (Public IP): Have the lowest limit (e.g., 20 requests per minute) to prevent bot attacks.

The Architecture:

  • FastAPI Gateway: Handles HTTP routing, authentication extraction, and rate limiting.

  • Redis: Acts as the distributed, high-performance counter for rate limits.

  • LangGraph Backend: Executes the multi-agent RAG workflow, maintaining conversation state via a Checkpointer.

Part 2: Designing the Dual-Layer Rate Limiter

To build an enterprise-grade rate limiter, we must address three challenges:

  1. Distributed State: If your FastAPI app runs on multiple replicas (Kubernetes pods), in-memory counters will fail. We must use Redis.

  2. Race Conditions: A simple GET then SET in Redis is not atomic. We will use Redis INCR with an expiration to ensure thread safety.

  3. Header Standards: When rejecting a request with a 429, we must return standard headers (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After) so client applications can handle the backoff gracefully.

Part 3: The Code Implementation

Below is the complete, end-to-end implementation.

1. Dependencies

pip install fastapi uvicorn redis langchain-openai langgraph langchain-core pydantic

2. The Redis Rate Limiter & FastAPI Middleware

import time
import logging
from typing import Optional, Tuple
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
import redis.asyncio as redis

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Initialize Redis connection (Enterprise: Use a connection pool)
redis_client = redis.from_url("redis://localhost:6379", decode_responses=True)

class DualLayerRateLimiter:
    """Handles the logic for checking and incrementing rate limits in Redis."""

    def __init__(self, redis_conn: redis.Redis):
        self.redis = redis_conn

    async def check_limit(self, identifier: str, limit: int, window_seconds: int) -> Tuple[bool, int, int]:
        """
        Checks the rate limit. Uses a sliding/fixed window approach.
        Returns: (is_allowed, remaining_requests, limit)
        """
        # Create a time-bounded key (Fixed window per minute)
        current_window = int(time.time() // window_seconds)
        redis_key = f"ratelimit:{identifier}:{current_window}"

        # Atomic increment. If key doesn't exist, it's created with value 1.
        current_count = await self.redis.incr(redis_key)

        # Set expiration only on the first request of the window to prevent race conditions
        if current_count == 1:
            await self.redis.expire(redis_key, window_seconds + 1) # +1s buffer

        is_allowed = current_count <= limit
        remaining = max(0, limit - current_count)

        return is_allowed, remaining, limit

class RateLimitMiddleware(BaseHTTPMiddleware):
    """
    FastAPI Middleware that intercepts requests, extracts IP/API Key,
    and enforces dual-layer rate limiting.
    """
    def __init__(self, app, limiter: DualLayerRateLimiter):
        super().__init__(app)
        self.limiter = limiter
        # Define tiers: (limit per minute)
        self.api_key_limit = 60
        self.internal_ip_limit = 120
        self.public_ip_limit = 20
        self.window = 60 # 60 seconds

    async def dispatch(self, request: Request, call_next):
        # Skip rate limiting for health checks and docs
        if request.url.path in ["/health", "/docs", "/openapi.json"]:
            return await call_next(request)

        # 1. Extract Client IP (Handling reverse proxies like AWS ALB / Nginx)
        forwarded_for = request.headers.get("X-Forwarded-For")
        if forwarded_for:
            client_ip = forwarded_for.split(",")[0].strip()
        else:
            client_ip = request.client.host if request.client else "unknown"

        # 2. Extract API Key
        api_key = request.headers.get("X-API-Key") or request.headers.get("Authorization", "").replace("Bearer ", "")

        # 3. Determine Identifier and Limit Tier
        if api_key and api_key != "":
            identifier = f"key:{api_key}"
            limit = self.api_key_limit
            tier = "API_KEY"
        elif client_ip.startswith("10.") or client_ip.startswith("192.168."): # Mock internal IP check
            identifier = f"ip:{client_ip}"
            limit = self.internal_ip_limit
            tier = "INTERNAL_IP"
        else:
            identifier = f"ip:{client_ip}"
            limit = self.public_ip_limit
            tier = "PUBLIC_IP"

        # 4. Check Limit
        is_allowed, remaining, max_limit = await self.limiter.check_limit(identifier, limit, self.window)

        if not is_allowed:
            logger.warning(f"Rate limit exceeded for {tier} {identifier}. Limit: {max_limit}")
            return JSONResponse(
                status_code=429,
                content={"error": "Too Many Requests", "message": "Rate limit exceeded. Please slow down."},
                headers={
                    "X-RateLimit-Limit": str(max_limit),
                    "X-RateLimit-Remaining": "0",
                    "Retry-After": str(self.window),
                    "X-RateLimit-Tier": tier
                }
            )

        # 5. Proceed and inject rate limit headers into the successful response
        response = await call_next(request)
        response.headers["X-RateLimit-Limit"] = str(max_limit)
        response.headers["X-RateLimit-Remaining"] = str(remaining)
        response.headers["X-RateLimit-Tier"] = tier
        return response

3. The Multi-Agent LangGraph RAG Backend

Now, we build the AI backend. We will use a multi-agent approach: a Router Agent directs the query to a RAG Retriever Agent, which passes context to a Synthesizer Agent. We use LangGraph's Checkpointer for memory.

from typing import TypedDict, Annotated, Sequence, Literal
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.tools import tool

# --- 1. Define the State ---
class GraphState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], "The conversation history"]
    user_id: str
    retrieved_context: str
    next_agent: str

# --- 2. Define the Tools (RAG) ---
@tool
def search_internal_knowledge_base(query: str) -> str:
    """Searches the enterprise vector database for internal policies and documentation."""
    # In production, this queries Pinecone/Milvus/Weaviate
    return "Acme Corp Policy: Employees are entitled to 25 days of PTO. Rollover is permitted up to 5 days."

# --- 3. Define the Agent Nodes ---
llm = ChatOpenAI(model="gpt-4o", temperature=0)

async def router_node(state: GraphState):
    """Determines if the query requires RAG or can be answered directly."""
    # Simplified routing logic for demonstration
    messages = state["messages"]
    last_msg = messages[-1].content.lower()

    if "policy" in last_msg or "pto" in last_msg or "hr" in last_msg:
        return {"next_agent": "rag_retriever"}
    return {"next_agent": "synthesizer"}

async def rag_retriever_node(state: GraphState):
    """Executes the RAG tool and stores context in state."""
    last_msg = state["messages"][-1].content
    # In a real agent, the LLM would decide the tool arguments
    context = search_internal_knowledge_base.invoke({"query": last_msg})
    return {"retrieved_context": context, "next_agent": "synthesizer"}

async def synthesizer_node(state: GraphState):
    """Synthesizes the final answer using the LLM and retrieved context."""
    context = state.get("retrieved_context", "No specific context retrieved.")
    messages = state["messages"]

    prompt = [
        SystemMessage(content=f"You are an enterprise AI assistant. Use the following context to answer the user:\n\nContext: {context}\n\nIf the context doesn't contain the answer, state that you don't know."),
        *messages
    ]

    response = await llm.ainvoke(prompt)
    return {"messages": [response], "next_agent": "end"}

# --- 4. Build and Compile the Graph ---
def route_next(state: GraphState) -> Literal["rag_retriever", "synthesizer", "end"]:
    return state["next_agent"]

workflow = StateGraph(GraphState)
workflow.add_node("router", router_node)
workflow.add_node("rag_retriever", rag_retriever_node)
workflow.add_node("synthesizer", synthesizer_node)

workflow.set_entry_point("router")
workflow.add_conditional_edges("router", route_next)
workflow.add_edge("rag_retriever", "synthesizer")
workflow.add_edge("synthesizer", END)

# Initialize Memory (Enterprise: Use AsyncPostgresSaver)
memory = MemorySaver()
rag_graph = workflow.compile(checkpointer=memory)

4. Tying it Together: The FastAPI App

from pydantic import BaseModel

app = FastAPI(title="Enterprise RAG Gateway")

# Initialize Limiter and Middleware
limiter = DualLayerRateLimiter(redis_client)
app.add_middleware(RateLimitMiddleware, limiter=limiter)

class ChatRequest(BaseModel):
    message: str
    user_id: str
    thread_id: str # Used for LangGraph memory checkpointing

@app.post("/v1/chat")
async def chat_endpoint(payload: ChatRequest, request: Request):
    """
    The main endpoint. Protected by the RateLimitMiddleware.
    Invokes the LangGraph asynchronously.
    """
    # Extract user identity for LangGraph state
    user_id = payload.user_id
    thread_id = payload.thread_id

    # Prepare LangGraph config for memory persistence
    config = {"configurable": {"thread_id": thread_id}}

    # Initial state input
    inputs = {
        "messages": [HumanMessage(content=payload.message)],
        "user_id": user_id,
        "retrieved_context": "",
        "next_agent": ""
    }

    try:
        # Invoke the multi-agent graph asynchronously
        final_state = await rag_graph.ainvoke(inputs, config)

        # Extract the final AI response
        ai_message = final_state["messages"][-1].content

        return {
            "response": ai_message,
            "thread_id": thread_id,
            "metadata": {
                "agents_used": ["router", "rag_retriever", "synthesizer"],
                "user_id": user_id
            }
        }
    except Exception as e:
        logger.error(f"LangGraph execution failed: {str(e)}")
        raise HTTPException(status_code=500, detail="Internal AI processing error")

@app.get("/health")
async def health_check():
    return {"status": "healthy", "timestamp": time.time()}

Part 4: Testing the Implementation

To test this, run the FastAPI app (uvicorn main:app --reload) and use curl to simulate traffic.

Test 1: Normal Request (API Key)

curl -X POST "http://localhost:8000/v1/chat" \
-H "X-API-Key: partner_key_123" \
-H "Content-Type: application/json" \
-d '{"message": "What is the PTO policy?", "user_id": "user_1", "thread_id": "thread_99"}' \
-i

Expected Response: 200 OK with headers X-RateLimit-Limit: 60 and X-RateLimit-Remaining: 59. The AI will correctly retrieve and synthesize the PTO policy using the RAG agent.

Test 2: Triggering the 429 Rate Limit

Run a quick loop to exhaust the public IP limit (20 requests/minute).

for i in {1..25}; do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST "http://localhost:8000/v1/chat" \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello", "user_id": "user_2", "thread_id": "thread_100"}'
done

Expected Output: The first 20 requests will return 200. The remaining 5 will return 429.

If you inspect the headers of the 429 response:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 0
Retry-After: 60
X-RateLimit-Tier: PUBLIC_IP
Content-Type: application/json

{"error": "Too Many Requests", "message": "Rate limit exceeded. Please slow down."}

Test 3: Verifying Memory and State

Send a follow-up message using the same thread_id to prove the LangGraph Checkpointer is maintaining state across requests.

curl -X POST "http://localhost:8000/v1/chat" \
-H "X-API-Key: partner_key_123" \
-H "Content-Type: application/json" \
-d '{"message": "Can I rollover more than 5 days?", "user_id": "user_1", "thread_id": "thread_99"}'

Expected Response: The AI will remember the previous context about the 5-day rollover limit without needing the RAG tool to fetch it again, demonstrating successful state persistence.

Part 5: Enterprise Best Practices & Next Steps

Implementing this architecture gives you a robust, production-ready AI gateway. However, to fully harden this for a Fortune 500 deployment, consider the following enhancements:

  1. Atomic Lua Scripts in Redis: While INCR and EXPIRE work well, a strict enterprise environment should use a Redis Lua script to perform the check-and-set atomically, preventing edge-case race conditions during high-concurrency spikes.

  2. Token-Based Rate Limiting: Instead of limiting by requests, limit by LLM tokens. A request that generates 10,000 tokens should cost more against the rate limit than a request generating 100 tokens. You can intercept the LangGraph output, calculate token usage, and decrement a Redis token bucket.

  3. Distributed Checkpointing: In the code above, we used MemorySaver for brevity. In production, you must swap this for AsyncPostgresSaver or AsyncRedisSaver so that conversation memory survives FastAPI pod restarts and scales horizontally.

  4. Circuit Breakers: Combine this rate limiter with a circuit breaker (like pybreaker). If the underlying vector database or LLM provider goes down, the circuit breaker should immediately return a 503 without wasting time executing the LangGraph nodes.

By combining FastAPI's dual-layer rate limiting with LangGraph's stateful multi-agent execution, you ensure that your enterprise AI is not only intelligent and context-aware but also financially predictable and highly resilient to abuse.

ASP.NET Core 10.0 Hosting Recommendation

One of the most important things when choosing a good ASP.NET Core 9.0 hosting is the feature and reliability. HostForLIFE is the leading provider of Windows hosting and affordable ASP.NET Core, their servers are optimized for PHP web applications. The performance and the uptime of the hosting service are excellent and the features of the web hosting plan are even greater than what many hosting providers ask you to pay for. 

At HostForLIFE.eu, customers can also experience fast ASP.NET Core hosting. The company invested a lot of money to ensure the best and fastest performance of the datacenters, servers, network and other facilities. Its datacenters are equipped with the top equipments like cooling system, fire detection, high speed Internet connection, and so on. That is why HostForLIFEASP.NET guarantees 99.9% uptime for ASP.NET Core. And the engineers do regular maintenance and monitoring works to assure its Orchard hosting are security and always up.

 

 

Read More...

Wednesday, 22 July 2026

Using ASP.NET Core's BackgroundService for Prolonged Tasks

Leave a Comment

An application shouldn't execute all of its tasks as part of an HTTP request. While certain processes must run continually in the background, others take a long time to finish. Email processing, report generation, data synchronization, temporary file cleanup, and message consumption from a queue are a few examples.


Your application may become slower and provide a worse user experience if these actions are executed directly within a controller or API endpoint. To address this issue, ASP.NET Core offers the BackgroundService class.

With BackgroundService, you can maintain the responsiveness and scalability of your application while doing lengthy activities independently of incoming HTTP requests.

This post will teach you how to utilize BackgroundService, when to use it, and how to create dependable background processes in ASP.NET Core.

What Is BackgroundService?

BackgroundService is an abstract class provided by ASP.NET Core for implementing hosted services that run in the background.

Unlike a controller, a background service starts when the application starts and continues running until the application stops.

Some common use cases include:

  • Sending emails

  • Processing message queues

  • Importing data from external systems

  • Scheduled cleanup tasks

  • Generating reports

  • Monitoring application health

  • Processing uploaded files

Since these operations run independently, they don't block incoming user requests.

Creating a Background Service

Creating a background service is simple. Create a class that inherits from BackgroundService.

using Microsoft.Extensions.Hosting;

public class WorkerService : BackgroundService
{
    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            Console.WriteLine("Background task is running...");

            await Task.Delay(5000, stoppingToken);
        }
    }
}

The ExecuteAsync method contains the logic that runs continuously until the application shuts down.

Registering the Background Service

After creating the service, register it with the dependency injection container.

builder.Services.AddHostedService<WorkerService>();

When the application starts, ASP.NET Core automatically starts the background service.

No additional configuration is required.

Understanding the Cancellation Token

A background service should always respond gracefully when the application is shutting down.

ASP.NET Core provides a CancellationToken that signals when the service should stop.

Example:

while (!stoppingToken.IsCancellationRequested)
{
    await ProcessDataAsync();

    await Task.Delay(10000, stoppingToken);
}

Checking the cancellation token ensures that the application can shut down cleanly without leaving unfinished operations.

Processing Queue Messages

One of the most common uses of BackgroundService is processing messages from a queue.

A typical workflow looks like this:

  1. A user submits a request.

  2. The application places a message in a queue.

  3. The API immediately returns a response.

  4. The background service reads the message.

  5. The task is processed asynchronously.

This approach improves responsiveness because users don't have to wait for lengthy operations to complete.

Using Dependency Injection

Background services can use other application services through dependency injection.

For example:

public class WorkerService : BackgroundService
{
    private readonly ILogger<WorkerService> _logger;

    public WorkerService(ILogger<WorkerService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker is running.");

            await Task.Delay(5000, stoppingToken);
        }
    }
}

This makes it easy to use logging, database services, or other application components inside the background service.

Handle Exceptions Properly

A background service should never stop unexpectedly because of an unhandled exception.

Instead, catch exceptions, log them, and continue processing when appropriate.

try
{
    await ProcessOrdersAsync();
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

Proper exception handling improves application reliability and simplifies troubleshooting.

Avoid Blocking Operations

Background services should use asynchronous methods whenever possible.

Instead of:

Thread.Sleep(5000);

Use:

await Task.Delay(5000, stoppingToken);

Asynchronous operations free application threads to handle other work, improving scalability.

Real-World Example

Imagine an online shopping application.

When a customer places an order:

  1. The API saves the order.

  2. A message is added to a queue.

  3. The API immediately returns a success response.

  4. A background service processes the order.

  5. The service sends a confirmation email.

  6. Inventory is updated.

  7. A shipping request is created.

The customer receives an immediate response while the remaining tasks continue in the background.

Best Practices

When using BackgroundService in ASP.NET Core, follow these recommendations:

  • Keep background tasks independent of HTTP requests.

  • Always use the provided CancellationToken.

  • Prefer asynchronous operations over blocking calls.

  • Handle exceptions to prevent unexpected service termination.

  • Use dependency injection instead of creating services manually.

  • Log important events and errors for easier monitoring.

  • Avoid performing CPU-intensive work on the main application thread.

  • Monitor background service performance and resource usage in production.

Conclusion

BackgroundService provides a clean and reliable way to run long-running tasks in ASP.NET Core applications. By moving work such as email processing, queue consumption, report generation, and scheduled maintenance outside the request pipeline, you can improve application responsiveness and deliver a better user experience.

When combined with dependency injection, asynchronous programming, and proper error handling, BackgroundService becomes a powerful tool for building scalable and maintainable applications. By following the best practices outlined in this article, you can create background processes that run efficiently and reliably in production environments.

ASP.NET Core 10.0 Hosting Recommendation

One of the most important things when choosing a good ASP.NET Core 10.0 hosting is the feature and reliability. HostForLIFE is the leading provider of Windows hosting and affordable ASP.NET Core 10.0, their servers are optimized for PHP web applications. The performance and the uptime of the hosting service are excellent and the features of the web hosting plan are even greater than what many hosting providers ask you to pay for. 

At HostForLIFE.eu, customers can also experience fast ASP.NET Core hosting. The company invested a lot of money to ensure the best and fastest performance of the datacenters, servers, network and other facilities. Its datacenters are equipped with the top equipments like cooling system, fire detection, high speed Internet connection, and so on. That is why HostForLIFE guarantees 99.9% uptime for ASP.NET Core. And the engineers do regular maintenance and monitoring works to assure its Orchard hosting are security and always up.

Read More...

Tuesday, 14 July 2026

Creating a Low-Allocation, Streaming Excel Writer in .NET

Leave a Comment

You've undoubtedly faced the same issue if you've ever dealt with Excel creation in.NET: creating a basic.xlsx file requires allocating tens or even hundreds of gigabytes of RAM. Well-known libraries like EPPlus or ClosedXML typically allot 30–100 MB for a 10,000-row dataset with four string columns. Only when a web API is offering file downloads under concurrent load does this issue get worse.


The main methods I employed to create a zero-dependency Excel writer that uses less than 1 MB for the same 10,000-row workload and finishes 8–10× quicker are described in this post. IBufferWriter integration, streaming techniques, OOXML internals, and real-world benchmark comparisons will all be covered.

Why OOXML?

An .xlsx file is just a ZIP archive containing XML files. The structure is well-defined:

  • [Content_Types].xml – Content type declarations

  • _rels/.rels – Package-level relationships

  • xl/workbook.xml – Workbook definition (sheet names)

  • xl/_rels/workbook.xml.rels – Workbook relationships

  • xl/styles.xml – Cell styles

  • xl/sharedStrings.xml – Shared string table (optional)

  • xl/worksheets/sheet1.xml – Sheet data

  • xl/theme/theme1.xml – Theme definition (optional)

This means we don't need any third-party library to generate a valid .xlsx file. We just need to:

  1. Write XML parts to a ZIP stream

  2. Handle the OPC (Open Packaging Convention) relationship structure

  3. Manage cell data, styles, and shared strings efficiently

Technique 1: Direct-to-Stream Writing

Most Excel libraries build an in-memory DOM first, then serialize it. ClosedXML, for example, builds an entire XLWorkbook object graph with IXLRowIXLCell, styles, formulas, etc. — all in memory — before writing.

A streaming approach flips this: write XML directly to the output stream as data arrives. No intermediate object model.

Here's the core idea in simplified form:

// Traditional DOM approach: everything lives in memory
var workbook = new XLWorkbook();
var sheet = workbook.Worksheets.Add("Sheet1");
sheet.Cell(1, 1).Value = "Header";
// ... add 10,000 rows
workbook.SaveAs("output.xlsx");  // only now does serialization happen

// Streaming approach: write as we go
using var zip = new ZipArchive(stream, mode);
var sheetEntry = zip.CreateEntry("xl/worksheets/sheet1.xml");
using var sw = new StreamWriter(sheetEntry.Open());
sw.Write("<?xml version=\"1.0\"?>");
sw.Write("<worksheet xmlns=\"...\"><sheetData>");
foreach (var row in data)
{
    // Write row XML directly — no intermediate objects
    sw.Write($"<row><c t=\"inlineStr\"><is><t>{Escape(row.Name)}</t></is></c></row>");
}
sw.Write("</sheetData></worksheet>");

This pattern eliminates the entire in-memory object graph. The tradeoff is that you lose random access (no modifying cell B4 after writing row 100), but for export scenarios, you never need it anyway.

Technique 2: IBufferWriter<byte> for Low-Allocation Paths

.NET has a lesser-known gem: System.Buffers.IBufferWriter<byte>. It lets you write bytes directly into rented pooled buffers, avoiding intermediate byte[] allocations and copies.

Here's how it works in practice:

// Traditional: allocates a byte[]
var ms = new MemoryStream();
Xlsx.Write(ms, data);
byte[] result = ms.ToArray();  // another allocation + copy

// IBufferWriter: writes to pooled buffers
var writer = new ArrayBufferWriter<byte>();
Xlsx.Write(writer, data);
// writer.WrittenSpan is a ReadOnlySpan<byte> over rented memory
// No intermediate allocations

The IBufferWriter<byte> path is the primary low-allocation hot path. It chains through ZipArchive → DeflateStream → XML writer, all using IBufferWriter<byte> under the hood, eliminating allocations at every layer.

This is also the technique that System.Text.Json uses under the hood for its high-performance serialization — the same pattern applies to XML generation.

Technique 3: Inline Strings vs. Shared Strings Table (SST)

OOXML supports two ways to store string values:

Inline strings (t="inlineStr"): The string value lives directly inside the cell element. No shared strings file needed. This is optimal when strings are mostly unique.

Shared Strings Table (t="s" with index): Strings are stored once in xl/sharedStrings.xml, and cells reference them by index. This is optimal when strings are highly repeated (e.g., status values, categories).

Most libraries default to SST (or build it unconditionally), but SST is wasteful when strings are unique — you double the memory: once for the SST, once for the cell references.

My approach uses a heuristic: pre-scan the first 64 rows. If string deduplication ratio is below 70%, stay with inline strings. Only switch to SST when it actually saves space. This is similar to what Excel itself does when saving files.

// Auto-detect based on data characteristics
Xlsx.ToBytes(data, p => p.WithAutoSst(true));

// Benchmarks on 10k rows with 16 unique strings (96% dedup):
// SST path:  2,804 μs, 955 KB  — dedup saves XML space
// Inline:    2,933 μs, 979 KB  — inline avoids SST bookkeeping
// For unique strings, inline is both faster and smaller

Technique 4: Fluent Export Profiles

Rather than a verbose configuration object, a fluent API keeps the code readable and the allocation minimal:

// Fluent profile — no intermediate configuration objects allocated
var bytes = Xlsx.ToBytes(orders, p => p
    .Sheet("Orders")
    .Column(x => x.OrderNo, c => c.WithName("Order #").WithWidth(30))
    .Column(x => x.Amount,  c => c.WithFormat("0.00").WithFontColor("#0000FF"))
    .Ignore(x => x.InternalId)
    .WithFreezeHeader());

// Traditional (ClosedXML) — configuration mixed with data manipulation
using var wb = new XLWorkbook();
var ws = wb.Worksheets.Add("Orders");
ws.Cell(1, 1).Value = "Order #";  // mutating a DOM object
ws.Cell(1, 1).Style.Font.Bold = true;
// ...

The Action<ExportProfile<T>> callback is compiled into a delegate once (via Expression.Compile or source-gen fast paths with [XlsxExportable]), and the configuration is applied during streaming without allocating intermediate objects.

Technique 5: IAsyncEnumerable<T> for True Streaming

For web API scenarios where data comes from an async source (database, gRPC, event stream), IAsyncEnumerable<T> enables true end-to-end streaming:

// Database → stream → HTTP response — no materialized list
app.MapGet("/export/orders", async (HttpContext ctx, AppDbContext db) =>
{
    ctx.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    await Xlsx.WriteAsync(
        ctx.Response.Body,
        ReadOrdersAsync(db),  // IAsyncEnumerable<Order>
        p => p.Sheet("Orders"));
});

async IAsyncEnumerable<Order> ReadOrdersAsync(AppDbContext db)
{
    await foreach (var order in db.Orders.AsAsyncEnumerable()
        .Where(o => o.Status == OrderStatus.Active))
        yield return order;
}

This means the HTTP response body stream is written concurrently with the database query. At no point is the full dataset materialized in memory. Contrast this with:

// Traditional: load all data, then write
var orders = await db.Orders.Where(...).ToListAsync();  // materialized in memory
ClosedXML...SaveAs(stream);                              // writes from memory

The API is symmetric — reading also uses IAsyncEnumerable:

await foreach (var order in Xlsx.ReadAsync<Order>(stream))
{
    await db.Orders.AddAsync(order);
}

Note: IAsyncEnumerable reading streams rows as they're parsed from the ZIP/XWL, but the actual XML cell parsing is synchronous CPU work. The async benefit is primarily on the I/O side.

Real-World Benchmarks

All benchmarks run on an Apple M4 with .NET 10, comparing against MiniExcel, ClosedXML, EPPlus, and raw OpenXml SDK. The benchmark code is available in the companion repository.

10,000 rows × 4 string columns

LibraryMeanAllocatedRatio
Magicodes.IE.IO2.93 ms979 KB
MiniExcel11.91 ms36,340 KB37×
EPPlus40.22 ms29,560 KB30×
ClosedXML50.44 ms84,396 KB86×
OpenXml SDK48.96 ms28,596 KB29×

10,000 rows × mixed types (string + number + datetime + bool)

LibraryMeanAllocatedRatio
Magicodes.IE.IO4.99 ms1,158 KB
MiniExcel12.97 ms37,906 KB33×
EPPlus87.13 ms38,400 KB33×
ClosedXML68.26 ms102,309 KB88×

Key takeaways

  • Speed: 4× faster than MiniExcel, 8–10× faster than EPPlus/ClosedXML

  • Memory: 30–100× less allocation than alternatives

  • Gen2 GC: Minimal or zero Gen2 collections vs. competitors that trigger Gen2 on every iteration

The 37–88× allocation difference isn't a small optimization — it's the difference between a server that can handle thousands of concurrent exports and one that falls over under GC pressure.

When to Use Each Approach

ScenarioRecommendation
File export, known small dataAny library works; choose based on ease of use
Web API file download (HTTP stream)Streaming approach — avoid materializing byte[]
Database → Excel (large datasets)IAsyncEnumerable + WriteAsync — no full materialization
High-concurrency server exportLow-allocation streaming — minimize GC pauses
Complex formatting/charts/pivot tablesClosedXML or EPPlus — they have richer formatting APIs
Read-only template generationStreaming is ideal — you never need to mutate cells post-write

Limitations

No single approach fits all use cases. The streaming writer has known tradeoffs:

  • No random access: Once a row is written, you can't go back and modify it. This is fine for export, not for interactive editing.

  • Reader doesn't evaluate formulasRead<T> reads cached values, not formula results.

  • No ZIP64: The underlying ZIP writer doesn't support ZIP64 yet — files must fit within ZIP32 limits (4 GB raw, effectively ~2 GB compressed for large workbooks).

  • Formatting is output-only: You specify styles declaratively; there's no DOM to programmatically modify after creation.

Conclusion

The streaming, low-allocation approach to OOXML generation is not inherently complex — it's just applying well-established .NET patterns (Span<T>IBufferWriter<T>IAsyncEnumerable<T>) to a problem that's traditionally been solved with DOM-style libraries. The result is an 8–10× speed improvement and 30–100× less memory allocation, without sacrificing the type safety and fluent API that .NET developers expect.

The techniques described here — direct-to-stream XML writing, IBufferWriter<byte> pooling, SST heuristics, and IAsyncEnumerable integration — are transferable to any XML-based format and serve as a general template for high-performance .NET I/O.

Best ASP.NET Core 10.0 Hosting Recommendation

One of the most important things when choosing a good ASP.NET Core 10.0 hosting is the feature and reliability. HostForLIFE is the leading provider of Windows hosting and affordable ASP.NET Core 10.0, their servers are optimized for PHP web applications. The performance and the uptime of the hosting service are excellent and the features of the web hosting plan are even greater than what many hosting providers ask you to pay for. 

At HostForLIFE, customers can also experience fast ASP.NET Core 10.0 hosting. The company invested a lot of money to ensure the best and fastest performance of the datacenters, servers, network and other facilities. Its datacenters are equipped with the top equipments like cooling system, fire detection, high speed Internet connection, and so on. That is why HostForLIFE guarantees 99.9% uptime for ASP.NET Core 10.0. And the engineers do regular maintenance and monitoring works to assure its Orchard hosting are security and always up.

Read More...

Tuesday, 7 July 2026

A Practical Comparison between Event Sourcing and CRUD Applications

Leave a Comment

Event Sourcing vs. CRUD Applications: A Practical Comparison Most commercial applications are based on the CRUD (Create, Read, Update, Delete) architecture. Whether you're creating an e-commerce platform, inventory management system, banking application, or customer portal, CRUD operations make it simple to handle data.

However, as systems expand in complexity, companies frequently want more than simply the present status of their data. They may require a comprehensive audit trail, historical reconstruction, event-driven workflows, or assistance with complicated business processes comparison.

This is where Event Sourcing becomes an alternative architectural approach.

Instead of storing only the current state of data, Event Sourcing stores every change as a sequence of events. The current state is then derived by replaying those events.

Both approaches are valuable, but they solve different problems. Understanding their strengths, limitations, and trade-offs is essential when designing modern applications.

In this article, we'll compare Event Sourcing and CRUD architectures, examine real-world scenarios, and discuss when each approach is the better choice.

Understanding CRUD Applications

CRUD is the most common application architecture.

The four operations are:

  • Create

  • Read

  • Update

  • Delete

Example:

Customer Record
       ↓
Create
       ↓
Update
       ↓
Delete

A database stores only the latest version of the data.

Consider a customer record.

Initial state:

{
  "id": 101,
  "name": "John Smith",
  "status": "Active"
}
JSON

After an update:

{
  "id": 101,
  "name": "John Smith",
  "status": "Premium"
}
JSON

The previous state is typically lost unless separate auditing mechanisms are implemented.

Understanding Event Sourcing

Event Sourcing stores every state change as an immutable event.

Instead of storing the current state:

Customer Created
Customer Activated
Customer Upgraded

The application reconstructs the current state by replaying all events.

Example:

Event 1:
CustomerCreated

Event 2:
CustomerActivated

Event 3:
CustomerUpgraded

Current state:

Replay Events
      ↓
Premium Customer

The entire history remains available permanently.

Core Architectural Difference

The primary difference lies in what gets stored.

CRUD

Stores current state.

Example:

Database
      ↓
Current Customer Record

Event Sourcing

Stores business events.

Example:

Event Store
      ↓
CustomerCreated
CustomerActivated
CustomerUpgraded

Current state becomes a derived representation rather than the primary source of truth.

Real-World Example: Bank Account

Consider a banking application.

CRUD Approach

Current record:

{
  "accountId": 1001,
  "balance": 1200
}
JSON

When money is deposited:

Balance = 1200

After deposit:

Balance = 1500

Only the latest balance exists.

Event Sourcing Approach

Events:

AccountOpened
DepositMade(1000)
DepositMade(500)
WithdrawalMade(300)

Current balance:

Replay Events
      ↓
1200

The complete transaction history is preserved automatically.

Data Storage Comparison

CRUD Storage

Customer Table
Order Table
Product Table

Data is updated directly.

Event Sourcing Storage

Event Store
      ↓
Business Events

Events are never modified or deleted.

This creates an immutable record of business activity.

Auditability

Audit requirements often influence architectural decisions.

CRUD

Requires additional auditing mechanisms.

Example:

Application
      ↓
Database
      ↓
Audit Table

Developers must explicitly capture changes.

Event Sourcing

Auditing is built into the architecture.

Example:

Every Event
      ↓
Permanent History

The audit trail exists automatically.

This is one of Event Sourcing's strongest advantages.

Handling Business History

Many systems need historical insights.

Questions such as:

  • Who changed this record?

  • When was it changed?

  • What was the previous value?

CRUD

These answers may require:

  • Audit tables

  • Change tracking

  • Log analysis

Event Sourcing

The answers already exist within the event stream.

Example:

Event Timeline
      ↓
Complete Business History

Historical analysis becomes significantly easier.

Integration with Event-Driven Systems

Modern architectures frequently use asynchronous communication.

Example:

Order Created
      ↓
Inventory Service

Order Created
      ↓
Shipping Service

Order Created
      ↓
Billing Service

CRUD

Additional mechanisms are often required to publish events.

Event Sourcing

Events already exist as part of the persistence model.

This makes integration with event-driven systems more natural.

Performance Considerations

Performance characteristics differ significantly.

CRUD Performance

Read operations are typically straightforward.

Example:

SELECT *
FROM Customers
WHERE Id = 101;

Current state is immediately available.

Event Sourcing Performance

State reconstruction may require replaying events.

Example:

1000 Events
      ↓
Rebuild State

To improve performance, systems often use snapshots.

Example:

Snapshot
      ↓
Recent Events
      ↓
Current State

This reduces replay overhead.

Complexity Comparison

CRUD

Advantages:

  • Simple implementation

  • Familiar design

  • Broad tooling support

  • Easy onboarding

Architecture:

Application
      ↓
Database

Event Sourcing

Advantages:

  • Rich business history

  • Built-in auditing

  • Better event integration

Architecture:

Application
      ↓
Event Store
      ↓
Projections
      ↓
Read Models

Event Sourcing introduces additional complexity.

Common CRUD Use Cases

CRUD is often ideal for:

  • Content management systems

  • Inventory applications

  • Internal business tools

  • Administrative portals

  • Standard web applications

Example:

Application
      ↓
Relational Database

The simplicity of CRUD is often sufficient.

Common Event Sourcing Use Cases

Event Sourcing is valuable when:

  • Auditing is critical

  • Business history matters

  • Event-driven architectures are required

  • Regulatory compliance is important

  • Complex workflows exist

Examples:

  • Banking systems

  • Trading platforms

  • Insurance systems

  • Financial applications

  • Logistics platforms

These domains often benefit from immutable event histories.

Can They Be Combined?

Yes.

Many modern systems use a hybrid approach.

Architecture:

CRUD Components
      ↓
Simple Workflows

Event Sourcing Components
      ↓
Critical Business Processes

Organizations often apply Event Sourcing only where its benefits justify the added complexity.

This approach balances maintainability and functionality.

Benefits of CRUD

Simplicity

Easy to understand and implement.

Mature Ecosystem

Supported by virtually every database platform.

Fast Development

Suitable for most business applications.

Efficient Reads

Current state is immediately available.

Benefits of Event Sourcing

Complete Audit Trail

Every change is permanently recorded.

Historical Reconstruction

Past states can be recreated.

Event-Driven Integration

Events naturally drive workflows.

Strong Domain Modeling

Business actions become first-class concepts.

These benefits make Event Sourcing attractive for complex domains.

Best Practices

When choosing between CRUD and Event Sourcing, consider the following recommendations.

Start with Business Requirements

Do not adopt Event Sourcing simply because it is popular.

Use CRUD for Simpler Systems

Most applications do not require full event histories.

Use Event Sourcing for High-Audit Domains

Regulated industries often benefit significantly.

Consider Operational Complexity

Event Sourcing requires additional infrastructure and expertise.

Evaluate Long-Term Needs

Future reporting, analytics, and compliance requirements may influence the decision.

Architectural choices should support business goals rather than technology trends.

Conclusion
CRUD and Event Sourcing are fundamentally distinct techniques to handling application data. CRUD focuses on storing and updating current information, making it simple, efficient, and appropriate for the vast majority of business applications. Its simple design and robust ecosystem make it the go-to solution for many development teams.

Event Sourcing, on the other hand, views business events as the source of truth. By preserving every modification as an immutable event, it gives a comprehensive history of system activity, makes auditing easier, and interacts well with event-driven systems. However, these advantages are accompanied with greater complexity and operational costs.

Best ASP.NET Core 10.0 Hosting Recommendation

One of the most important things when choosing a good ASP.NET Core 10.0 hosting is the feature and reliability. HostForLIFE is the leading provider of Windows hosting and affordable ASP.NET Core, their servers are optimized for PHP web applications. The performance and the uptime of the hosting service are excellent and the features of the web hosting plan are even greater than what many hosting providers ask you to pay for. 

At HostForLIFEASP.NET, customers can also experience fast ASP.NET Core hosting. The company invested a lot of money to ensure the best and fastest performance of the datacenters, servers, network and other facilities. Its datacenters are equipped with the top equipments like cooling system, fire detection, high speed Internet connection, and so on. That is why HostForLIFEASP.NET guarantees 99.9% uptime for ASP.NET Core. And the engineers do regular maintenance and monitoring works to assure its Orchard hosting are security and always up.
Read More...

Tuesday, 30 June 2026

Using.NET to Create AI-Powered Technical Debt Prioritization Systems

Leave a Comment

Every software project accumulates technical debt over time. Quick fixes, rushed releases, outdated dependencies, duplicated code, missing tests, architectural shortcuts, and legacy implementations are often necessary to meet business deadlines. While these decisions may provide short-term benefits, they can create long-term maintenance challenges.

 

The real problem isn't identifying technical debt. Modern tools like SonarQube, GitHub Advanced Security, and static analyzers can easily generate thousands of findings. The challenge is determining which technical debt items should be addressed first.

Engineering teams frequently ask:

  • Which code smells create the highest business risk?

  • What technical debt impacts system performance?

  • Which issues increase security vulnerabilities?

  • What refactoring work should be prioritized next sprint?

  • Which legacy components create the greatest operational burden?

Artificial Intelligence can help answer these questions by analyzing code quality metrics, incident history, repository activity, service dependencies, and business impact data to prioritize technical debt intelligently.

In this article, we'll build an AI-powered technical debt prioritization platform using ASP.NET Core, Azure OpenAI, GitHub APIs, and code quality analysis data.

Understanding Technical Debt

Technical debt refers to the future cost of maintaining or improving software due to earlier design or implementation decisions.

Common examples include:

  • Duplicated code

  • Legacy frameworks

  • Hardcoded configurations

  • Large classes

  • Tight coupling

  • Missing unit tests

  • Outdated dependencies

  • Poor documentation

Consider the following example:

public class UserManager
{
    // 2500 lines of code
}

A massive class may function correctly today but significantly increase maintenance costs in the future.

The Problem with Traditional Prioritization

Most organizations prioritize technical debt based on:

  • Developer intuition

  • Static analysis scores

  • Team discussions

  • Available sprint capacity

This approach often leads to inconsistent decisions.

For example:

A code smell affecting a low-traffic internal tool may receive the same priority as a security vulnerability in a customer-facing payment system.

Without context, prioritization becomes difficult.

Why AI Improves Technical Debt Management

AI can evaluate technical debt from multiple perspectives simultaneously.

Examples include:

  • Code complexity

  • Production incidents

  • Service criticality

  • Security risks

  • Change frequency

  • Developer activity

  • Business impact

Instead of simply reporting issues, AI can determine which ones matter most.

Solution Architecture

A technical debt prioritization platform typically consists of four layers.

Analysis Layer

Collect information from:

  • GitHub

  • SonarQube

  • Azure DevOps

  • Static Analysis Tools

  • Dependency Scanners

Processing Layer

ASP.NET Core services aggregate technical debt metrics.

AI Prioritization Layer

Azure OpenAI evaluates risk and business impact.

Reporting Layer

Recommendations are displayed through dashboards and engineering portals.

Creating the ASP.NET Core Project

Create a new project.

dotnet new webapi -n TechnicalDebtAdvisor

Install required packages.

dotnet add package Azure.AI.OpenAI
dotnet add package Octokit

These packages provide repository integration and AI capabilities.

Modeling Technical Debt Items

Create a model representing debt findings.

public class TechnicalDebtItem
{
    public string Title { get; set; }

    public string Category { get; set; }

    public string Severity { get; set; }

    public int ComplexityScore { get; set; }

    public string ServiceName { get; set; }
}

Each finding becomes an input for prioritization analysis.

Collecting Repository Metrics

GitHub repositories provide valuable prioritization signals.

Examples include:

  • Commit frequency

  • Pull request activity

  • Contributor count

  • Deployment frequency

Create a repository model.

public class RepositoryMetrics
{
    public int MonthlyCommits { get; set; }

    public int PullRequests { get; set; }

    public int Contributors { get; set; }
}

Frequently modified components often deserve higher priority.

Incorporating Operational Data

Technical debt should not be evaluated in isolation.

Operational data provides important context.

Example:

public class IncidentMetrics
{
    public int IncidentCount { get; set; }

    public int OutageMinutes { get; set; }

    public int SupportTickets { get; set; }
}

If a component frequently contributes to incidents, related technical debt should receive greater attention.

Measuring Code Complexity

Complex code often creates maintenance challenges.

Example metrics include:

  • Cyclomatic complexity

  • Class size

  • Method count

  • Dependency count

Model:

public class ComplexityMetrics
{
    public int CyclomaticComplexity { get; set; }

    public int MethodCount { get; set; }

    public int DependencyCount { get; set; }
}

Higher complexity often correlates with higher maintenance costs.

Building the AI Prioritization Engine

Create a service that evaluates technical debt.

public class TechnicalDebtAIService
{
    private readonly OpenAIClient _client;

    public TechnicalDebtAIService(
        OpenAIClient client)
    {
        _client = client;
    }

    public async Task<string> PrioritizeAsync(
        string debtData)
    {
        var prompt = $"""
        Analyze technical debt findings.

        Determine:

        1. Priority ranking
        2. Business impact
        3. Engineering risk
        4. Recommended actions

        {debtData}
        """;

        var response =
            await _client.GetChatCompletionsAsync(
                "gpt-4o",
                new ChatCompletionsOptions
                {
                    Messages =
                    {
                        new ChatMessage(
                            ChatRole.User,
                            prompt)
                    }
                });

        return response.Value
            .Choices[0]
            .Message
            .Content;
    }
}

The AI model evaluates technical and business factors together.

Example AI Analysis

Input:

Issue:
Legacy Authentication Module

Complexity:
High

Incidents:
15

Affected Users:
100,000

Generated output:

Priority:
Critical

Business Impact:
High

Reason:
Authentication failures directly impact
customer access and security posture.

Recommendation:
Refactor within next sprint.

This provides much richer context than a simple severity score.

Creating Technical Debt Scores

AI can generate composite scores.

Example:

Complexity Score:
82

Business Impact:
91

Operational Risk:
88

Overall Priority:
89

Engineering leaders can use these scores for roadmap planning.

Evaluating Dependency Risks

Many technical debt issues exist within shared libraries or core services.

Example:

Library:
Authentication SDK

Dependent Services:
24

Risk:
High

AI can increase priority based on dependency impact.

Identifying Hidden Technical Debt

Traditional scanners often miss architectural problems.

Examples include:

  • Service coupling

  • Unclear ownership

  • Legacy workflows

  • Knowledge silos

AI can identify these patterns by analyzing repository activity and operational behavior.

Forecasting Future Costs

One of the most valuable AI capabilities is predicting future impact.

Example:

Current Risk:
Medium

Expected Risk in 6 Months:
High

Reason:
Increasing deployment frequency and
growing dependency usage.

This helps organizations address issues before they become critical.

Sprint Planning Recommendations

AI can recommend debt remediation work during sprint planning.

Example:

Recommended Sprint Tasks:

1. Upgrade Authentication Library

2. Reduce Payment Service Complexity

3. Add Missing Integration Tests

4. Remove Deprecated API Endpoints

This enables data-driven prioritization.

Advanced Enterprise Features

Large organizations often enhance prioritization systems with additional intelligence.

Service Criticality Analysis

Evaluate:

  • Revenue impact

  • User traffic

  • Business dependency

when calculating priorities.

Historical Incident Correlation

Link technical debt findings to previous outages.

Team Capacity Planning

Recommend remediation work based on available engineering resources.

Executive Reporting

Generate summaries for engineering leadership.

Example:

Top 10 Technical Debt Risks

Estimated Annual Cost:
$240,000

Recommended Investment:
4 Engineering Weeks

These reports improve strategic decision-making.

Best Practices

Combine Technical and Business Metrics

Technical debt should never be prioritized solely by code quality scores.

Continuously Refresh Data

Repository activity and operational metrics change frequently.

Validate AI Recommendations

Engineering teams should review priorities before execution.

Focus on High-Impact Debt

Not all technical debt requires immediate action.

Measure Outcomes

Track whether debt reduction efforts improve reliability and developer productivity.

Benefits of AI-Powered Technical Debt Prioritization

Organizations implementing intelligent prioritization systems often achieve:

  • Better engineering focus

  • Reduced maintenance costs

  • Improved system reliability

  • Faster development cycles

  • Stronger architectural health

  • More effective sprint planning

Teams spend less time debating priorities and more time solving meaningful problems.

Conclusion

Although mismanaged technological debt can greatly impede innovation and raise operational risk, it is unavoidable. Because they only consider code quality rather than commercial and operational effect, traditional prioritizing techniques frequently fall short.

Organizations may create AI-powered technical debt prioritization systems that detect the most important problems, predict future hazards, and suggest focused remediation strategies by integrating ASP.NET Core, repository analytics, operational telemetry, and Azure OpenAI. Intelligent technical debt management will become a crucial skill for preserving software quality and delivery speed as engineering companies continue to grow.
Read More...