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.