The HTTP layer becomes a thin adaptor, intentions are explicit, and invariants reside in the domain in mature.NET systems that converge on a feature-first structure. Request > Entity > Processor > Response is one name for that form. It is what those concepts typically result from when scaled across teams and time; it does not take the place of Clean Architecture or CQRS. In order to keep the surface modest and the business logic at the center, this article describes REPR as a fundamental design and shows how to express it precisely in modern.NET using FastEndpoints.

Clarity is the aim, with each use-case being represented by a slice made up of a consumer-shaped contract (Response), a truthful domain core (Entity), a use case script (Processor), and an intent (Request). The slice itself conveys the meaning, so incidental abstractions, controllers, and service layers disappear. As a result, the codebase reads by verbs instead of layers, which is the logical unit of reasoning for both engineering and products.

Why REPR Emerges from Clean Architecture and CQRS?
Clean Architecture pushes dependencies inward and centres decisions in the domain. CQRS separates reads and writes to sharpen intent. Yet both leave open the practical question: What is the smallest useful unit of a feature? REPR answers with a compact pipeline. The Request captures intent without leaking storage concerns. The Entity enforces business rules and invariants. The Processor orchestrates precisely what the use-case demands and nothing more. The Response returns a business outcome suitable for its consumer, not an accidental snapshot of internal state.

This alignment reduces cognitive load. A feature has a single entry and a single exit, and the domain is the only place allowed to change truth. Cross-cutting concerns become pipeline steps around the Processor instead of boilerplate inside it. Teams scale by owning sets of verbs rather than slices of a layered cake.

The Feature Slice: Directory Shape and Mental Model

A coherent slice contains only what it needs, co-located for discoverability. An example for approving a Submission (the same structure fits quotes, orders, pricing, or claims):
/Features/Submissions/Approve

ApproveSubmissionRequest.cs

ApproveSubmissionResponse.cs

ApproveSubmissionValidator.cs

ApproveSubmissionEndpoint.cs

ApproveSubmissionProcessor.cs

Submission.cs

ISubmissionRepository.cs

EfSubmissionRepository.cs


Each file is small and purposeful. The Endpoint adapts transport to intent. The Processor applies policy and coordinates domain work. The Entity owns invariants and protects truth. The repository is a narrow port that returns the shapes the domain needs.

The REPR Flow
The Request expresses what the caller wants. The Processor asks the Entity to perform domain operations under invariant checks. The Repository persists or projects as needed. The Response communicates the outcome in consumer terms. The HTTP or messaging edge is merely a conduit.

Request and Response: Intent and Outcome
Requests model intent; they are not transport bags or EF rows. Responses model what consumers need; they are not entities.
// Request > intent, not storage schema.

public sealed record ApproveSubmissionRequest(long SubmissionId, string ApprovedBy);

// Response > consumer-shaped business outcome.

public sealed record ApproveSubmissionResponse(

long SubmissionId,

DateTime ApprovedAtUtc,

string ApprovedBy,

string StatusMessage);


Names should read like language in the domain. Versioning the Response is an explicit contract decision, not an accident.

Entity: The Only Place Allowed to Change Truth
Entities own invariants. Processors never toggle fields directly; they call domain methods that enforce rules atomically.

public sealed class Submission(long id, DateTime createdAtUtc)

{

public long Id { get; } = id;

public DateTime CreatedAtUtc { get; } = createdAtUtc;

public bool IsApproved { get; private set; }

public DateTime? ApprovedAtUtc { get; private set; }

public string? ApprovedBy { get; private set; }

public Result Approve(string approver, DateTime nowUtc)

{

if (IsApproved)

return Result.Fail("Submission is already approved.");

if (string.IsNullOrWhiteSpace(approver))

return Result.Fail("Approver is required.");

IsApproved  = true;

ApprovedAtUtc = nowUtc;

ApprovedBy  = approver;

return Result.Ok();

}

}

A small, explicit domain method prevents entire categories of drift, such as direct flag mutation scattered across handlers.

Repository Port: Narrow and Purposeful

Commands require Entities to enforce invariants; queries typically require projections. A port keeps the Processor independent of storage concerns.

public interface ISubmissionRepository

{

Task<Submission?> GetAsync(long id, CancellationToken ct);

Task SaveAsync(Submission Submission, CancellationToken ct);

}


Implementation choices (EF Core, Dapper, external service) do not change the Processor surface. N+1 issues are solved inside the repository contract, not leaked into callers.

Processor: The Use-Case Script
The Processor composes the use-case: load, ask, change, persist, reply. It does not perform transport work or data access gymnastics; it orchestrates policy with the Entity.

public interface IClock { DateTime UtcNow { get; } }

public sealed class SystemClock : IClock { public DateTime UtcNow => DateTime.UtcNow; }

public sealed class ApproveSubmissionProcessor(ISubmissionRepository repo, IClock clock)

{

public async Task<Result<ApproveSubmissionResponse>> Handle(ApproveSubmissionRequest req, CancellationToken ct)

{

var Submission = await repo.GetAsync(req.SubmissionId, ct);

if (Submission is null)

return Result.Fail<ApproveSubmissionResponse>("Submission not found.");

var approved = Submission.Approve(req.ApprovedBy, clock.UtcNow);

if (approved.IsFailure)

return approved.Cast<ApproveSubmissionResponse>();

await repo.SaveAsync(Submission, ct);

return Result.Ok(new ApproveSubmissionResponse(

Submission.Id,

Submission.ApprovedAtUtc!.Value,

Submission.ApprovedBy!,

"Approved"));

}

}


This class should remain short. If it grows, the Entity likely needs a new domain operation to absorb rules.

FastEndpoints: The Precise HTTP Adapter
FastEndpoints models the HTTP edge with minimal ceremony. The Endpoint accepts the Request and delegates to the Processor, returning a Response or an error mapping.

using FastEndpoints;

public sealed class ApproveSubmissionEndpoint(ApproveSubmissionProcessor processor)

: Endpoint<ApproveSubmissionRequest, ApproveSubmissionResponse>

{

public override void Configure()

{

Post("/Submission/Submissions/{SubmissionId:long}/approve");

Version(1);

Summary(s =>

{

s.Summary = "Approve a Submission";

s.Description = "Approves a Submission if business rules allow.";

s.ExampleRequest = new ApproveSubmissionRequest(42, "Approver01");

});

}

public override async Task HandleAsync(ApproveSubmissionRequest req, CancellationToken ct)

{

var result = await processor.Handle(req, ct);

if (result.IsSuccess)

await SendOkAsync(result.Value, ct);

else

await SendErrorsAsync(400, result.Message);

}

}


The Endpoint remains transport-centric. It does not read data stores, compute business rules, or create hidden dependencies.

Validation: Polite at the Edge, Strict in the Core

Transport-level validation protects the Processor from malformed input, and invariants in the Entity protect the core from impossible states.
using FastEndpoints;

using FluentValidation;

public sealed class ApproveSubmissionValidator : Validator<ApproveSubmissionRequest>

{

public ApproveSubmissionValidator()

{

RuleLevelCascadeMode = CascadeMode.Stop;

RuleFor(r => r.SubmissionId).GreaterThan(0);

RuleFor(r => r.ApprovedBy).NotEmpty().MaximumLength(200);

}

}

This split is essential. It prevents duplication and avoids shifting business rules into validators where they cannot be enforced consistently.

Program Setup: Small Surface, Clear Wiring

Minimal registration with DI and FastEndpoints keeps the composition explicit and discoverable.
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(...);

builder.Services.AddScoped<ISubmissionRepository, EfSubmissionRepository>();

builder.Services.AddScoped<ApproveSubmissionProcessor>();

builder.Services.AddSingleton<IClock, SystemClock>();

builder.Services.AddFastEndpoints();

builder.Services.AddFluentValidationAutoValidation();

builder.Services.AddSwaggerDoc();

var app = builder.Build();

app.UseAuthorization();

app.UseFastEndpoints();

app.UseOpenApi();

app.UseSwaggerUi3();

app.Run();


New slices add themselves by file convention; the application is composed of features rather than layers.

Query Slices: Reads Benefit Even More
Read models become explicit, projection-first, and immune to accidental IQueryable leakage. The Request captures query intent, the Processor (or Endpoint for simple reads) projects to a consumer shape.
public sealed record GetSubmissionsRequest(int Page = 1, int PageSize = 20);

public sealed record SubmissionSummary(long Id, bool IsApproved, DateTime CreatedAtUtc);

public sealed class GetSubmissionsEndpoint(MyDbContext db)

: Endpoint<GetSubmissionsRequest, IReadOnlyList<SubmissionSummary>>

{

public override void Configure()

{

Get("/uwSubmission/Submissions");

Version(1);

}

public override async Task HandleAsync(GetSubmissionsRequest req, CancellationToken ct)

{

if (req.Page <= 0 || req.PageSize is <= 0 or > 200)

{

await SendErrorsAsync(400, "Invalid paging");

return;

}

var data = await db.Submissions

.AsNoTracking()

.OrderByDescending(h => h.CreatedAtUtc)

.Skip((req.Page - 1) * req.PageSize)

.Take(req.PageSize)

.Select(h => new SubmissionSummary(h.Id, h.IsApproved, h.CreatedAtUtc))

.ToListAsync(ct);

await SendOkAsync(data, ct);

}

}


Read and write slices share the same mental model and directory shape, simplifying navigation and ownership.

Cross-Cutting Concerns: Pipeline, Not Boilerplate

In REPR, cross-cutting concerns belong around the Processor as pipeline steps or behaviours, not inside it. FastEndpoints supports pre/post processors at the HTTP edge; a mediator or composition pattern can provide behaviours around the Processor for logging, correlation, authorisation, idempotency, and resilience.

A simple correlation pre-processor:
public sealed class CorrelationPreProcessor<TReq, TRes>(

ILogger<CorrelationPreProcessor<TReq,TRes>> log)

: IPreProcessor<Endpoint<TReq,TRes>>

where TReq : notnull

{

public Task PreProcessAsync(Endpoint<TReq,TRes> ep, CancellationToken ct)

{

var cid = ep.HttpContext.TraceIdentifier;

ep.HttpContext.Items["cid"] = cid;

log.LogInformation("Handling {Request} cid={Cid}", typeof(TReq).Name, cid);

return Task.CompletedTask;

}

}


Applied globally or per endpoint, this keeps transport concerns at the edge and business concerns in the core.

Events and Outbox: Facts Out, Reliably

Clarity is the aim, with each use-case being represented by a slice made up of a consumer-shaped contract (Response), a truthful domain core (Entity), a use case script (Processor), and an intent (Request). The slice itself conveys the meaning, so incidental abstractions, controllers, and service layers disappear. As a result, the codebase reads by verbs instead of layers, which is the logical unit of reasoning for both engineering and products.

This pattern aligns neatly with REPR’s flow: intent in, invariant-checked state change, facts recorded, outcome returned.

Testing: Small, Fast, and Honest

REPR improves testability by shrinking surface areas. Entities are tested directly via domain methods. Processors are tested with fake repositories and clocks, asserting on Result<T> and domain state. Endpoints can be verified with FastEndpoints’ in-memory host where HTTP-level assurance is required, but business logic never depends on a web server to be testable.

A Processor test is concise and meaningful:
public async Task Approve_Sets_State_And_Returns_Response()

{

var now = new DateTime(2025, 10, 24, 12, 0, 0, DateTimeKind.Utc);

var clock = Substitute.For<IClock>(); clock.UtcNow.Returns(now);

var Submission = new Submission(42, now.AddDays(-1));

var repo = Substitute.For<ISubmissionRepository>();

repo.GetAsync(42, Arg.Any<CancellationToken>()).Returns(Submission);

var processor = new ApproveSubmissionProcessor(repo, clock);

var result = await processor.Handle(new ApproveSubmissionRequest(42, "PK"), CancellationToken.None);

result.IsSuccess.ShouldBeTrue();

Submission.IsApproved.ShouldBeTrue();

Submission.ApprovedAtUtc.ShouldBe(now);

Submission.ApprovedBy.ShouldBe("PK");

await repo.Received(1).SaveAsync(Submission, Arg.Any<CancellationToken>());

}


The test demonstrates intent, rule enforcement, and persistence orchestration without scaffolding overhead.

Anti-Patterns to Avoid

A few pitfalls undermine the benefits:

  • Requests that mirror database rows rather than intent encourage accidental coupling and bloat.
  • Processors that toggle fields directly instead of calling domain methods bypass invariants.
  • Repositories that return IQueryable leak persistence concerns and invite N+1 issues outside the boundary.
  • Responses that expose Entities create fragile contracts and accidental internal coupling.
  • Generic “BaseRepository<T>” or “Service” classes that centralise convenience instead of domain language lead to anaemic designs.

REPR’s strength is clarity; resist abstractions that obscure intent.

Impact and Ownership

Eams use verbs and results to reason. When a slice of a story—Request, Processor, Response—with invariants visible in an entity, PRs examine it more quickly. Because logs and analytics match Request types and semantic processes rather than raw URLs, observability improves. Instead of mapping to controllers or horizontal layers, ownership maps to features, allowing for autonomous delivery free from unintentional contention.

REPR is not a framework and does not require a mediator, controller, or a particular storage engine. It is the minimal shape a serious codebase adopts when Clean Architecture and CQRS are applied with discipline and scaled across a product’s lifetime. Expressed with FastEndpoints, the HTTP edge becomes quiet and intention-based, and the domain regains custody of truth.

The practical move is simple: for the next non-trivial feature, create a slice named for the verb. Write the Request so the intent is explicit. Put rules in an Entity method. Keep the Processor short and orchestral. Return a Response that a consumer would actually want. Repeat until the architecture fades into the furniture and only the product remains visible.

HostForLIFE ASP.NET Core 10.0 Hosting

European Best, cheap and reliable ASP.NET Core 10.0 hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.