On this page

Coming from C# or VB.NET

DirectSharp keeps ordinary .NET types and libraries while making application intent visible through words, named blocks and explicit contracts.

Bring your knowledge of .NET classes, interfaces, generics, delegates and libraries. DirectSharp uses those same contracts. Learn the source conventions first, then pay particular attention to names, defaults and asynchronous ownership: familiar-looking code can express a different policy.

This page uses the Customer application. Its Customer has Id, Name, Email and IsActive properties. Registration validates input and creates an inactive customer. Snippets below are independent comparisons or explicitly identified fragments, not extra declarations to paste into that application.

action / constructor / self: translate familiar .NET structure

C# VB.NET DirectSharp
Method returning a value Function action with returns
Method returning void Sub action without returns
this Me self
Constructor named after its class Sub New constructor … end constructor
List<T> List(Of T) List <of T>
if and braces If … End If if … then … end if
foreach For Each for_each … end for_each
null Nothing nothing for actual absence
using / await using Using; explicit async cleanup where needed using selects cleanup from the static contract and execution context

An action's returns clause describes its logical result. An async action returning String exposes a task-returning CLR operation, not a synchronous String method. Interfaces, overrides and imported APIs retain their fixed signatures.

Imports bring names into scope; project references supply assemblies and NuGet dependencies. Importing FluentValidation does not install it. DirectSharp's automatic import covers the DirectSharp root namespace, not every child namespace or the whole .NET framework. Use Global.System when an absolute namespace root matters.

require / ensure / contextual if: make the rule visible

This pricing action uses the walkthrough Customer's IsActive property. An active customer receives ten percent off; everyone else pays the original amount. The input and output must both be nonnegative.

public static action PriceAfterDiscount(customer as Customer, amount as Decimal)
    returns Decimal
    require amount >= 0
    ensure result >= 0

    if customer is not nothing and .IsActive then
        return amount * 0.90
    end if
    return amount
end action

The entry requirement and exit guarantee are visible before the calculation. Inside the condition, .IsActive refers to the customer already being checked. The ensure covers both returns; it does not need to be repeated on each branch. No custom error text is needed to restate either condition.

A C# version makes the same policy explicit by sharing the final result check:

public static decimal PriceAfterDiscount(Customer? customer, decimal amount)
{
    if (amount < 0)
        throw new ArgumentException("Requirement failed: amount >= 0", nameof(amount));

    decimal result;
    if (customer is not null && customer.IsActive)
        result = amount * 0.90m;
    else
        result = amount;

    if (result < 0)
        throw new InvalidOperationException("Postcondition failed: result >= 0");
    return result;
}

The C# checks are ordinary application code. DirectSharp expresses them as contracts, so adding another normal return does not require remembering to copy the output check. Its condition also reads without repeating the receiver, and end if/end action identify the blocks being closed. This is the reading difference to look for in larger examples—not a promise that every small operation needs fewer lines.

nothing / is empty: normalize a display label

These standalone declarations implement the same small display policy. An absent or whitespace-only name is shown as “Unnamed customer”; other names are trimmed. They do not validate registration or modify a Customer.

DirectSharp:

public class CustomerLabels
    public static action Display(name as String)
        returns String
        if name is nothing then
            return "Unnamed customer"
        end if
        let label = name.Trim()
        if label is empty then
            return "Unnamed customer"
        end if
        return label
    end action
end class

C#:

public static class CustomerLabels
{
    public static string Display(string? name)
    {
        if (string.IsNullOrWhiteSpace(name)) return "Unnamed customer";
        return name.Trim();
    }
}

VB.NET:

Public Class CustomerLabels
    Public Shared Function Display(name As String) As String
        If String.IsNullOrWhiteSpace(name) Then Return "Unnamed customer"
        Return name.Trim()
    End Function
End Class

All three return “Ana” for “ Ana ” and the fallback for absence or spaces. DirectSharp checks absence before calling Trim, then asks whether the normalized label is empty. The same policy uses String.IsNullOrWhiteSpace in the .NET examples; that API call is not a DirectSharp predicate. Named endings expose the decision and action boundaries.

self / arguments: qualify case-insensitive names

A rose is a Rose is a ROSE. Like VB.NET, DirectSharp binds identifiers without case distinctions. Unlike C#, changing capitalization cannot select between a parameter and a member.

This DirectSharp action replaces a Customer's existing Name after checking its input. Put it inside the walkthrough's Customer only if you choose to add that operation:

public action Rename(name as String)
    require arguments.name is not nothing
    require arguments.name.Trim() is not empty
    self.Name = arguments.name.Trim()
end action

The explicit arguments.name selects the input; self.Name selects the property. Bare name would be ambiguous here. This is static name binding, not a runtime argument dictionary. A port that uses name and Name to distinguish symbols needs qualification, not different capitalization. Imported CLR members that collide after case folding can also require a diagnostic.

Name identity does not determine data identity. A Dictionary constructed with StringComparer.Ordinal still distinguishes the keys “Ana” and “ana”. Authentication subject identifiers, service keys and protocol values retain their own comparison policies. See names and collisions.

default / nothing / equality: check semantic differences

These fragments assume the named application values are in scope. The results concern DirectSharp-authored expressions; imported APIs retain their documented contracts.

Question Example Result or consequence
Does case select a different local? Declare customer, then try to declare CUSTOMER in the same scope Duplicate-name diagnostic
Does identifier casing rewrite stored data? Read an Ordinal dictionary with a differently cased key The dictionary's comparer decides; the language does not rewrite the key
Is default the same as nothing? let label as String = default Empty String; an explicit nothing remains absent
Does default construct an ordinary class? let customer as Customer = default nothing, unless a visible custom default supplies something else
Is String equality the C# operator? "Ana" = "ana" true under DirectSharp's ordinal case-insensitive String comparison
Does an imported comparison change too? Global.System.String.Equals("Ana", "ana", Global.System.StringComparison.Ordinal) false; explicit .NET contract
Is readonly deep immutability? let readonly customer = existingCustomer; then assign customer.Name Allowed for a present object with a writable property; rebinding customer is forbidden
Does a nullable operand mutate into its default? Add an absent Nullable <of Decimal> to a Decimal in a supported authored arithmetic operation Uses the effective numeric default locally; the stored operand is still absent
Does try_get mean only “key missing”? try_get customers[index] Normally catchable receiver/index/accessor failures become nothing; inspect LastAttempt or choose a narrower API

VB.NET's Nothing can also denote a value type's default in a typed context. Do not mechanically substitute it for DirectSharp nothing. C# default(string) is null, while DirectSharp's effective String default is empty. Targetless default is invalid: the compiler needs a type. See defaults and CLR boundaries.

Ordinary C# String equality is case-sensitive. VB.NET String comparison depends on its applicable comparison rules, including Option Compare. When a rule must match across all three languages, state the comparer explicitly. For credentials or protocol data, choose the required security/protocol comparison rather than assuming a language default is appropriate.

RuleFor(.Name): pass a selector to FluentValidation

The CustomerValidator constructor contains this DirectSharp rule:

RuleFor(.Name).NotEmpty().Length(2, 100)

The corresponding C# and VB.NET constructor statements are:

RuleFor(customer => customer.Name).NotEmpty().Length(2, 100);
RuleFor(Function(customer) customer.Name).NotEmpty().Length(2, 100)

These are FluentValidation calls in a validator inheriting AbstractValidator<Customer>, with its namespace imported and package referenced. The selector targets the library's expression-tree parameter; it is not the current Name value. The same DirectSharp receiver expression can target a delegate when that is what a library requests. A query provider still decides which expression trees it can translate.

The optional Validator class kind reuses the base/interface header. It adds no runtime base class and no validation language. Follow ordinary validation, then the optional kind.

await / task_of / run: choose asynchronous intent

Ordinary awaits are explicit. The compiler can infer async implementation for an ordinary action that awaits, including a public action. Here is a complete DirectSharp declaration; it has no implicit cancellation parameter:

public class CustomerNameFiles
    public static action ReadAsync(path as String)
        returns String
        return await Global.System.IO.File.ReadAllTextAsync(path)
    end action
end class

The corresponding task-returning methods in C# and VB.NET are:

public static class CustomerNameFiles
{
    public static async System.Threading.Tasks.Task<string> ReadAsync(string path)
    {
        return await System.IO.File.ReadAllTextAsync(path);
    }
}
Public Class CustomerNameFiles
    Public Shared Async Function ReadAsync(path As String) As System.Threading.Tasks.Task(Of String)
        Return Await System.IO.File.ReadAllTextAsync(path)
    End Function
End Class

In DirectSharp, explicit async action fixes an asynchronous contract and supplies its standard cancellation context. Inference alone supplies neither a token parameter nor Cancellation. This distinction matters in public libraries: changing an inferred action's body can change its emitted return contract. Fixed interface/override signatures and synchronous boundaries remain enforced; the compiler does not invent async void.

Inside an ordinary action, with a supplied path:

let pending = task_of(Global.System.IO.File.ReadAllTextAsync(path))
try
    let name = await pending
    Global.System.Console.WriteLine(name)
catch error as Global.System.Exception
    Global.System.Console.WriteLine(error.Message)
end try

Task_of invokes once and keeps the actual operation. Await observes its result or stored failure. The handler above protects the await; a synchronous failure obtaining the operation occurs before try, so move the capture inside try if it needs the same handler. Capturing does not schedule a thread or automatically join work. Native Task/ValueTask consumption rules remain in force.

DirectSharp form Intent
let name = await CustomerNameFiles.ReadAsync(path) Invoke, wait, then use the String
let pending = task_of(CustomerNameFiles.ReadAsync(path)) Invoke and retain the task; someone must observe it
let other = pending Copy an existing task value; no fresh invocation
let pending = CustomerNameFiles.ReadAsync(path) Compile-time error: choose await, task_of or an owned run
run inside concurrent all/first/any Register owned work; scope policy controls joining and result publication

The same fresh-invocation rule applies in arguments, returns and discarded statements, even when a Task target type is expected. The compiler warns about locally detectable forgotten captures; it cannot prove observation through every external library.

Two successive awaits remain sequential. Task_of can retain independently started operations, but concurrent scopes are the usual application choice when all work must settle before leaving. Start with the dashboard; then read first/any selection and failure rules. Ordinary await retains .NET continuation behavior. ConfigureAwait(false) is a library opt-out from context capture, not UI dispatch or a request for a background thread.

using / dispose: let scopes perform cleanup

Use using for an owned resource. In an already asynchronous context, a statically available IAsyncDisposable contract is preferred; a synchronous context uses IDisposable when available. Async-only cleanup can require permitted async inference. Dual-interface cleanup alone does not make an otherwise synchronous action asynchronous. A static Object view cannot discover a hidden disposable interface dynamically.

There is no separate await using spelling. Selection happens from the static contract and execution context, and the generated cleanup waits before control leaves. Constructors, synchronous callbacks and other fixed synchronous boundaries cannot silently block on async-only disposal. Imported objects retain native lifecycle behavior; DirectSharp-authored dispose/async dispose declarations have their own shared lifecycle rules. See the selection table.

Dispose what this operation owns. The report worker owns its DI scope; its renderer borrows shared Gates and a scoped source. Leaving a concurrent scope joins owned work before a containing resource scope can end. Capturing an unowned task does not create that protection.

Take / TryTake: choose strict or tolerant selection

DirectSharp's opt-in DirectSharp.Collections.SequenceExtensions supplies the strict/Try helpers; import Global.DirectSharp.Collections or qualify the static class. Counts/positions are Integer. If LINQ extensions are equally applicable, the compiler diagnoses ambiguity rather than giving either family priority. The library Take and TryTake contracts differ from System.Linq.Enumerable.Take. With items containing integers 0 through 6:

DirectSharp library call When work happens Result
items.Take(5, 10) Deferred traversal Can yield 5 and 6, then throws ArgumentOutOfRangeException when traversal discovers shortage
items.TryTake(5, 10) One traversal at the call Collected 5, 6; reuse does not reread the source
items.TryTake(20, 30) At the call, using available metadata or traversal Actual nothing because nothing was selected
items.Take(-1, 2) Numeric validation at the call ArgumentOutOfRangeException without source traversal

Both positions are inclusive. Try normalizes negative positions to zero; an adjusted reversed interval returns nothing. Source reading and cleanup failures still propagate. A collected result preserves references to its elements, not deep copies. Count-taking Take, Skip and TakeLast have their own strict/Try contracts. Explicit imported LINQ calls keep LINQ's behavior; a migration must choose the intended operation rather than merely copying its name. See sequence contracts.

with name / break / continue: target an enclosing loop

C# break and continue act on the innermost loop. DirectSharp retains those short forms and lets a loop carry an explicit name. This fragment skips the remaining orders for an invalid customer order, or stops customer processing on a critical order:

for_each customer in customers with name: customers_loop
    for_each order in customer.Orders
        if order.IsInvalid then
            continue customers_loop
        end if
        if order.IsCritical then
            break customers_loop
        end if
        ProcessOrder(order)
    end for_each
end for_each

An illustrative C# implementation uses explicit destinations to express the same outer-loop transfers:

foreach (var customer in customers)
{
    foreach (var order in customer.Orders)
    {
        if (order.IsInvalid)
            goto nextCustomer;
        if (order.IsCritical)
            goto customersFinished;
        ProcessOrder(order);
    }
nextCustomer:
    ; // Continue through the outer foreach acquisition.
}
customersFinished:
    ;

The application supplies customers and ProcessOrder. Native foreach cleanup occurs when control leaves its enumeration scope; a translation must preserve it. A while target must resume its condition, and a post-test repeat target must resume its trailing test. Moving a continue destination to the start of an arbitrary body can change behavior.

DirectSharp also provides general statement destinations: mark finished corresponds to a C# label such as finished:, and goto finished supports forward or backward transfer. Mark names and loop names have different roles. Neither form relaxes definite assignment, resource cleanup, callable boundaries or concurrent iteration boundaries. See named loops and marks.

Continue with the application

Follow Customer registration through HTTP, then try the guided exercises and answers. For equivalent complete tasks in several languages, use the comparison pages. For precise diagnostics and C# lowering, use the reference.