On this page

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