On this page

Standard library

This reference defines the standard-library contracts and their relationship to the language. The API catalogue collects CLR declarations and typed source forms; the library plan explains responsibilities and implementation strategies.

The DirectSharp standard library supplies the coordination, collection, text and service abstractions used in everyday DirectSharp programs. Its public namespace is DirectSharp; its implementation is primarily ordinary C# and .NET. The same objects can cross a DirectSharp/C# boundary without an adapter language or a second object model.

Namespaces and runtime

DirectSharp is automatically available in ordinary source. Subnamespaces are imported explicitly. Importing the root does not import every optional package.

project
    root_namespace Acme.Orders
    import Global.System.Linq
end project

Application code uses normal CLR names such as Global.System.Collections.Generic.List <of Customer>. The Global. prefix starts at the actual CLR namespace root. A project can opt out of the automatic standard-library import; manual imports then work normally.

project
    auto_import_directsharp false
    import DirectSharp
end project

The setting disables only the automatic root import. The explicit manual import above remains effective; omission or explicit true enables automatic root import.

The planned DirectSharp.Runtime layer holds compiler support separately from the namespace application code normally writes. Optional copying, serialization, mapping and framework integrations can be separately packaged. The public namespace does not specify a NuGet package layout or target-framework version.

Surface Role Related language construct
Gate Shared, async-capable bounded coordination enter, concurrent limit:
IConsumable<T> and adapters Destructive/forward-only acquisition with completion consume
Sequence helpers Readable sequence access and selection Ordinary member calls
Chars, Unicode Named characters and explicit code-unit/scalar operations Strings and formatting
Service providers and scopes Composition, lookup and ownership needs, services, provide
Runtime support State and lowering support where ordinary CLR operations are insufficient Lazy publication, contracts and other compiler-selected operations

Gate

A Gate represents a fixed number of permits. With one permit it provides exclusive access; with several permits it bounds access to a shared resource.

let exclusiveGate = new Gate()
let databaseGate = new Gate(2)

These recorded construction forms mean one permit and two permits respectively. The form Gate(n, timeout) associates a default acquisition timeout with the Gate. Both count-taking constructors throw ArgumentOutOfRangeException for a nonpositive count. Gate implements IDisposable. Its acquisition methods are ordinary library members, not language keywords.

Use enter ... leave to acquire and release a permit structurally:

enter databaseGate
    await SaveInvoiceAsync(invoice)
leave

The scope releases an acquired permit on normal exit, return, exception or cancellation. A failed acquisition must not release a permit it never acquired. leave closes the region; it is not an ordinary Release() call scattered through business logic.

A shared Gate can bound several independent concurrent constructs:

concurrent for_each invoice in invoices with limit: databaseGate
    await SaveInvoiceAsync(invoice)
end for_each

An integer limit creates a limit private to that construct. A Gate limit participates in the same resource bound as other constructs using that object. A loop holds its permit until the entire iteration, including owned child operations, has finished.

Configuration What it bounds
new Gate(2) Simultaneous acquisitions of that object
enter gate with timeout: Global.System.TimeSpan.FromSeconds(2) Wait to acquire a permit
concurrent ... with limit: 4 Simultaneous child operations or whole loop iterations
concurrent ... with timeout: Global.System.TimeSpan.FromSeconds(30) Deadline for the whole concurrent operation

An acquisition timeout of TimeSpan.Zero means try once immediately. If a permit is unavailable, throw TimeoutException without running the body or releasing a permit. Timeout.InfiniteTimeSpan (minus one millisecond) means no acquisition deadline; all other negative values throw ArgumentOutOfRangeException. Positive durations bound the wait to acquire, not execution after acquisition. An explicit acquisition override replaces the Gate's default for that acquisition. Large valid durations require suitable runtime timer handling rather than a narrowing cast into milliseconds.

Gate is non-reentrant: every enter needs another permit. Entering the same one-permit Gate while its outer region still holds the permit waits and can time out or be cancelled. Without a timeout/cancellation it can stay blocked. A two-permit Gate can allow one nested acquisition when the second permit is free. Structure code around one acquisition for work that should share one permit; no recursive grant is implied.

Gate does not promise FIFO waiter ordering or fairness. Use its capacity bound rather than assumed wake-up order. Source-order result collection is a separate language contract. A whole-operation timeout requests cooperative cancellation where possible and never terminates a thread; acquisition and operation deadlines remain different.

For a live Gate and valid acquisition arguments, an already-cancelled caller fails before taking even a free permit or running the body. When a waiting acquisition fails from cancellation or timeout, the first cause recorded by that acquisition determines OperationCanceledException versus TimeoutException. Initially competing requests prefer caller cancellation; a later caller signal does not replace a recorded timeout.

Grant, caller cancellation and acquisition timeout compete through one atomically recorded acquisition outcome. A cause recorded first prevents entry; a grant recorded first permits the body to begin even if cancellation arrives just afterwards. Gate adds no second cancellation check before the body. Protected work can still respond to its caller token normally. If native capacity was obtained after a cause had committed, restore it without running the body. This acquisition boundary does not replace whole-concurrent-scope cancellation or result-publication rules.

Acquisition from C# and other CLR callers

The API catalogue records the selected public member signatures of DirectSharp.Gate:

Read the CLR declarations in the API catalogue.

An overload without timeout uses the Gate's configured default acquisition timeout. An explicit TimeSpan replaces that default for this acquisition. Synchronous acquisition waits through the calling thread; asynchronous acquisition returns a ValueTask to await. Both preserve the same timeout/cancellation arbitration and failure rules.

Every successful acquisition returns its own IDisposable permit. Disposing that permit releases its acquisition at most once; repeated disposal cannot add capacity. A failed acquisition returns no permit. The permit does not own or dispose the Gate, and there is no unpaired public Release method.

For example, this C# method borrows a Gate and invokes application-supplied work after successful acquisition. It assumes the same System/Threading/Tasks imports:

static async Task SaveBoundedAsync(DirectSharp.Gate gate,
    Func<CancellationToken, Task> saveAsync, CancellationToken cancellation)
{
    using (await gate.AcquireAsync(TimeSpan.FromSeconds(2), cancellation))
    {
        await saveAsync(cancellation);
    }
}

The using scope releases the permit after work completes or throws. It disposes no Gate. If acquisition fails, the work does not run and no permit is released. Passing cancellation to the application operation lets that operation cooperate; acquiring a permit adds no hidden post-grant cancellation check.

Owning a Gate

Gate implements IDisposable. The owner disposes it after all users finish. An enter/leave region or a supplied concurrent limit borrows the Gate and releases its acquired permits; it does not dispose the shared object.

using writes = new Gate(2)
    await SaveCopiesAsync(sourcePaths, destinationDirectory, writes)
end using

This fragment is in an explicit async action with the two inputs and the SaveCopiesAsync action from the concurrency guide. That action joins its concurrent work before returning, so the owning using scope can dispose writes afterwards. The called action borrows the supplied Gate.

Trying to dispose while acquisitions are pending or held throws InvalidOperationException and leaves the Gate usable. Existing holders/waiters can finish, then the owner can retry disposal. Successful disposal is idempotent; later acquisition throws ObjectDisposedException. Disposal does not cancel holders or synchronously wait for their work.

On .NET a Gate can use SemaphoreSlim or an equivalent async-capable primitive. The primitive stays private; no raw Native property is exposed. C#/.NET callers use Gate itself through its ordinary library API. It must preserve structured ownership and must not become C# lock(obj). Detailed language rules and lowering are in coordination and concurrency.

Consumable sources

IConsumable<T> is the ordinary CLR acquisition contract behind consume. A source acquires items destructively or advances a forward-only cursor. It reports permanent completion separately from temporary emptiness and supports cancellation or asynchronous waiting when the source requires them.

let pending = new Global.System.Collections.Generic.Queue <of String>()
pending.Enqueue("invoice-101")
pending.Enqueue("invoice-102")

consume invoiceId from pending
    Global.System.Console.WriteLine(invoiceId)
end consume

This processes the queue in dequeue order and empties it. Ordinary for_each enumeration is a different operation: it does not consume a source merely because that source implements IEnumerable<T>.

.NET source Acquisition/item Completion
Queue<T> Dequeued T, FIFO Empty ends the drain session
Stack<T> Popped T, LIFO Empty ends the drain session
ConcurrentQueue<T> Completion-aware wrapper; FIFO Producers explicitly completed and queue drained
ConcurrentStack<T> Completion-aware wrapper; LIFO Producers explicitly completed and stack drained
ConcurrentBag<T> Completion-aware wrapper; native unspecified order Producers explicitly completed and bag drained
BlockingCollection<T> Native blocking take/wait CompleteAdding and drained
ChannelReader<T> Native async read/wait Channel completed and drained
IDataReader / DbDataReader Current IDataRecord view after Read/ReadAsync False for the current result set
TextReader / StreamReader, explicit line adapter String line Native null/EOF; blank line is an item
Stream, explicit byte-chunk adapter Independent Byte[] chunk Zero-byte read for a positive requested size
Custom IConsumable<T> Its ordinary acquisition member Protocol reports permanent completion

An array, ordinary list or arbitrary enumerable is not a consumable source without an explicit adapter. The static Consumables factories provide explicit borrowed-source adapters; ILiveConsumable supplies coordinated Add/TryComplete for concurrent queue/stack/bag producer sessions. Factory and live-source declarations list their exact return types and validation. Known native consume sources still need no factory syntax; private registration metadata is compiler integration.

Acquisition and results

The library contract uses one awaitable operation. Queues can complete it synchronously; channels and other async-capable sources can wait normally:

Read the CLR declarations in the API catalogue.

The declarations use ordinary System/Threading/Tasks imports. HasItem distinguishes completion from an acquired payload. FromItem(null), zero and false remain items when their types permit them. Completed/default has no item; reading Item then throws InvalidOperationException. An empty live source waits. A source failure or cancelled native wait remains a failure rather than a fabricated completed result.

DirectSharp.IConcurrentConsumable<T> is the ordinary CLR marker for a custom consumable that supports concurrent bodies. It adds no acquisition member. Implementing it promises safe overlapping acquisition and validity of acquired items across subsequent acquisitions. It does not prove that an implementation keeps that promise or make mutable payload objects thread-safe. Compiler-known native adapters retain their established capability handling. Direct concurrent consumption of a raw database reader remains forbidden: serializing Read alone cannot stabilize its mutable current-row view.

Live concurrent collections

A bare concurrent queue, stack or bag cannot tell whether an empty moment is permanent. A live wrapper must connect producer arrival notifications and explicit completion to waiting acquisitions. Merely observing a thread-safe collection's emptiness is insufficient; arbitrary direct producer writes do not automatically signal a wrapper. Completion drains buffered items before ending the session.

An explicitly reported producer failure closes producer admission through the wrapper. Already-buffered items remain available in the collection's native order, with no new ordering guarantee for ConcurrentBag. After draining, acquisition propagates the original producer failure instead of reporting successful Completed. Later acquisitions retain and propagate that terminal failure. Consumables.FromConcurrentQueue/Stack/Bag return ILiveConsumable with Add/TryComplete. The first terminal transition wins; later calls return false, null failure is rejected before transition, and Add after closure throws without admission. The catalogue gives the exact declarations.

This failed-completion route is distinct from a cancelled acquisition wait or a failure in a consumer's body. Those events do not silently report producer failure or create an acknowledgement, retry or requeue protocol. A ChannelReader adapter retains its native channel fault behavior; the wrapper policy does not replace it.

Channels are built-in .NET producer/consumer sources with async waiting and explicit completion, so ChannelReader is a useful direct adapter target. BlockingCollection also has completion, but its native waits block; a reference async bridge may offload a blocking wait to a worker. That bridge is not a claim of native asynchronous I/O. Cancellation of an acquisition does not itself close or dispose the borrowed source.

Borrowing sources

Adapters borrow the underlying collection, reader or stream. Keep normal caller-owned using/disposal scopes. Completion, an early loop exit or failure does not silently dispose the source or its delivered item, cancel producers, acknowledge a message or requeue consumed data. Native read/wait exceptions propagate. The interface alone does not make arbitrary implementations safe for concurrent acquisition; appropriate adapter/source guarantees are still required.

Database readers

A consumed reader row is an IDataRecord view of the reader's current position:

using reader = command.ExecuteReader()
    consume row from reader
        let invoiceId = row.GetGuid(0)
        let amount = row.GetDecimal(1)
        RecordInvoice(invoiceId, amount)
    end consume
end using

Here command is an existing database command and RecordInvoice is application code. The row view is ephemeral. Advancing the reader changes its current record, so retaining row does not retain that row's values. Project required values into an independent object before storing them or processing them concurrently.

Consumption stops at the current result set. It never calls NextResult() implicitly. Multiple result sets remain explicit application logic. consume also does not transfer reader ownership; the surrounding using scope owns cleanup in this example.

Direct concurrent consumption of a raw IDataReader or DbDataReader is a compile-time error because there is one mutable cursor. Safe concurrent acquisition must be supplied by a suitable independent-value adapter or by another supported source.

Text lines and byte chunks

Use an explicit line adapter for TextReader/StreamReader. ReadLine's null result means EOF, while an empty String represents a real blank line. A custom source can still acquire an actual null item through FromItem; the line adapter translates its particular native EOF convention. Encoding and newline removal belong to the .NET reader.

A byte-chunk adapter works with ordinary Stream sources such as FileStream, NetworkStream or compatible compression streams. It requires a positive chunk size, keeps short positive reads as data and ends on a zero-byte read. Each returned Byte[] remains independent of later reads, including the final partial chunk. No seek/Length capability is required and no text encoding is guessed.

Both modes borrow the source and use native cancellable/async reads where available. A shared cursor still needs acquisition serialization; independent strings/chunks do not automatically make overlapping native reads safe. Direct .NET APIs remain available for more specialized needs. PipeReader additionally requires a precise buffer-lease/AdvanceTo contract; it is not interchangeable with a copied byte-chunk source. Existing enumerator cursors may have explicit adapters with their native lifetime/token rules, without making arbitrary IEnumerable automatically consumable.

Collections and sequence helpers

The strict/Try extensions are in DirectSharp.Collections.SequenceExtensions. Add the child namespace to the relevant file or existing project block:

import Global.DirectSharp.Collections

The automatic DirectSharp root import does not import it. Counts and inclusive positions use Integer/System.Int32. Existing LINQ keeps its contract; when both extension families are equally applicable, qualify the intended static method instead of relying on hidden precedence. The API catalogue lists all eight declarations.

DirectSharp uses ordinary CLR collections and .NET LINQ APIs. The library also defines readable sequence helpers. Method/property names are library symbols, not reserved language keywords.

Helper form Contract
items.First First item; nothing when empty
items.Last Last item; nothing when empty
items.Take(3) Exactly three leading items; throw if unavailable
items.TryTake(3) Up to three leading items collected at the call; nothing if none
items.TakeLast(3) Exactly three trailing items; throw if unavailable
items.TryTakeLast(3) Up to three trailing items collected at the call; nothing if none
items.Skip(5) Require five items to skip, then yield the remainder
items.TrySkip(5) Skip available items and collect the remainder; nothing if none
items.Take(5, 10) Positions five through ten, inclusive; positions are zero-based
items.TryTake(5, 10) Tolerant inclusive selection; normalize negatives to zero and clip oversized end

Collected Try results

Inclusive/count TryTake, TrySkip and TryTakeLast return nullable IReadOnlyList<T> in their public CLR contract. A present result exposes Count and index access to its collected items in source order. Nothing means that no items were selected or the source was missing. The helpers collect at the call under their individual traversal rules; later enumeration reuses those selected values without reading the source again.

The read-only interface supplies no mutation members through that interface. It does not promise deep immutability, freeze the selected objects or prevent a caller from casting to an implementation type. The concrete container remains an implementation detail. Strict deferred helpers retain their separate sequence contracts.

Nothing sources

DirectSharp's strict Take, Skip and TakeLast reject a nothing source immediately with ArgumentNullException, including the inclusive Take(start, end) overload. Check source presence before validating numeric arguments. A missing source therefore fails even for a zero request or invalid bounds, before any enumeration.

Their Try counterparts return actual nothing immediately for a nothing source, before numeric normalization or selection. This includes TryTake(start, end), TryTake(count), TrySkip(count) and TryTakeLast(count). A missing source does not acquire an enumerator, even with a zero request or invalid numeric arguments.

Source and request Strict helper Try counterpart
Nothing, positive count Immediate ArgumentNullException Nothing at the call
Nothing, zero count Immediate ArgumentNullException Nothing at the call
Nothing, negative count or invalid inclusive bounds Immediate ArgumentNullException, before bounds validation Nothing at the call, before normalization
Present source Existing strict bounds and execution rules Existing tolerant bounds and collected-selection rules

This policy concerns the selected DirectSharp helpers. It does not change imported LINQ contracts, add null-safe member access or catch source-reading exceptions. Coalescing can handle the actual nothing returned by a Try helper; it does not catch a strict helper's exception.

Inclusive selection

The two-argument DirectSharp Take(start, end) denotes inclusive positions. It is distinct from LINQ Take(count) and from the CLR overload accepting a Range. With sufficient source items, Take(5, 10) denotes six items.

DirectSharp's First/Last absence contract is also distinct from LINQ First()/Last(), which ordinarily throw for an empty sequence. Normal overload and extension lookup still applies. A method with a familiar name retains the contract of the symbol actually bound; imports must not silently alter the meaning of an already bound CLR call.

When paging is intended to clip a short final page, bind ordinary .NET LINQ helpers, whose contracts remain unchanged. The familiar composition is:

let page = customers.Skip(20).Take(10)

The strict two-position helper returns a deferred streaming IEnumerable<T>. A negative start, negative end or end below start throws ArgumentOutOfRangeException at the call, before enumeration. A source too short for the full interval also throws ArgumentOutOfRangeException, when enumeration discovers the shortage. This includes a start beyond the source or an empty source. A selected prefix can be delivered before that later error; stopping early does not check the remaining source.

For seven items numbered zero through six:

Call Result
items.Take(-3, 2) ArgumentOutOfRangeException at the call
items.Take(10, 5) ArgumentOutOfRangeException at the call
items.Take(5, 10) 5, 6, then ArgumentOutOfRangeException if enumeration continues
items.Take(20, 30) ArgumentOutOfRangeException during enumeration
items.TryTake(-3, 2) 0, 1, 2
items.TryTake(-3, -1) 0: both negative positions normalize to zero
items.TryTake(5, 10) 5, 6
items.TryTake(10, 5) nothing without source traversal
items.TryTake(20, 30) nothing using available length or one traversal

Take captures its source and positions without enumerating them. Each enumeration obtains a fresh source enumerator and rechecks the source available then. No Count pass or implicit snapshot is supplied. It stops after the inclusive end and disposes its enumerator on completion, failure or early consumer exit. A maximum Integer end does not introduce position overflow. Ordinary source exceptions retain their original meaning.

Choose strict or tolerant bounds

Take(start, end) requires the complete interval. TryTake(start, end) normalizes each negative position to zero and clips an oversized end to the last available item. An adjusted end below start returns nothing without source traversal. For other intervals, TryTake reads the source once at the call, only as far as the requested inclusive end or source exhaustion, and collects the available selected items. No enumerating Count pass or repeated source enumeration is needed. For arrays and ordinary lists, available Length/Count plus indexed copying can avoid traversing skipped items; sources without a suitable fast path use one bounded forward pass. If there are no selected items, including a start beyond the source or an empty source, return nothing. Otherwise return the collected selection. Reusing that result does not reread the source or observe later changes to the source's membership; selected object references are not deep copies.

let window = items.TryTake(start, finish) ?? throw

This fragment assumes a present items sequence and Integer start/finish. The short ?? throw uses the normal contextual exception rule: an absent method-call result throws NullReferenceException. To select an argument exception explicitly, write ?? throw new Global.System.ArgumentOutOfRangeException("interval"). A custom message alone retains the contextual category.

TryTake disposes its source enumerator before returning. Reading, acquisition or cleanup failures propagate at the call; an argument exception from the source is not mistaken for invalid selection bounds. Coalescing handles actual absence after a successful call and does not catch those failures. Strict Take remains deferred; tolerant TryTake collects its requested window at the call. Both helpers are ordinary C#-implemented library APIs, with no new keyword or Boolean throwing flag.

Require a non-empty selection

Coalescing checks absence, not sequence contents, and does not catch a later enumeration failure. Strict Take enforces its own bounds. For a general pipeline that can legitimately yield no items, use a separate non-empty contract. This example uses the ordinary LINQ import and materializes once so the postcondition and caller share one snapshot:

public action RequiredItems(
    items as Global.System.Collections.Generic.IEnumerable <of Integer>
)
    returns Global.System.Collections.Generic.List <of Integer>
    ensure result is not empty
    return items.ToList()
end action

Alternatively, turn a failed non-empty test into absence explicitly and use the existing conditional/coalescing expressions:

let window = items.ToList()
let requiredWindow = if(window is not empty, window) ?? throw new Global.System.InvalidOperationException("The selected interval must contain an item")

Here items is an input in the enclosing action. The two-argument if returns nothing when the condition is false; ?? throw then checks that actual absence. This is a content requirement independent of strict or tolerant positional selection.

The established helpers do not add start..end, from-end indexes or new slicing grammar. These selection contracts do not assign receiver/return types or error behavior to every similarly named helper. Chapter 15 gives the two-position rules and a reference C# pattern.

Count-taking strict and tolerant pairs

DirectSharp's count-taking Take, Skip and TakeLast reject a negative count immediately with ArgumentOutOfRangeException. They also require the requested count to exist. For seven items, Take(10), Skip(10) and TakeLast(10) fail rather than clipping. Strict source work is deferred: Take can deliver a prefix before discovering a shortage; Skip fails before yielding a remainder; TakeLast must reach the end before producing its tail on a general enumerable. Source failures retain their own meaning.

Zero is valid: Take(0)/TakeLast(0) return present empty, and Skip(0) yields all items. Skip(7) over seven items succeeds with a present empty result. Ordinary LINQ calls with those names keep their separate native contracts; no implicit preference or new overload priority is supplied.

The Try counterparts normalize negative counts to zero and collect at the call. TryTake collects only the available prefix, TryTakeLast keeps the available tail, and TrySkip collects the remaining items after skipping. Any of these Try helpers returns actual nothing when no items are selected. Thus TryTake(0), TryTakeLast(-2) and TrySkip(10) over seven items return nothing, while TryTake(10)/TryTakeLast(10) return all seven.

let tail = items.TryTakeLast(3) ?? throw
let remainder = items.TrySkip(5) ?? throw

These fragments assume a present items sequence. The existing short throw handles an absent result. TryTakeLast and TrySkip must reach the source end when no suitable metadata path exists. TrySkip stores the remainder, so large results need memory and an unbounded source cannot return. Neither helper makes a counting prepass, rereads its source when its collected result is reused, or suppresses source failures. Zero-count TryTake/TryTakeLast need not read any source items. Safe available Count/Length optimizations remain allowed; custom enumerator behavior cannot be ignored merely because Count exists.

The count-taking specification gives precise boundaries, execution and C# traversal patterns.

Indexed access

Required indexed access preserves failures. An invalid array/list position or missing map key is not converted to absence automatically. Use try_get when absence is the intended result:

let region = try_get regions["MK"] ?? "Unknown"

map[key] ??= value has assignment intent: it inserts a missing key or fills a present nothing. It leaves any other stored value intact. This ordinary update is not atomic. Indexed lookup gives the evaluation and failure rules.

Exchanging writable values

The ordinary helper swap(a, b) exchanges two writable locations of the same effective type. Each location expression is evaluated once, in written order. Readonly bindings, iteration bindings and literal values are invalid targets.

let current = "Draft"
let previous = "Published"
swap(current, previous)
// current is "Published"; previous is "Draft"

The exchange is generic over the value type, including compatible CLR references and ordinary structs; it is not restricted to the types supported by Interlocked. Both targets still need the same effective type and writable storage. Normal CLR representation restrictions apply.

atomic swap(a, b) requests protected exchange. An ordinary swap in a concurrent body automatically receives the same protection; inside an atomic block it participates in that region. The compiler supplies synchronization compatible with regions protecting the same shared state. Related pair reads and competing mutations need compatible protection; unrelated CLR accesses are not automatically coordinated.

C# library helpers can implement addressable storage exchange using generic by-reference parameters. Readable/writable properties and custom indexers use synchronous getter/setter adapters under the same stable shared synchronization domain. Resolve receivers/index arguments once; under protection, read both values in written order, then write right to left and left to right. A setter exception may leave partial changes; no rollback is implied. A fresh lock per invocation does not coordinate calls.

The compiler still handles locations, lexical concurrent context and domain selection. These source forms do not establish a final exported C# signature. The specification provides illustrative by-reference and accessor lowerings.

Text and Unicode

Chars supplies named text/control values, including NewLine, CR, LF, CRLF, Tab, Null, Escape, Backspace, FormFeed, VerticalTab, Space, SingleQuote, DoubleQuote and Backslash.

The fixed single-unit Chars values have Char type. NewLine and CRLF have String type. Chars.NewLine follows the runtime platform. CR, LF and CRLF identify specific values independently of the platform. Backslash is literal in DirectSharp strings; named constants make control characters visible without C# escape notation.

let heading = "Invoice" + Chars.Tab + "Amount"
let report = heading + Chars.NewLine + "101" + Chars.Tab + "125.00"
Chars value Type UTF-16 value
CR Char U+000D
LF Char U+000A
Tab Char U+0009
Null Char U+0000, not nothing
Escape Char U+001B
Backspace Char U+0008
FormFeed Char U+000C
VerticalTab Char U+000B
Space Char U+0020
SingleQuote Char U+0027
DoubleQuote Char U+0022
Backslash Char U+005C
CRLF String U+000D followed by U+000A
NewLine String Environment.NewLine

Unicode supplies String values such as NBSP, ZeroWidthSpace, BOM, Bullet, Ellipsis, LineSeparator, ParagraphSeparator and ReplacementCharacter.

Unicode value Scalar in its String
NBSP U+00A0
ZeroWidthSpace U+200B
BOM U+FEFF
Bullet U+2022
Ellipsis U+2026
LineSeparator U+2028
ParagraphSeparator U+2029
ReplacementCharacter U+FFFD

The conversion helpers distinguish UTF-16 code units from Unicode code points:

Recorded helper Purpose
Chars.FromCodeUnit(value) Long input; Char for 0–65535; out-of-range input throws ArgumentOutOfRangeException
Chars.CodeUnit(value) Integer value of a Char, including surrogate units
Unicode.FromCodePoint(value) Long input; String for a valid scalar; invalid input throws ArgumentOutOfRangeException
Unicode.CodePoint(value) Integer value of exactly one scalar in the entire String; otherwise ArgumentException
Unicode.CodePoints(text) Traverse scalars; malformed UTF-16 throws ArgumentException

A CLR Char holds one UTF-16 code unit. A supplementary Unicode character requires a surrogate pair; it does not fit in one Char. These helpers are ordinary C#-implemented APIs. The code-unit helper accepts isolated surrogate units because they are valid individual UTF-16 units. The scalar helpers reject them: valid scalars are 0–0x10FFFF excluding 0xD800–0xDFFF. They do not silently insert replacement text. U+0000 and Unicode noncharacters are still valid scalars; application validation is a separate policy.

let face = Unicode.FromCodePoint(0x1F600)
let scalar = Unicode.CodePoint(face)

FromCodeUnit and FromCodePoint accept Long; an Integer argument widens normally. Validate the full Long before narrowing so a large invalid input cannot wrap into a valid code unit/scalar. CodeUnit and CodePoint return Integer; every valid result fits, and callers can widen normally to Long.

Face holds 😀 and scalar is 0x1F600. CodePoint requires exactly one scalar, so "AB", empty text and malformed surrogate text throw ArgumentException. Nothing throws ArgumentNullException. A composed-looking sequence such as e plus a combining accent contains two scalars and fails the single-scalar check; scalar count is different from grapheme count.

CodePoints preserves scalar order and combines valid surrogate pairs. Empty text has no scalars; nothing throws ArgumentNullException. An isolated high/low surrogate produces ArgumentException when traversal reaches it, with no replacement or continuation. Ordinary String storage and Char iteration preserve the original units. The specification defines these boundaries and a Rune decoding pattern. CodePoints returns a deferred streaming IEnumerable <of Integer>. It rejects nothing at the call; malformed text throws during enumeration. Each enumeration traverses afresh, and a valid prefix can be observed before a malformed suffix fails. Early exit does not decode that suffix. ToList/ToArray explicitly materializes. The two construction helpers accept Long and validate before narrowing; their inverse helpers return Integer. Root static Chars/Unicode expose fixed named const fields, a platform NewLine getter and the five selected static conversion members. Package layout remains separate.

Casing, URI components and escaped contents

These text operations are extension members of the public static class DirectSharp.Text.TextExtensions. Import Global.DirectSharp.Text explicitly; the automatic root import does not include the child namespace. Wrap widths and Truncate/Ellipsis lengths use Integer, with no implicit Long narrowing. All fifteen signatures are ordinary CLR library declarations.

Upper/Lower use invariant .NET casing, so the host's current culture does not change their result. Empty stays empty and missing source throws ArgumentNullException. Identifier-style CamelCase/PascalCase/SnakeCase/KebabCase recognize case transitions and acronyms: HTTPServer2ID becomes httpServer2Id or http_server2_id. They strictly reject malformed UTF-16, use runtime Unicode categories and invariant simple casing, and apply no normalization. These transformations do not change ordinary case-insensitive language comparison or source identifier rules.

UrlEncode encodes one URI component: a b/c becomes a%20b%2Fc. A previously encoded a%20b becomes a%2520b, because the percent sign is input data. Use an ordinary form-content API for form encoding and a URI builder when assembling a complete address. Missing source throws ArgumentNullException; empty stays empty and other behavior follows Uri.EscapeDataString.

JsonEscape/CSharpEscape return escaped contents without enclosing quotes; they do not serialize an object. Missing source throws ArgumentNullException and empty remains empty. Reject malformed UTF-16, escape quote/backslash/control characters and preserve other readable Unicode. U+007F–009F and the Unicode line/paragraph separators use explicit Unicode escapes. These are JSON/ordinary C# string contents; they do not make text safe for inline HTML or define other C# literal forms. The API catalogue gives the exact escape ranges and export boundaries.

HtmlEncode guards missing input, then uses WebUtility.HtmlEncode for HTML text or quoted attribute data. XmlEncode guards missing input, then writes an XML element-text fragment through a checked, declaration-free XmlWriter with newline entitization. It preserves carriage returns through an XML reader and rejects invalid XML characters/UTF-16. Empty stays empty in both helpers, and an existing entity is encoded again. For XML attributes or documents use the writer's structural methods. Encoding examples show the different destinations and exact C# strategies.

Shape a preview or wrap a description

Wrap/Truncate/Ellipsis count runtime Unicode text elements, preserving a recognized emoji or combining sequence as a unit. They do not measure display columns or change native String indexing. Truncation budgets include the suffix: Ellipsis length 4 turns ABCDE into ABC…. A suffix longer than the budget is itself clipped; an already-fitting source is unchanged. Plain Truncate uses no suffix, and a zero-length truncation is valid and empty.

Wrap preserves existing CRLF/CR/LF and inserts platform newlines when wrapping. It greedily packs words, collapses ASCII spaces/tabs between words and splits oversized words at text-element boundaries. Other whitespace, including NBSP, remains word content. Missing source/suffix, negative truncation length, nonpositive wrap width and malformed UTF-16 fail explicitly. Entire source and supplied suffix are validated even if some text would be discarded. See layout examples and validation order.

Formatting and conversion

Interpolation evaluates expressions once, in source order. nothing contributes empty text; evaluation failures propagate. Normal formatting uses current culture.

let summary = "Invoice {{invoice.Number}}: {{invoice.Total:N2}}"

Binary buffers support hex, hex_upper, base64, utf8, ascii and ascii_dots presentation. ascii_dots retains printable ASCII and renders non-printable bytes as .. Formatting an arbitrary Stream never implicitly reads or consumes it; materialize the required bytes first.

Formatting helpers can use ordinary CLR formatting where it matches the language contract. JSON serialization, escaping and diagnostics are distinct operations. Explicit culture or serialization policy can always be expressed through ordinary .NET APIs.

Round/Truncate/Floor/Ceiling have selected Decimal and Double member families, preserving the argument's numeric type. Round without an explicit mode uses AwayFromZero, with zero digits when omitted. Thus Decimal 2.5 rounds to 3 and Double -2.5 rounds to -3. Explicit MidpointRounding selects the native mode. Digits use Integer and the native Decimal 0–28/Double 0–15 ranges. Imported Math.Round retains its native defaults. See all twelve member fragments and binding limits; the containing class is root DirectSharp.Numbers. Numbers.Round(amount, 2) uses ordinary type/member lookup. The recorded bare helper calls still need their compiler binding account; root namespace import is not an implicit static import.

Services and scopes

ServiceProvider, ServiceScope, ServiceScopeFactory, ServiceRegistry and MainServiceProvider form the runtime service model. They work with normal .NET dependency injection rather than creating a second container requirement.

Operation Meaning
Provider CreateScope() Create an independent disposable scope
Scope-factory Create() Create a scope through a narrow borrowed capability
GetService <of T>() Required singular lookup; missing registration throws DirectSharp.ServiceNotFoundException
GetAllServices <of T>() Resolve all matching services at the call; return IReadOnlyList, present empty when no matches
HasService <of T>() Inspect registration availability without activating a service where introspection is supported
MainServiceProvider.Set(provider) Appoint an existing provider without transferring ownership

Lookup signatures and failures

ServiceScope and root-safe ServiceProvider lookup expose these selected ordinary CLR member signatures. The catalogue shows contracts without supplying an implementation or selecting new generic constraints:

Read the CLR declarations in the API catalogue.

Keyed calls overload the existing names. For example, scope.GetService <of ILogger>("audit") performs required lookup for that exact keyed identity. Keyed GetService, GetAllServices and HasService reject a null key with ArgumentNullException before lookup or activation. Use the no-key overload for unkeyed lookup. A statically known invalid service-key declaration is diagnosed rather than silently becoming unkeyed. This adds no language keywords or new key grammar and preserves other exact typed key values.

An unregistered required singular service throws the dedicated DirectSharp.ServiceNotFoundException, derived from InvalidOperationException. Its read-only ServiceType property has type System.Type; its read-only ServiceKey property has type object? in C#, with null identifying unkeyed lookup. Callers can catch the specific exception or its standard invalid-operation base and inspect the requested identity without parsing Message. The two identity-bearing constructors retain type/key and optional message/inner details. Null serviceType fails; null constructor key means unkeyed metadata. No identity-less or formatter serialization constructor is supplied; diagnostics must not dump unrelated runtime values.

Constructor/activation errors, a disposed provider and prohibited root lookup are different failures; they must not be mislabeled as a missing registration. Direct Microsoft DI calls retain their own native contracts, including optional lookup returning null.

Plural activation and borrowed results

GetAllServices resolves the entire matching collection at the call and returns IReadOnlyList<T>, present empty when there are no matches. Reusing this result reuses the same resolved instances; enumeration does not create fresh transient services. Different calls still follow the selected registration lifetimes, so later calls can resolve new transient instances. No new registration-order rule is implied by the indexed result interface.

An activation failure propagates at the call without returning a successful partial collection. Already-created services retain their existing provider/scope ownership. There is no implied rollback, immediate disposal or ownership transfer caused by that failure. A collected list neither makes services immutable nor extends a scope's lifetime; consumers keep services borrowed and must not dispose them.

Service identity includes the service type and exact typed key; String keys are case-sensitive. This is distinct from case-insensitive language identifiers. A root provider exposes direct retrieval only for singleton/root-safe services; scope-bound retrieval uses an explicit scope. Root safety applies before activating a prohibited graph. HasService describes registration availability, so it can report a scoped registration even when root retrieval is prohibited.

Providers/scopes own services they create. needs values and lookup results are borrowed; consumers must not dispose them. Nested scopes have independent scoped instances. A singleton graph may not reach a scoped dependency, including through a transient intermediary.

A services declaration defines composition. Its Create operation builds an independent provider; Register contributes to an existing host registry without creating a second provider. No ambient current scope exists. Libraries do not appoint a main provider merely by declaring a composition module.

new T(arguments) from scope constructs a fresh caller-owned object and supplies missing needs from that scope. It is not a lookup of T's registration. Explicit with needs override scope-supplied needs; borrowed dependencies remain scope-owned. See services and .NET.

Compiler support and optional libraries

The compiler-support plan describes private lowering support. Application code should use the public contracts above, not depend on generated helper names.

Core copy_of is a shallow-copy operation. Deep copying belongs to ordinary libraries with explicit policy for identity, cycles, resources and shared objects. It is not a universal built-in operation. FluentValidation, EF Core, Dapper, logging and other .NET packages remain directly usable without a DirectSharp feature adapter.