DEV Community

Sabin Sim
Sabin Sim

Posted on

C#.NET - day 06

Day 6: Architect Version — Defensive Search with Pagination in ASP.NET Core

Designing a search flow that stays stable under bad input and heavy traffic

Introduction

At this stage, the goal is no longer just “making search work.” The focus shifts to something more important:

How can a system stay responsive and safe when data grows and requests become unpredictable?

This Architect version introduces pagination, validation, and defensive design to prevent performance degradation and server crashes.


🌊 System Flow (Architect View)

  1. Request (Input): The client sends a request like GET /api/hello/search?name=sa&page=1&size=5.
  2. Inspection (Service): The Service sanitizes the input:
    • Invalid page numbers are corrected.
    • Excessive page sizes are capped to protect the server.
  3. Query (Repository): A query plan is built without loading unnecessary data. Filtering is case-insensitive, and pagination is applied using Skip and Take.
  4. Response (Output): Only optimized, limited data is returned to the client.

1️⃣ Repository: Query Strategy and Pagination

The Repository acts as a lightweight database engine. Its responsibility is to execute efficient queries against stored data.

using System.Collections.Concurrent;
using HelloFlow.Models;

namespace HelloFlow.Repositories;

public class HelloRepository
{
    private readonly ConcurrentDictionary<string, HelloResponse> _storage = new();

    public void Save(HelloResponse data)
    {
        var key = Guid.NewGuid().ToString();
        _storage.TryAdd(key, data);
    }

    public List<HelloResponse> GetAll()
    {
        return _storage.Values.ToList();
    }

    public List<HelloResponse> SearchAdvanced(string keyword, int pageNumber, int pageSize)
    {
        var query = _storage.Values.AsEnumerable();

        if (!string.IsNullOrWhiteSpace(keyword))
        {
            query = query.Where(x =>
                x.Message.Contains(keyword, StringComparison.OrdinalIgnoreCase));
        }

        return query
            .OrderByDescending(x => x.CreatedAt)
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .ToList();
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach avoids loading all data into memory at once and mirrors how real database queries behave.


2️⃣ Service: Input Validation and Server Protection

The Service layer acts as a checkpoint. Its job is not querying data, but protecting the system from invalid or dangerous input.

using HelloFlow.Models;
using HelloFlow.Repositories;

namespace HelloFlow.Services;

public class HelloService
{
    private readonly HelloRepository _repository;

    public HelloService(HelloRepository repository)
    {
        _repository = repository;
    }

    public HelloResponse GetHello(string name)
    {
        var response = new HelloResponse
        {
            Message = $"Hello, {name}!",
            CreatedAt = DateTime.Now,
            Location = "Cazis, Switzerland"
        };

        _repository.Save(response);
        return response;
    }

    public List<HelloResponse> FindHelloAdvanced(string keyword, int page, int size)
    {
        int safePage = page < 1 ? 1 : page;
        int safeSize = size > 50 ? 50 : size;

        return _repository.SearchAdvanced(keyword, safePage, safeSize);
    }
}
Enter fullscreen mode Exit fullscreen mode

Page numbers are normalized, and excessive requests are throttled. This prevents memory exhaustion and performance issues.


3️⃣ Controller: Request Routing Only

The Controller remains intentionally simple. It extracts parameters from the request and forwards them.

using Microsoft.AspNetCore.Mvc;
using HelloFlow.Services;

namespace HelloFlow.Controllers;

[ApiController]
[Route("api/[controller]")]
public class HelloController : ControllerBase
{
    private readonly HelloService _service;

    public HelloController(HelloService service)
    {
        _service = service;
    }

    [HttpGet]
    public IActionResult SayHello(string name)
    {
        var result = _service.GetHello(name);
        return Ok(result);
    }

    [HttpGet("search")]
    public IActionResult SearchHello(
        [FromQuery] string? name,
        [FromQuery] int page = 1,
        [FromQuery] int size = 10
    )
    {
        var results = _service.FindHelloAdvanced(name ?? "", page, size);
        return Ok(results);
    }
}
Enter fullscreen mode Exit fullscreen mode

No business rules, no data logic—just routing.


🧠 Why This Is an “Architect Version”

This version does more than provide search functionality. It anticipates real-world conditions:

  • Large datasets
  • Unexpected input values
  • Sudden spikes in traffic

Pagination limits memory usage. Validation protects the server. Query planning prepares the codebase for a future database migration.


🧠 Mental Model Summary

In short:

  • Controller: receives requests
  • Service: validates and enforces policy
  • Repository: executes optimized queries

Each layer has a clear boundary.


✍️ My Notes & Reflections

  • This part was not easy to fully understand. For now, I decided to focus on confirming the overall structure and flow, and come back to the details later. Still, I feel that I understand the core idea up to this point.
  • The system now feels more robust: it can handle sudden spikes in requests, supports searching, and works correctly regardless of letter casing.
  • Compared to my previous experience with Python, the visible feedback in C# feels more limited. I type the code, click run, and check the result, but there is a certain ambiguity in that process.
  • It feels like I almost understand it, but not quite. If I ask myself, “Do you really understand this?”, I cannot confidently answer yes. That uncertainty probably means I do not fully understand it yet.
  • Still, I believe this is part of the process. By thinking, revisiting, and repeating these steps, I expect that familiarity will eventually turn into real understanding.

Top comments (0)