On this page

A tour of DirectSharp

Read the code first, then follow the features that interest you. This tour moves from a customer operation to contracts, objects, patterns, concurrency and resource ownership. Each section is a short introduction with a link to the details.

The opening example uses the Customer application's model and service. Later examples are independent declarations or action fragments; their inputs and application helpers come from the surrounding application.

action / returns: follow a customer operation

A DirectSharp action is an operation with typed inputs and an optional result. Here an application creates a customer, applies business rules and looks the customer up again:

import System

public class Program
    static action Run()
        returns String
        let service = new CustomerService()
        let customer = new Customer(1, "Ana", "ana@example.com")
        service.Register(customer)
        customer.ApproveLimit(250)
        customer.Rename("Ana Petrova")
        let found = service.FindByEmail("ANA@EXAMPLE.COM")
        require found is not nothing
        ensure result = "Ana Petrova: 1 customer"
        let summary = found.Name + ": " + service.Count() + " customer"
        Console.WriteLine(summary)
        return summary
    end action
end class

Expected output:

Ana Petrova: 1 customer

The model stores customer state; the service registers customers and finds them by email. require checks entry conditions, ensure checks successful completion, and ordinary .NET collections hold the data. Identifiers and language-level String equality ignore case. The rule is visible in the lookup for "ANA@EXAMPLE.COM".

require / ensure / select: express a delivery policy

public class DeliveryQuote
    public static action Calculate(orderTotal as Decimal, destination as String)
        returns Decimal
        require orderTotal >= 0
        require destination
        ensure result >= 0

        if orderTotal >= 100 then
            return 0
        end if

        return select destination
            case "MK"
                take 3
            case "DE", "AT"
                take 8
            else
                take 15
        end select
    end action
end class

A bare String requirement checks presence and nonemptiness; a numeric requirement states its condition. Standard exceptions and messages are supplied automatically. ensure covers every normal exit, including the early return. Selection evaluates its subject once and uses the first matching case; take supplies a value to the selection, while return completes the action.

Contracts and errors · Conditions and selection

action calls: reuse and check a policy

let local = DeliveryQuote.Calculate(75, "mk")       // 3: String matching ignores case.
let international = DeliveryQuote.Calculate(75, "FR") // 15
let free = DeliveryQuote.Calculate(100, "FR")       // 0: the early-return guarantee still applies.

let / default / nothing: make values explicit

let quantity = 3                     // Infer Integer.
let total as Decimal = default       // The effective numeric default is zero.
let title as String = default        // The effective String default is empty.
let customer as Customer = nothing   // Actual absence.
let displayName = customer.Name ?? "Guest"

Property reads propagate absence through missing receivers; ?? supplies a fallback only for actual nothing. Defaults and absence remain different. Identifiers ignore case too: quantity and Quantity name the same local. Named endings such as end if and end action identify the block being closed.

Values, defaults and absence

if / contextual dot: follow one subject

if customer is not nothing and .IsActive and .CreditLimit >= 500 then
    SendOffer(customer)
end if

The leading dot continues from customer without repeating its name. Short-circuit evaluation protects later tests. Inside a library selector, dot instead denotes the selector's typed parameter: customers.Where(.IsActive) is an ordinary LINQ call with its namespace imported.

Conditions · Selectors and callables

constructor / with properties / property: keep object rules together

public class ReportOptions
    public constructor()
        with properties
            readonly Title as String
            IncludeDetails as Boolean = true
        end properties
        require self.Title
    end constructor

    public property Heading as String
        get
            return "{{Title}} report"
        end get
    end property
end class

let options = new ReportOptions() with
    title: "Daily orders"
end with

with properties declares stored members and promotes them into constructor inputs. The named with supplies those inputs before construction; it is not a sequence of later setter calls. Heading is a computed read-only property. {{Title}} interpolates the value into text.

Objects and constructor inputs · Properties and initialization

needs: declare injected dependencies

import Global.Microsoft.Extensions.Logging.*

public class ReportHandler
    needs
        logger as ILogger <of ReportHandler>
    end needs

    public action Handle(title as String)
        require title
        logger.LogInformation("Preparing {Title}", title)
    end action
end class

The caller or service provider must supply the logger. It is a stable dependency across constructors and remains borrowed, rather than becoming owned by the handler. ILogger and LogInformation are ordinary .NET logging APIs; {Title} here is the logging library's structured message placeholder.

Dependencies and service scopes

data / of: carry typed messages and collections

public data ReportRequest
    Title as String
    IncludeDetails as Boolean = true
end data

public class Batch <of T>
    public Items as new Global.System.Collections.Generic.List <of T>
end class

let request = new ReportRequest("Daily orders")
let batch = new Batch <of ReportRequest>()
batch.Items.Add(request)

Data members are initialized during construction and have record value semantics. The generic Batch keeps its actual item type: this batch accepts ReportRequest, rather than untyped Object values. A read-only data shape does not freeze mutable objects it references.

Data and classes · Generics

use / use_if: group operations on a receiver

use options
    .IncludeDetails = false
    Global.System.Console.WriteLine(.Heading)
end use

use_if customer.Account.Owner
    .Notify()
end use_if

use captures one receiver and requires it to be present. use_if skips the body when any qualification segment is absent; getter and body exceptions still propagate. Neither form disposes the receiver. For a shallow copy instead, use use copy_of source.

Receiver scopes

is a / is like / is match: ask readable questions

if customer is a PremiumCustomer then
    customer.ApplyVipRules()  // Runtime type refinement in this branch.
end if

if filename is like "report-{{let period = *}}.csv" then
    Global.System.Console.WriteLine("Report period: {{period}}")
end if

if invoiceReference is match "^INV-(?<invoiceNumber>[0-9]+)$" then
    Global.System.Console.WriteLine("Invoice number: {{invoiceNumber}}")
end if

is a tests type compatibility. is like uses wildcard patterns; is match uses regular expressions. Known named captures belong to the successful branch. Ordinary text matching ignores case; explicit case-sensitive forms are available when a protocol requires them.

Types and refinement · Patterns and captures

lazy: compute only when a value is needed

let index = lazy BuildIndex(orders)
if needsLookup then
    Show(index)  // The first value use builds the index; later uses share it.
end if

The whole initializer waits for its first ordinary use. It does not start a background task or require .Value. Successful values and initialization failures are cached, including under concurrent first reads.

Lazy values

for_each / consume: visit items or acquire work

for_each order in orders
    for_each line in order.Lines
        PrintLine(line)
    end for_each
end for_each

consume job from pendingJobs
    Process(job)
end consume

Enumeration visits a sequence; consumption removes queued items or advances a supported forward-only source. Here pendingJobs is a Queue supplied by the application; it is empty after normal consumption. A supported live source can wait for arrivals until permanent completion. Named endings make the nested loop boundaries visible.

Loops and numeric ranges · Consumable sources

try / try_get / ??: make best-effort work explicit

let port = try_get Global.System.Int32.Parse(portText) ?? 443
try RecordPreviewOpened()  // Optional telemetry must not prevent the preview.

A normally catchable parse failure becomes nothing, so the fallback supplies 443. Blockless try suppresses normally catchable statement failures and continues without a value. Use a typed catch when only a particular failure is acceptable. LastAttempt retains the most recently completed attempt's exception metadata in this block.

Best-effort acquisition

try / catch / retry: make another attempt deliberately

try
    SendReport()
catch Global.System.TimeoutException
    if LastAttempt.Number < 3 then
        retry
    end if
    throw
finally
    RecordDeliveryFinished()
end try

This permits three attempts on timeout and rethrows the final failure. Other failures propagate normally. Retry restarts the protected body; it does not run this finally between attempts or add backoff. Finally runs when the try is actually left. The application must make retrying SendReport safe for its delivery contract.

Exception handling and retry

await / task_of: distinguish a result from an operation

let pending = task_of(client.GetStringAsync(summaryAddress))
PrepareLayout()
let summary = await pending

task_of invokes once and retains the operation. await observes its result or failure. An ordinary action containing await can infer its asynchronous implementation; async action explicitly fixes an asynchronous contract and supplies the standard cancellation context.

Async intent and cancellation

concurrent all: complete independent work together

concurrent all
    returns summary as String, alerts as String
    summary = run client.GetStringAsync(summaryAddress)
    alerts = run client.GetStringAsync(alertsAddress)
end concurrent
ShowDashboard(summary, alerts)

Both operations can run together. Their results are published after the owned work settles, and success requires both. A run can also contain several application statements. These snippets belong in actions with a borrowed HttpClient and supplied addresses.

Concurrent scopes and run bodies

concurrent first: choose the first successful answer

concurrent first
    returns body as String
    body = run client.GetStringAsync(primaryAddress)
    body = run client.GetStringAsync(backupAddress)
end concurrent
ShowDocument(body)

The first successful producer wins; a fast failure does not beat a later success. The scope requests cooperative cancellation of unfinished competitors where supported and joins them before leaving. concurrent any instead selects the first completion, including a failure.

Winner selection, cancellation and joining

concurrent for_each / Gate: bound work and protect a shared step

using writes = new Gate(2)
    concurrent for_each item in items with limit: 4
        let output = await BuildOutputAsync(item)
        enter writes
            await StoreOutputAsync(output)
        leave
    end for_each
end using

Up to four iterations run together; only two enter this write step at once. The Gate permit is released even when work fails. The loop joins its work before the owning using disposes the Gate. Share one Gate between operations when they must share the same capacity.

Concurrent loops and Gates

using / dispose / unmanaged: make ownership visible

using reader = new Global.System.IO.StreamReader(path)
    ProcessText(reader.ReadToEnd())
end using

The reader is cleaned up on normal completion, return and exception. In an asynchronous context, using prefers a statically available async disposal contract and awaits it automatically. let using can instead own a resource until the containing scope ends.

A class owning raw native memory can declare its cleanup explicitly:

public class NativeBuffer
    private address as Global.System.IntPtr = Global.System.IntPtr.Zero

    public constructor(byteCount as Integer)
        require byteCount > 0
        address = Global.System.Runtime.InteropServices.Marshal.AllocHGlobal(byteCount)
    end constructor

    dispose
        unmanaged
            if address <> Global.System.IntPtr.Zero then
                Global.System.Runtime.InteropServices.Marshal.FreeHGlobal(address)
                address = Global.System.IntPtr.Zero
            end if
        end unmanaged
    end dispose
end class

Ordinary statements in dispose perform managed cleanup; the unmanaged section also supplies the synchronous finalizer fallback. There is no separately authored destructor. A class can additionally declare async dispose for asynchronous managed cleanup. Prefer ordinary .NET safe-handle abstractions when they already express the required native ownership.

Resource lifetime and authored cleanup

class kind: name a reusable class header

import Global.FluentValidation.*

public class kind Validator <of T>
    exposes IValidator <of T>
    inherits AbstractValidator <of T>
end class kind

public class ReportRequestValidator is Validator <of ReportRequest>
    public constructor()
        RuleFor(.Title).NotEmpty().Length(2, 100)
    end constructor
end class

The kind reuses an inheritance/interface header without adding another runtime base class. RuleFor and its selector remain ordinary FluentValidation API calls; the kind introduces no validation DSL.

Classes and class kinds

wrap: apply a reusable source transformation

An application can attach a known wrapper to a suitable declaration:

wrap cached with engine: scriban

Here cached is an available application wrapper, not a built-in cache policy. A wrapper transforms DirectSharp into DirectSharp; the expanded program retains ordinary typing, contracts and resource rules. Most reuse still belongs in normal actions, classes and libraries.

Extensions, features and wrappers

project / import / Global: bring names into scope

project
    root_namespace DeliveryQuotes
    import Global.System.*
end project

The single project declaration supplies a root namespace and shared imports. File-level imports apply to their own file; Global. starts at the actual .NET namespace root. DirectSharp uses ordinary .NET classes, interfaces and libraries.

Continue with the language guide for chapter-by-chapter explanations, the Customer walkthrough for connected application code, or the reference for exact semantics.

Next: Values and operators