On this page

Extending and adapting an application

Most reusable application code belongs in actions, classes, interfaces and libraries. Extensions add useful calls to existing types. Attributes describe framework metadata. Typed features adapt a library's awkward call shape, and wrappers transform selected source at compilation. Each mechanism has a different boundary.

extension: add an operation to an existing type

A business-day helper can belong beside the application rather than inside the .NET DateOnly type.

extension Global.System.DateOnly
    public action IsBusinessDay()
        returns Boolean
        return self.DayOfWeek <> Global.System.DayOfWeek.Saturday and
            self.DayOfWeek <> Global.System.DayOfWeek.Sunday
    end action
end extension

public action FirstBusinessDate(start as Global.System.DateOnly)
    returns Global.System.DateOnly
    let candidate = start
    while not candidate.IsBusinessDay()
        candidate = candidate.AddDays(1)
    end while
    return candidate
end action

This helper handles weekends. It deliberately does not claim to handle regional public holidays; that policy could be supplied by an ordinary calendar service. The extension reads like a member call while leaving the original CLR type unchanged.

self denotes the receiver. Extensions cannot bypass its access rules. Generic extensions use ordinary generic types and constraints. Core extensions provide methods; they do not invent extension properties or add stored state to imported types.

attributes / description: document a declaration

Libraries such as serializers interpret ordinary .NET attributes. DirectSharp places them after the declaration they describe.

public class ExportCustomer
    description "A customer record written to an external JSON report."

    public Id as Global.System.Guid
        description "Stable customer identifier."

    public DisplayName as String
        attributes Global.System.Text.Json.Serialization.JsonPropertyNameAttribute("display_name")
        description "Name shown in the receiving application."
end class

public action SerializeCustomer(customer as ExportCustomer)
    returns String
    return Global.System.Text.Json.JsonSerializer.Serialize(customer)
end action

The JSON attribute changes the serializer's property-name mapping. It does not change DirectSharp identifier comparison or make the property a special language construct.

A structured description is documentation rather than a runtime attribute. It can become the summary shown by an editor or documentation generator. Use ordinary comments for a local computation; use /// for richer documentation when needed.

Parameter and return attributes use with attributes after the complete declaration. C# bracket shorthand is accepted on parameters for interoperability, while classes/actions/properties retain DirectSharp's declaration-following form.

feature: adapt a repeated library vocabulary

A FluentValidation validator is an ordinary class using RuleFor, NotEmpty and other library methods. A repository calls EF Core or Dapper directly. Logging uses its normal .NET API. These are useful library names; they are not reasons to add validation, SQL or logging grammar.

A feature is appropriate when repeated framework composition has a genuinely awkward shape: a fluent pipeline, expression-capturing query or configurable endpoint-registration sequence. The feature remains a typed adapter over existing APIs.

feature query
    target Global.System.Linq.IQueryable <of T>

    operation where(predicate as captured expression)
        calls Global.System.Linq.Queryable.Where
        sets current from result
    end operation
end feature

The operation tells the compiler which member to bind and how its result updates the current pipeline value. Generic inference and overload resolution remain ordinary binding. The captured predicate is supplied in the representation the target API expects, such as an expression tree for Queryable.

When an operation changes the current value's type, subsequent leading-dot access follows that new type. When two subjects are active, named contexts keep them separate. A feature block that accepts application code still contains ordinary DirectSharp with its usual contracts, scopes and control flow.

Feature vocabulary is available through known source and references. Referencing the appropriate package makes its declarations available; no separate project activation list is needed. Mentioning an unavailable feature does not download its framework. Conflicting available feature names produce a diagnostic rather than hidden reference-order precedence.

A feature cannot generate arbitrary classes or change core semantics. Use it for typed API composition and keep domain behavior in normal application declarations.

wrap: transform a selected declaration

A wrapper applies a restricted compile-time transformation to an existing declaration or region. Its engine receives target metadata and original source fragments and emits expanded DirectSharp. That expanded source is then parsed, bound and checked like ordinary code.

An application can select an available engine explicitly:

wrap cached with engine: scriban

This line belongs to a suitable declaration and assumes a referenced cached wrapper and available Scriban engine. A project may supply a default to avoid repeating the selection:

project
    default_wrapper_engine scriban
end project

The engine is a tooling capability. The language does not require Scriban, and the engine does not emit C# directly as its final language result.

A useful timing wrapper can expand into ordinary DirectSharp cleanup:

public action MeasureReport(path as String)
    let timer = Global.System.Diagnostics.Stopwatch.StartNew()
    try
        let contents = Global.System.IO.File.ReadAllText(path)
        Global.System.Console.WriteLine("Report has {{contents.Length}} characters")
    finally
        timer.Stop()
        Global.System.Console.WriteLine("Read completed in {{timer.ElapsedMilliseconds}} ms")
    end try
end action

This complete ordinary action shows the semantics a timing transformation should preserve. The finally records time even when file reading throws. Simply appending a logging statement after a body would not guarantee that behavior.

Wrappers apply in deterministic written order. Generated names must avoid collisions. Templates receive read-only restricted metadata rather than unrestricted compiler/system access. Inspecting expanded DirectSharp is valuable because that program determines the actual scopes, returns and cleanup behavior.

block wrap selects a region. A transformation using its body can omit the original generic block; one using its full source can retain that lexical scope. Wrapping does not imply a runtime interceptor, a proceed call or an automatic callback after every possible exit.

#define / #if / #include: select source before parsing

Conditional compilation can include or exclude declarations before parsing. Definitions are typed compiler values, not automatic bare-name text macros.

#define INCLUDE_DIAGNOSTICS = true

#if __compiler__.define("INCLUDE_DIAGNOSTICS")
public action DescribeExport(path as String)
    Global.System.Console.WriteLine("Export destination: {{path}}")
end action
#else
public action DescribeExport(path as String)
end action
#endif

#ifdef tests whether a definition exists, including one whose value is false. Live conflicts are diagnosed; project/local definition order does not silently choose a winner.

Use #define override to deliberately replace an existing visible definition; a conflicting ordinary #define remains an error. Include paths accept either String quote form:

#define MODE = "production"
#define override MODE = 'test'
#include "shared/contracts.inc"
#include 'shared/helpers.inc'

The named fragments must exist within the configured source boundaries. Includes use paths relative to the including file; replacing a local definition does not modify persistent project/solution configuration.

Text substitution is explicit: ${__compiler__.define("NAME")}. It has deterministic scalar formatting and is distinct from runtime interpolation. Include fragments use .inc, process at their inclusion point and preserve source locations. Inclusion cycles and paths escaping configured source boundaries are errors.

Prefer runtime configuration for choices that should vary after deployment. Preprocessing selects source at compilation; it does not become a general-purpose macro language or a hidden package-management system.

For the detailed contracts, see extensions, features, wrappers and preprocessing.

Practice: extensions, features and wrappers

Review the export feature claims. Explain which behavior comes from the feature and which comes from the ordinary library calls.

Previous: Application design · Next: Application examples