Hello, I'm

Spyros
Ponaris

Athens, Greece  ·  Web Partner.gr

BSc · University of Sunderland MSc · University of Greenwich MCP & MCSD Certified
120+ GitHub Repos
57 Articles Published
645 GitHub Followers
750 Dev.to Followers
Scroll

About Me

SP
Spyros Ponaris — Tech Lead and Senior Software Engineer

I'm a Tech Lead and Senior Software Engineer based in Athens, Greece, with a deep passion for .NET technologies, enterprise architecture, and building scalable, maintainable systems on-premises and on Microsoft Azure.

With a BSc from the University of Sunderland and an MSc from the University of Greenwich, complemented by MCP and MCSD certifications, I bring both academic depth and hands-on expertise to every project.

I'm also an active author on Dev.to and CoderLegion, with 57 published articles covering design patterns, EF Core, Blazor, microservices, and modern .NET. I love sharing knowledge and collaborating with the developer community.

Experience & Credentials

2020 – Present

Tech Lead

Web Partner.gr  ·  Athens, Greece

Leading engineering teams to deliver enterprise .NET solutions. Driving architectural decisions, mentoring developers, and establishing best practices around microservices, CQRS, and clean architecture.

.NET Microservices Team Leadership Architecture
2014 – 2020

Senior Software Engineer

Athens, Greece

Designed and delivered complex enterprise applications with ASP.NET Core, Blazor, and SQL Server. Introduced design patterns, EF Core optimisations, and async programming practices across projects.

ASP.NET Core Blazor EF Core SQL Server
2007 – 2014

Software Engineer

Athens, Greece

Built business applications using C#, VB.NET, and SQL Server. Developed strong fundamentals in object-oriented design and database-driven development.

C# VB.NET SQL Server

Education & Certifications

MSc Computer Science

University of Greenwich

BSc Computer Science

University of Sunderland

MCSD

Microsoft Certified Solutions Developer

MCP

Microsoft Certified Professional

Beyond The Code

A little about what I get up to when the IDE is closed.

Technical Writing

57+ articles published across Dev.to and CoderLegion — sharing what I learn about .NET, design patterns, and architecture.

Topics I keep coming back to: EF Core performance, CQRS & MediatR, the Outbox & Saga patterns, Blazor render modes, async / concurrency in C#, clean architecture, and modern .NET 8 / 9 features. Active author and moderator on CoderLegion — publishing articles and reviewing community posts to help new authors get published.

57+ Articles 750 Dev.to Followers CoderLegion Author & Moderator

Open Source

120+ repositories on github.com/stevsharp — building and sharing side projects that explore new patterns, libraries, and ideas with 645 followers along for the ride.

Featured work includes OutboxPattern (transactional outbox in .NET), OutboxSagaDemo (Saga + Outbox with MassTransit), MicroservicesOcelotDemo (Ocelot API Gateway), MicroOrm (a lightweight .NET ORM), BlazorFactoryDemo, DbLoggerSample, and Billing (a .NET 8 billing system built with Clean Architecture — Domain, Application, and Infrastructure layers — backed by Entity Framework Core). Each repo is meant to be a readable reference, not a kitchen-sink framework.

120+ Repos 645 Followers C# · .NET

Open Source Projects

Reference implementations built to show enterprise .NET patterns composing together in a real codebase — readable, opinionated, and open for study.

Billing.Api

.NET 8 · Entity Framework Core · Clean Architecture

View on GitHub

A public reference Web API that models a complete invoice lifecycle — created, issued, paid, and cancelled — using a rich domain model. Every architectural decision is intentional: this is a codebase you can read to understand how enterprise .NET patterns compose, not a starter template or a kitchen-sink framework.

Patterns & Techniques

CA

Clean Architecture

Four concentric layers — Domain → Application → Infrastructure → API. Dependencies only point inward; the domain has zero framework references.

Billing.Domain/          ← entities, value objects, events
Billing.Application/     ← use-cases, handlers, abstractions
Billing.Infrastructure/  ← EF Core, outbox, identity
Billing/                 ← API controllers, DI wiring
DDD

Domain-Driven Design

Rich domain model: Invoice aggregate with InvoiceLine child entities, value objects, and domain events raised on every meaningful state transition.

// Value objects
InvoiceId | InvoiceNumber | Money

// Domain events (raised by Invoice aggregate)
InvoiceCreated  | InvoiceIssued
InvoicePaid     | InvoiceCancelled
CQRS

CQRS

Commands and queries use separate EF Core contexts. The write side goes through the full aggregate; the read side uses lean, no-tracking projections.

BillingWriteContext  // commands — full aggregate tracking
BillingReadContext   // queries — optimised projections
OB

Outbox Pattern

Domain events are persisted as OutboxMessages in the same transaction as the state change, then published by OutboxPublisher — guaranteeing at-least-once delivery even if the process crashes mid-flight.

// Single DB transaction:
1. Save aggregate changes
2. Insert OutboxMessage row

// Background step:
OutboxPublisher → dispatch to message broker
MB

MediatR Pipeline Behaviors

Cross-cutting concerns handled in the MediatR pipeline — no decorator boilerplate inside individual handlers.

ValidationBehavior   // FluentValidation before any handler
TransactionBehavior  // wraps ITransactionalRequest commands
                     //   in a DB transaction automatically
EF

EF Core Interceptors

DomainInterceptor hooks into SaveChangesAsync to collect and dispatch domain events from tracked aggregates automatically — no manual wiring required in handlers.

// DomainInterceptor.cs
override async ValueTask<int> SavingChangesAsync(...) {
    var events = context.GetDomainEvents();
    var result = await base.SavingChangesAsync(...);
    await dispatcher.DispatchAsync(events);
    return result;
}
MT

Multi-Tenancy

Tenant identity is resolved from the HTTP context via HttpTenantProvider and injected through ITenantProvider — the domain stays clean of request-scoped concerns.

ITenantProvider    // abstraction (Application layer)
HttpTenantProvider // resolved from HTTP context
ICurrentUser       // abstraction
HttpCurrentUser    // resolved from ClaimsPrincipal
UoW

Unit of Work

IUnitOfWork abstracts SaveChangesAsync so application-layer handlers commit work without depending on EF Core directly — the infrastructure detail stays behind an interface.

// Application layer sees only:
public interface IUnitOfWork {
    Task<int> SaveChangesAsync(CancellationToken ct);
}
// BillingWriteContext implements it in Infrastructure
.NET 8 EF Core Clean Architecture DDD CQRS Outbox Pattern MediatR Multi-Tenancy C#

Technical Skills

Languages

C# TypeScript JavaScript C++ Visual Basic SQL

🏗️ Frameworks

ASP.NET Core Blazor EF Core Angular Minimal APIs

🗄️ Databases & Caching

SQL Server PostgreSQL EF Core Redis

🔧 Libraries & Messaging

MassTransit MediatR Hangfire Dapper FluentValidation Syncfusion Apache Kafka RabbitMQ

🧩 Architecture & Patterns

Microservices CQRS Outbox Pattern Saga Pattern MVVM Design Patterns DDD Clean Architecture

☁️ Cloud & DevOps

Microsoft Azure Azure App Service Azure SQL Azure Service Bus Azure Functions Azure Blob Storage Azure Key Vault Azure DevOps CI/CD Pipelines

🚀 Specializations

Enterprise Architecture Concurrency Performance Tuning Technical Mentoring

Commercial Applications

Enterprise-grade software built for real-world industries — fully customisable, demo-ready, and one live in production.

Vres Metaforea

● LIVE

Moving Services Marketplace — Athens, Greece

A Greek-language marketplace that matches Athens residents needing relocation services with vetted moving companies. Users complete a multi-step form — origin, destination, apartment size, floors, dates, special requirements — and receive free, no-obligation quotes from 24+ registered companies across 17 Athens neighbourhoods. Features company ratings, verification badges, and geographic browsing with interactive maps.

Web Marketplace Multi-step Form Geo Search Reviews & Ratings Greek Market Production
Visit Live Site

CRM

Customer Relationship Management

A full-featured CRM for managing leads, contacts, pipelines, and customer interactions. Built with Blazor and .NET — designed to scale from small teams to large enterprises.

Blazor .NET EF Core SQL Server REST API
Greek CRM — Dashboard Greek CRM — New Quote Greek CRM — Customer Invoices Greek CRM — New Invoice
Ask for a Demo

Vet Application

Veterinary Practice Management

End-to-end veterinary clinic software covering patient records, appointments, treatments, billing, and reminders — helping vets focus on care, not paperwork.

ASP.NET Core Blazor EF Core Hangfire PDF Reports
Vet App — Pet Owners list Vet App — Owner detail with pets Vet App — New owner form Vet App — New pet form
Ask for a Demo

Training

Learning Management System

A structured LMS for delivering courses, tracking learner progress, and managing certifications — ideal for corporate training, onboarding, and professional development programmes.

Blazor .NET EF Core Hangfire Role-based Access

Topics Covered

C# Blazor ASP.NET Core EF Core Dapper SQL Server
Enquire About Training

Salon ERP

Beauty & Salon Business Suite

All-in-one ERP for salons and beauty businesses — appointments, staff scheduling, inventory, loyalty programmes, and financial reporting in a single platform.

Blazor .NET Syncfusion MediatR Multi-tenant
SalonPro — Dashboard with revenue chart and appointments SalonPro — Staff appointment calendar SalonPro — Services management with pricing
Ask for a Demo

Insurance & Auto Repair

Vehicle Repair Shop Management

Full workshop management for auto repair shops handling insurance claims — job cards, parts ordering, visual damage assessment diagrams, workflow tracking, and revenue analytics across multiple insurers.

Blazor .NET EF Core Syncfusion Insurance Workflow
Auto Repair — Dashboard with flow charts and appointments Auto Repair — Vehicle damage diagram
Ask for a Demo

Retail ERP

Order, Scheduling & Revenue Management

End-to-end retail management covering order scheduling, appointment calendars, product sales tracking, revenue dashboards, and top-customer analytics — built for businesses that need visibility across sales and operations.

Blazor .NET Syncfusion EF Core Revenue Analytics
Retail ERP — Appointment scheduling calendar Retail ERP — Revenue dashboard with top products and customers
Ask for a Demo

Service DB

Vehicle Service Records

A complete vehicle service management platform tracking customers, vehicles, and service history. Features a guided 3-step service wizard, revenue analytics, top-customer leaderboards, and a real-time activity feed — built on SQLite for zero-config deployment.

Blazor .NET SQLite EF Core Service Tracking
Service DB — Dashboard with revenue and top customers Service DB — New service wizard step 1: customer Service DB — New service wizard step 2: vehicle Service DB — New service wizard step 3: details
Ask for a Demo

Paint Bull

Auto Paint & Body Shop Management

A specialised management system for auto paint and body shops — handling job intake, paint mixing records, repair workflow, customer notifications, and billing with multi-user role-based access.

Blazor .NET SQL Server EF Core Role-based Access
Paint Bull — Login screen
Ask for a Demo

Latest Articles

Published on dev.to/stevsharp · 57+ articles loaded live from the Dev.to API

All EF Core Design Patterns Blazor Async / Threading Microservices C# / .NET

Deep-Dive Tutorials

Full-length guides and bite-sized snippets — from absolute-beginner C# basics to advanced T-SQL, OOP & SOLID, Minimal API, and Algorithms & Data Structures.

Beginner Guides · 4 tutorials

Deep-Dive Guides · 3 tutorials

Design Patterns · 1 tutorial

Quick Snippets · copy-paste ready

C#

Hello, Minimal API

The smallest possible ASP.NET Core Web API. One file, one endpoint, full HTTPS & DI.

// Program.cs — .NET 8+
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hello/{name}", (string name) =>
    Results.Ok(new { message = $"Hello, {name}!" }));

app.Run();
SQL Server

CRUD in 6 lines

Create a table, insert a row, query it back. Works in SSMS, Azure Data Studio, or sqlcmd.

CREATE TABLE dbo.Products (
    Id   INT IDENTITY PRIMARY KEY,
    Name NVARCHAR(100) NOT NULL,
    Price DECIMAL(10,2) NOT NULL
);

INSERT INTO dbo.Products (Name, Price) VALUES ('Coffee', 3.50);
SELECT TOP 10 * FROM dbo.Products ORDER BY Id DESC;
Azure Service Bus

Send your first message

Cloud-scale broker for enterprise messaging. Add Azure.Messaging.ServiceBus and you're done.

using Azure.Messaging.ServiceBus;

var client = new ServiceBusClient("<connection-string>");
var sender = client.CreateSender("orders");

await sender.SendMessageAsync(
    new ServiceBusMessage("{\"orderId\":42}"));

Console.WriteLine("Message sent ✔");
Azure Functions

Serverless HTTP trigger

Run code on demand, pay per execution. Isolated worker model on .NET 8.

public class HelloFunction
{
    [Function("Hello")]
    public HttpResponseData Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get")]
        HttpRequestData req)
    {
        var res = req.CreateResponse(HttpStatusCode.OK);
        res.WriteString("Hello from Azure Functions!");
        return res;
    }
}
RabbitMQ

Publish to a queue

Battle-tested AMQP broker. NuGet: RabbitMQ.Client.

using RabbitMQ.Client;
using System.Text;

var factory = new ConnectionFactory { HostName = "localhost" };
using var conn    = factory.CreateConnection();
using var channel = conn.CreateModel();

channel.QueueDeclare("hello", durable: false,
    exclusive: false, autoDelete: false);

var body = Encoding.UTF8.GetBytes("Hello, RabbitMQ!");
channel.BasicPublish("", "hello", null, body);
Redis

Cache anything in 4 lines

In-memory key/value store. NuGet: StackExchange.Redis.

using StackExchange.Redis;

var redis = await ConnectionMultiplexer
    .ConnectAsync("localhost:6379");
var db = redis.GetDatabase();

await db.StringSetAsync("user:42", "Spyros");
var name = await db.StringGetAsync("user:42"); // "Spyros"
Apache Kafka

Produce an event

Distributed event-streaming platform. NuGet: Confluent.Kafka.

using Confluent.Kafka;

var config = new ProducerConfig
    { BootstrapServers = "localhost:9092" };

using var producer = new ProducerBuilder
    <Null, string>(config).Build();

await producer.ProduceAsync("orders",
    new Message<Null, string> { Value = "{\"orderId\":42}" });
EF Core

DbContext & first query

The de-facto .NET ORM. Define a DbContext, register it, and LINQ your way to data.

public class Product { public int Id; public string Name = ""; }

public class AppDb : DbContext
{
    public DbSet<Product> Products => Set<Product>();
    protected override void OnConfiguring(DbContextOptionsBuilder o) =>
        o.UseSqlServer("Server=.;Database=Shop;Trusted_Connection=True");
}

using var db = new AppDb();
db.Products.Add(new Product { Name = "Coffee" });
await db.SaveChangesAsync();

var all = await db.Products.AsNoTracking().ToListAsync();
Dapper

Micro-ORM that flies

Hand-written SQL with object mapping. Fastest IDbConnection extension on NuGet. Owned by Stack Overflow.

using Dapper;
using Microsoft.Data.SqlClient;

public record Product(int Id, string Name, decimal Price);

using var conn = new SqlConnection(
    "Server=.;Database=Shop;Trusted_Connection=True");

// Query
var top = await conn.QueryAsync<Product>(
    "SELECT TOP 10 Id, Name, Price FROM Products ORDER BY Id DESC");

// Parameterised insert
await conn.ExecuteAsync(
    "INSERT INTO Products(Name, Price) VALUES(@Name, @Price)",
    new { Name = "Coffee", Price = 3.50m });
Hangfire

Fire-and-forget background jobs

Persistent background jobs for .NET — no Windows Service needed. NuGet: Hangfire.AspNetCore.

// Program.cs
builder.Services.AddHangfire(c =>
    c.UseSqlServerStorage("Server=.;Database=Hangfire;Trusted_Connection=True"));
builder.Services.AddHangfireServer();

var app = builder.Build();
app.UseHangfireDashboard("/jobs");

// Anywhere in your code:
BackgroundJob.Enqueue(() => Console.WriteLine("Hello from a job!"));
RecurringJob.AddOrUpdate("daily-report",
    () => Console.WriteLine("tick"), Cron.Daily);

Architecture Deep Dive

SOLID principles & C# design patterns — short, opinionated, with code you can actually paste into a project.

SOLID Principles

Robert C. Martin's five rules of object-oriented design. Memorise them once, apply them everywhere.

S

Single Responsibility

A class should have one reason to change. If you describe it with "and", split it.

// ❌ Two reasons to change: persistence AND email
class UserService {
    public void Register(User u) {
        _db.Save(u);
        _smtp.Send(u.Email, "Welcome");
    }
}

// ✅ One job each
class UserRepository    { public void Save(User u) => _db.Save(u); }
class WelcomeEmailer    { public void Send(User u) => _smtp.Send(u.Email, "Welcome"); }
class RegisterUserHandler(UserRepository repo, WelcomeEmailer mail) {
    public void Handle(User u) { repo.Save(u); mail.Send(u); }
}
O

Open / Closed

Open for extension, closed for modification. New behaviour ≠ editing old code.

// ❌ Every new shape edits Area()
class AreaCalc {
    public double Area(object s) => s switch {
        Circle c    => Math.PI * c.R * c.R,
        Square q    => q.Side * q.Side,
        _           => throw new();
    };
}

// ✅ Add a Triangle without touching AreaCalc
interface IShape { double Area(); }
record Circle(double R)   : IShape { public double Area() => Math.PI * R * R; }
record Square(double Side): IShape { public double Area() => Side * Side; }
record Triangle(double B, double H) : IShape { public double Area() => B * H / 2; }
L

Liskov Substitution

Subtypes must be usable in place of their base type — without surprises.

// ❌ Square "is-a" Rectangle... but breaks callers
class Rectangle { public virtual int W { get; set; } public virtual int H { get; set; } }
class Square : Rectangle {
    public override int W { set { base.W = base.H = value; } }
    public override int H { set { base.W = base.H = value; } }
}
// Caller expecting w*h area is now broken.

// ✅ Don't fake hierarchies. Model the actual contract.
interface IShape { int Area(); }
record Rect(int W, int H) : IShape { public int Area() => W * H; }
record Sq(int Side)       : IShape { public int Area() => Side * Side; }
I

Interface Segregation

Many small interfaces beat one fat one. Don't force clients to depend on methods they never call.

// ❌ Read-only consumers must implement writes
interface IRepository<T> {
    T  Get(int id);
    void Add(T x);
    void Delete(int id);
}

// ✅ Split by capability
interface IReader<T> { T Get(int id); }
interface IWriter<T> { void Add(T x); void Delete(int id); }

class ProductCatalog(IReader<Product> reader) {
    // Cannot accidentally mutate
}
D

Dependency Inversion

High-level code depends on abstractions, not concrete classes. Inject, don't new.

// ❌ OrderService nailed to SqlOrderRepo + SmtpMailer
class OrderService {
    private readonly SqlOrderRepo _repo = new();
    private readonly SmtpMailer  _mail = new();
}

// ✅ Depend on interfaces; let the container wire it up
interface IOrderRepo { void Save(Order o); }
interface IMailer    { void Send(string to, string body); }

class OrderService(IOrderRepo repo, IMailer mail) {
    public void Place(Order o) { repo.Save(o); mail.Send(o.Email, "Thanks!"); }
}

// Program.cs
builder.Services.AddScoped<IOrderRepo, SqlOrderRepo>();
builder.Services.AddScoped<IMailer,    SmtpMailer>();

C# Design Patterns

The classic Gang-of-Four catalogue plus the enterprise patterns I use most. Each card shows the idiomatic modern C# version.

Creational

Singleton

One instance, shared globally. In .NET, prefer DI's AddSingleton over hand-rolled statics.

// Thread-safe via Lazy<T>
public sealed class Config {
    private static readonly Lazy<Config> _i = new(() => new Config());
    public static Config Instance => _i.Value;
    private Config() { /* load settings */ }
}

// ✅ The .NET way — let DI manage lifetime
builder.Services.AddSingleton<IConfigStore, ConfigStore>();
Creational

Factory Method

Defer "which concrete type" to a subclass or method. Decouples creation from use.

abstract class Document { public abstract void Render(); }
class PdfDoc  : Document { public override void Render() => Console.WriteLine("PDF"); }
class HtmlDoc : Document { public override void Render() => Console.WriteLine("HTML"); }

abstract class Exporter {
    public void Export() { var d = Create(); d.Render(); }
    protected abstract Document Create();
}
class PdfExporter  : Exporter { protected override Document Create() => new PdfDoc(); }
class HtmlExporter : Exporter { protected override Document Create() => new HtmlDoc(); }
Creational

Builder

Fluent step-by-step construction of complex objects. Think StringBuilder, HttpRequestBuilder, WebApplication.CreateBuilder.

public class EmailBuilder {
    private readonly Email _e = new();
    public EmailBuilder From(string a) { _e.From = a; return this; }
    public EmailBuilder To(string a)   { _e.To   = a; return this; }
    public EmailBuilder Subject(string s) { _e.Subject = s; return this; }
    public EmailBuilder Body(string b)    { _e.Body = b; return this; }
    public Email Build() => _e;
}

var mail = new EmailBuilder()
    .From("me@x.gr").To("you@x.gr")
    .Subject("Hi").Body("Hello!").Build();
Structural

Adapter

Wrap an incompatible interface so it fits the one your code expects. The "shim" pattern.

// Existing legacy logger
class LegacyLogger { public void WriteLine(string s) => Console.WriteLine(s); }

// What modern code wants
interface ILogger { void Info(string msg); }

// Adapter glues them
class LegacyLoggerAdapter(LegacyLogger inner) : ILogger {
    public void Info(string msg) => inner.WriteLine($"[INFO] {msg}");
}
Structural

Decorator

Add behaviour by wrapping, not inheriting. Perfect for logging, caching, retries on top of an interface.

interface IPriceService { decimal Get(int id); }

class PriceService : IPriceService {
    public decimal Get(int id) => /* DB call */ 9.99m;
}

class CachingPriceService(IPriceService inner, IMemoryCache cache) : IPriceService {
    public decimal Get(int id) =>
        cache.GetOrCreate(id, _ => inner.Get(id));
}

class LoggingPriceService(IPriceService inner, ILogger<LoggingPriceService> log) : IPriceService {
    public decimal Get(int id) {
        log.LogInformation("Get({Id})", id);
        return inner.Get(id);
    }
}
Behavioral

Strategy

Swap algorithms behind a common interface. Kills switch statements that grow every quarter.

interface IShippingStrategy { decimal Cost(Order o); }

class FlatRate : IShippingStrategy { public decimal Cost(Order o) => 5m; }
class ByWeight : IShippingStrategy { public decimal Cost(Order o) => o.KG * 1.5m; }
class Express  : IShippingStrategy { public decimal Cost(Order o) => 15m + o.KG; }

class Cart(IShippingStrategy shipping) {
    public decimal Total(Order o) => o.Subtotal + shipping.Cost(o);
}
Behavioral

Observer

Publishers fire events, subscribers react. Built into C# via event / IObservable<T>.

class Stock {
    public event Action<decimal>? PriceChanged;
    private decimal _p;
    public decimal Price {
        get => _p;
        set { _p = value; PriceChanged?.Invoke(value); }
    }
}

var s = new Stock();
s.PriceChanged += p => Console.WriteLine($"Alert! {p}");
s.PriceChanged += p => auditLog.Write(p);
s.Price = 42.5m;  // both subscribers fire
Behavioral

Command

Encapsulate a request as an object — perfect for queues, undo, audit logs. Basis of CQRS & MediatR.

interface ICommand { void Execute(); }

class SendEmailCmd(string to, string body) : ICommand {
    public void Execute() => Console.WriteLine($"-> {to}: {body}");
}

class CommandQueue {
    private readonly Queue<ICommand> _q = new();
    public void Enqueue(ICommand c) => _q.Enqueue(c);
    public void RunAll() { while (_q.TryDequeue(out var c)) c.Execute(); }
}
Behavioral

Mediator (MediatR)

Decouple senders from handlers via a central bus. The de-facto pattern for CQRS handlers in .NET.

// Request + handler
public record CreateOrder(int CustomerId) : IRequest<int>;

public class CreateOrderHandler(AppDb db) : IRequestHandler<CreateOrder, int> {
    public async Task<int> Handle(CreateOrder cmd, CancellationToken ct) {
        var o = new Order { CustomerId = cmd.CustomerId };
        db.Orders.Add(o);
        await db.SaveChangesAsync(ct);
        return o.Id;
    }
}

// Controller — no idea who handles it
app.MapPost("/orders", async (CreateOrder c, IMediator m) =>
    Results.Ok(await m.Send(c)));
Enterprise

Repository

An in-memory collection illusion over a database. Keeps query intent out of business logic.

public interface IProductRepository {
    Task<Product?> GetAsync(int id);
    Task<IReadOnlyList<Product>> ListActiveAsync();
    Task AddAsync(Product p);
}

public class EfProductRepository(AppDb db) : IProductRepository {
    public Task<Product?> GetAsync(int id) => db.Products.FindAsync(id).AsTask();
    public async Task<IReadOnlyList<Product>> ListActiveAsync() =>
        await db.Products.Where(p => p.IsActive).ToListAsync();
    public async Task AddAsync(Product p) => await db.Products.AddAsync(p);
}
Enterprise

Unit of Work

Group changes across repositories into a single atomic commit. EF Core's DbContext already is one.

public interface IUnitOfWork : IAsyncDisposable {
    IOrderRepository  Orders { get; }
    IProductRepository Products { get; }
    Task<int> SaveChangesAsync(CancellationToken ct = default);
}

public class EfUnitOfWork(AppDb db) : IUnitOfWork {
    public IOrderRepository  Orders   { get; } = new EfOrderRepository(db);
    public IProductRepository Products { get; } = new EfProductRepository(db);
    public Task<int> SaveChangesAsync(CancellationToken ct = default) =>
        db.SaveChangesAsync(ct);
    public ValueTask DisposeAsync() => db.DisposeAsync();
}
Enterprise

CQRS

Split writes (commands → domain) from reads (queries → optimised projections). Scale them independently.

// Commands change state, return ids/units
public record PlaceOrder(int CustomerId, List<int> Items) : IRequest<int>;

// Queries return DTOs, never touch domain entities directly
public record GetOrderSummary(int OrderId) : IRequest<OrderSummaryDto>;

public class GetOrderSummaryHandler(AppDb db)
    : IRequestHandler<GetOrderSummary, OrderSummaryDto>
{
    public Task<OrderSummaryDto> Handle(GetOrderSummary q, CancellationToken ct) =>
        db.Orders
          .Where(o => o.Id == q.OrderId)
          .Select(o => new OrderSummaryDto(o.Id, o.Total, o.Status))
          .FirstAsync(ct);
}
Enterprise

Specification

Encapsulate a business rule as an object you can combine, reuse, and test independently.

public interface ISpecification<T> {
    Expression<Func<T, bool>> ToExpression();
}

public class ActiveProductSpec : ISpecification<Product> {
    public Expression<Func<Product, bool>> ToExpression() => p => p.IsActive;
}

public class PriceAboveSpec(decimal min) : ISpecification<Product> {
    public Expression<Func<Product, bool>> ToExpression() => p => p.Price > min;
}

// Usage: combine specs, run on IQueryable
var hot = db.Products
    .Where(new ActiveProductSpec().ToExpression())
    .Where(new PriceAboveSpec(50).ToExpression())
    .ToListAsync();

Get in Touch

Open to collaborations, technical discussions, mentoring, and new opportunities.

sponaris@gmail.com  ·  Athens, Greece