C# and DirectSharp for APIs and domain models
Read the aggregate invariant, then trace its HTTP and persistence boundary. DirectSharp should expose that business intent while C# remains free to use its normal framework patterns. See readability under real business rules for policy branches, initialization and coordination.
Consider an order service: adding lines changes the total, submitting an empty order fails, and submitted orders cannot change. An HTTP endpoint exposes the resulting order. Both versions use ordinary CLR classes, Decimal money and ASP.NET Core responses.
These are two different responsibilities. The domain object enforces its rules for every caller; the HTTP handler translates a lookup into a response. Moving the rule into a controller would leave background jobs and other callers able to bypass it.
An aggregate with protected state
This small PurchaseOrder is an aggregate root: callers change it through AddLine and Submit rather than writing Total or IsSubmitted. It omits line identities, persistence and domain events so the invariant is visible.
DirectSharp
// Application boundary: distinguish domain decisions from HTTP and persistence plumbing.
import System
public class PurchaseOrder
readonly Id as Integer
Total as Decimal with private set = 0
IsSubmitted as Boolean with private set = false
constructor(id as Integer)
require arguments.id > 0 otherwise new ArgumentOutOfRangeException("id")
self.Id = arguments.id
end constructor
action AddLine(quantity as Integer, unitPrice as Decimal)
require quantity > 0 otherwise new ArgumentOutOfRangeException("quantity")
require unitPrice > 0 otherwise new ArgumentOutOfRangeException("unitPrice")
require not IsSubmitted otherwise "A submitted order cannot be changed"
Total += quantity * unitPrice
end action
action Submit()
require not IsSubmitted otherwise "Order is already submitted"
require Total > 0 otherwise "An empty order cannot be submitted"
IsSubmitted = true
end action
end class
C#
// Application boundary: distinguish domain decisions from HTTP and persistence plumbing.
using System;
public class PurchaseOrder
{
public int Id { get; }
public decimal Total { get; private set; }
public bool IsSubmitted { get; private set; }
public PurchaseOrder(int id)
{
if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id));
Id = id;
}
public void AddLine(int quantity, decimal unitPrice)
{
if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
if (unitPrice <= 0) throw new ArgumentOutOfRangeException(nameof(unitPrice));
if (IsSubmitted) throw new InvalidOperationException("A submitted order cannot be changed");
Total += quantity * unitPrice;
}
public void Submit()
{
if (IsSubmitted) throw new InvalidOperationException("Order is already submitted");
if (Total <= 0) throw new InvalidOperationException("An empty order cannot be submitted");
IsSubmitted = true;
}
}
Both reject invalid quantities/prices before mutation, reject submission of an empty order, and reject edits or repeated submission afterward. Decimal overflow propagates and leaves Total unchanged. DirectSharp's contracts express those guards; C# uses explicit checks. A CLR caller sees normal properties and methods in either case.
The constructor also exposes a naming difference: DirectSharp uses arguments.id and self.Id to select input versus property explicitly. C# can distinguish id from Id by capitalization; DirectSharp treats those spellings as the same name.
DDD is an approach to modeling a domain, not a language keyword. Bounded contexts, aggregate boundaries, repository interfaces and transaction policies still need application design. Neither version supplies event sourcing, an outbox or transactional persistence by declaring a class.
An ordinary ASP.NET Core response
The host supplies an IReadOnlyDictionary of orders. An invalid numeric ID returns 400, an absent ID returns 404, and an existing ID returns 200 with the order serialized using ASP.NET Core's normal JSON settings. Missing host dependencies are programming errors, not a misleading 404.
DirectSharp
// Application boundary: distinguish domain decisions from HTTP and persistence plumbing.
import System
import System.Collections.Generic
import Microsoft.AspNetCore.Http
public class OrderApi
static action Find(id as Integer, orders as IReadOnlyDictionary <of Integer, PurchaseOrder>)
returns IResult
require orders is not nothing otherwise new ArgumentNullException("orders")
if id <= 0 then
return Results.BadRequest("Order ID must be positive")
end if
if not orders.ContainsKey(id) then
return Results.NotFound()
end if
return Results.Ok(orders[id])
end action
end class
C#
// Application boundary: distinguish domain decisions from HTTP and persistence plumbing.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
public static class OrderApi
{
public static IResult Find(int id, IReadOnlyDictionary<int, PurchaseOrder> orders)
{
ArgumentNullException.ThrowIfNull(orders);
if (id <= 0) return Results.BadRequest("Order ID must be positive");
if (!orders.ContainsKey(id)) return Results.NotFound();
return Results.Ok(orders[id]);
}
}
IResult, Results, IReadOnlyDictionary and their generic types are existing .NET APIs. DirectSharp does not need an HTTP feature adapter. Both handlers can use the same ASP.NET Core host registration: app.MapGet("/orders/{id:int}", (int id) => OrderApi.Find(id, orders));. The route's int constraint handles text that cannot bind to an integer before this handler runs.
The small endpoint serializes the aggregate to keep this boundary visible. A larger API can map to a separate response DTO when its wire contract must evolve independently of the domain model. A database-backed operation also needs real asynchronous I/O and cancellation; see async and resources.
Both implementations return ordinary ASP.NET Core IResult values with the same status codes and JSON. Host registration above supplies the application dependencies; the domain rule stays independent of HTTP.
Choosing between them
| Concern | DirectSharp | C# |
|---|---|---|
| Reading a domain rule | Named blocks, case-insensitive names and entry contracts | Familiar brace syntax, explicit checks and established conventions |
| Domain isolation | Ordinary classes/interfaces; domain logic need not depend on HTTP | The same CLR architecture and separation are available |
| API ecosystem | Uses normal ASP.NET Core types | Ordinary ASP.NET Core types and libraries |
| Validation | Native contracts or ordinary FluentValidation calls | Explicit guards or the same FluentValidation APIs |
| Persistence | Normal .NET repositories and libraries | Ordinary EF Core and Dapper integrations |
For an established C# team, syntax alone is not a migration plan. Keep useful libraries and wire contracts, try a bounded application module, and test its public CLR boundary. DirectSharp's proposed advantage is easier inspection of business code, including code generated by an agent; it does not change the meaning of DDD or make an API secure automatically.
Connected Customer HTTP/database walkthrough · Other workload choices · All comparisons