Programming with .NET
DirectSharp uses normal .NET types, libraries and application frameworks. A validator can inherit FluentValidation's AbstractValidator<Customer>, a repository can call Dapper, and an ASP.NET Core host can consume the application's CLR contracts.
The useful boundary is between application logic and framework responsibilities. Keep business rules in ordinary classes and actions. Let the framework own hosting, dependency scopes, database connections and transport behavior where it already provides those services.
Names, imports and dependencies
import makes namespaces available for binding. It does not download a package or load an assembly at runtime. Add external dependencies through the surrounding .NET project and reference the application assembly from its host.
import Global.System
import Global.System.Linq
import Global.FluentValidation
public class CustomerQueries
public static action ActiveNames(customers as Global.System.Collections.Generic.IEnumerable <of Customer>)
returns Global.System.Collections.Generic.List <of String>
require customers is not nothing
return customers.Where(.IsActive).OrderBy(.Name).Select(.Name).ToList()
end action
end class
Global roots a name at the CLR namespace root, even when the application has a project namespace. Import System.Linq to resolve its extension methods. Qualifying an external class does not independently import the namespace containing extensions for it.
DirectSharp names are case-insensitive. Imported APIs retain their recognizable spelling in source and emitted metadata. If imported members collide under case-insensitive lookup, casing cannot select one; resolve the ambiguity explicitly through a noncolliding API surface or adapter.
Delegates, expression trees and LINQ
A selector such as .Name takes its meaning from the target parameter:
customers.OrderBy(.Name)
validator.RuleFor(.Email)
OrderBy over an IEnumerable uses a delegate. RuleFor expects an expression tree that the validation library inspects. Queryable operators likewise require expression trees, which a database provider may translate. The compiler does not turn an unrepresentable lambda into a delegate just to make the call fit.
Ordinary .NET execution rules still matter. Enumerable.Where is deferred; ToList materializes it. IQueryable.Where builds a provider expression; its query executes through the provider when enumerated or materialized. An operation that is valid in memory is not automatically translatable to SQL.
Use named-parameter selectors when an implicit receiver would obscure the expression:
customers.Where(customer: customer.IsActive)
validator.RuleFor(customer: customer.Email)
For substantial logic, prefer an ordinary named action. Whether a provider can translate that action is a separate question; an arbitrary application action is not silently inlined into SQL.
FluentValidation
The customer example defines a model, a normal validator class, and a caller that inspects IsValid and Errors. RuleFor, NotEmpty, Length and EmailAddress are library methods, not language keywords.
Configure rules once in the validator's constructor and invoke them at a deliberate application boundary. Expected user correction should return validation failures. Contracts protect programmer assumptions; they should not be the normal mechanism for rendering a form's field errors.
A reusable class kind can contribute AbstractValidator<T> inheritance and expose IValidator<T> when several validators share that shape. It is optional: a single validator is already an ordinary class with a clear header.
If validation performs asynchronous I/O, call the library's ValidateAsync API and pass Cancellation.Token from an explicit async action. Do not run asynchronous rules through a synchronous Validate call. Select the validator lifetime according to its dependencies; a singleton validator must not capture a request-scoped DbContext.
Dapper
Dapper is useful when the application should own its SQL explicitly. The customer repository passes a CommandDefinition containing parameter values and a cancellation token, then calls QuerySingleOrDefaultAsync or ExecuteAsync.
Keep SQL parameters separate from command text. Parameterization handles values; it does not make arbitrary user-selected table or column names safe. If a report needs dynamic sort columns, choose from an application allowlist rather than interpolating input into SQL.
Own the connection for the entire operation, including result materialization. Do not return a live reader or deferred sequence from a scope that has already disposed its connection. For multi-statement changes, choose a transaction explicitly and pass it to every command participating in it.
The repository wraps DbException in a specific application infrastructure exception and lets cancellation propagate. That preserves the distinction between an absent row, a storage failure and a cancelled request.
Entity Framework Core
EF Core is a useful alternative when the application benefits from change tracking, entity relationships and provider-translated queries. It still uses ordinary classes and typed selectors.
The following context uses the same Customer model and SQL Server table as the Dapper example. Reference Microsoft.EntityFrameworkCore and the chosen provider, such as Microsoft.EntityFrameworkCore.SqlServer.
import Global.Microsoft.EntityFrameworkCore
public class ApplicationDbContext
inherits DbContext
public constructor(options as DbContextOptions <of ApplicationDbContext>)
calls base(options)
end constructor
public property Customers as DbSet <of Customer>
get
return Set <of Customer>()
end get
end property
protected action OnModelCreating(builder as ModelBuilder)
overrides base.OnModelCreating
call base.OnModelCreating(builder)
builder.Entity <of Customer>().ToTable("Customers", "dbo")
builder.Entity <of Customer>().HasKey(.Id)
builder.Entity <of Customer>().Property(.Name).IsRequired().HasMaxLength(100)
builder.Entity <of Customer>().Property(.Email).IsRequired().HasMaxLength(320)
end action
end class
The override relationship is explicit. The separate call base.OnModelCreating invokes the base implementation. The context's options come from the host; the model does not create its own provider or open a hidden connection in a constructor.
A service borrows the context for one scope:
import Global.System.Collections.Generic
import Global.System.Linq
import Global.Microsoft.EntityFrameworkCore
public class CustomerReadService
private readonly db as ApplicationDbContext
public constructor(context as ApplicationDbContext)
require context is not nothing
db = context
end constructor
public async action ActiveAsync()
returns List <of Customer>
Cancellation.ThrowIfCanceled()
return await db.Customers.AsNoTracking().Where(.IsActive).OrderBy(.Name).ToListAsync(Cancellation.Token)
end action
end class
AsNoTracking is appropriate for this read-only result. The database provider determines SQL translation and collation; source case-insensitivity does not override SQL ordering or string comparison. Pass the token explicitly to ToListAsync.
DbContext is not thread-safe. Do not start sibling concurrent operations on the same context. Sequential actions may share a request-scoped context; independent concurrent database operations need independent contexts and an application policy for consistency between their results.
A C# ASP.NET Core host can register the ordinary context and service:
using Microsoft.EntityFrameworkCore;
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddScoped<CustomerReadService>();
The container disposes the scoped context. CustomerReadService should not dispose that borrowed dependency. Choose Dapper and EF Core according to query/control and modeling needs; both can coexist, but shared transactions and connections require explicit coordination.
Async APIs and cancellation
An explicit async action's declared result is its logical value. Its normal .NET contract returns Task<T>, with a final optional CancellationToken parameter. Imported methods keep names such as FindAsync and SaveChangesAsync.
An ordinary fresh awaitable call must explicitly await its result, capture its operation with task_of, or launch it through run in a concurrent scope. A bare call is diagnosed even in task-typed storage or arguments; expected type does not choose intent. Capture preserves the actual Task/ValueTask shape. See operation capture for overload rules and native task combinators. Ordinary public actions may infer async implementation, changing their emitted return type as required. Optional explicit async fixes the asynchronous contract; interface/base contracts retain their declared signatures.
import Global.System.Net.Http
public class RemoteDocumentClient
private readonly http as HttpClient
public constructor(client as HttpClient)
require client is not nothing
http = client
end constructor
public async action DownloadAsync(uri as String)
returns String
require uri is not whitespace
Cancellation.ThrowIfCanceled()
return await http.GetStringAsync(uri, Cancellation.Token)
end action
end class
The cancellation context belongs to an explicitly async declaration. DirectSharp calls between such actions propagate its token through the known contract. For imported APIs, pass Cancellation.Token explicitly when an overload would otherwise be ambiguous.
Cancellation is cooperative. It does not roll back a database commit, undo a completed HTTP operation, or guarantee that an external service stopped working. Application retries need idempotency and side-effect policies even when the caller cancelled its wait.
HttpClient is borrowed here. Configure it through the host or IHttpClientFactory and reuse it according to that lifetime. Do not create and dispose a new client for every request merely because it is IDisposable.
The concurrency example overlaps independent requests with concurrent all and limits outgoing calls through a shared Gate. Explicit awaits preserve sequential flow within a workflow; run or deliberately captured tasks express overlapping work.
ASP.NET Core
The customer API keeps the model, validation, repository and service in DirectSharp, and shows explicit C# hosting and controller integration. Framework glue can be in either language when its required contracts are available; it does not require a feature adapter.
A DirectSharp controller can also express normal CLR attributes and inherit ControllerBase. This smaller controller returns a stored customer through the same service:
import Global.System
import Global.Microsoft.AspNetCore.Mvc
public class CustomerLookupController
inherits ControllerBase
attributes ApiController(), Route("api/customer-lookup")
private readonly customers as CustomerRegistrationService
public constructor(service as CustomerRegistrationService)
require service is not nothing
customers = service
end constructor
public async action Get(id as Guid with attributes FromRoute())
returns IActionResult
attributes HttpGet("{id:guid}")
if id = Guid.Empty then
return BadRequest()
end if
let customer = await customers.FindAsync(id)
if customer is nothing then
return NotFound()
end if
return Ok(customer)
end action
end class
ApiController, Route, HttpGet and FromRoute are framework attributes. Declaration attributes follow their declaration header; parameter metadata uses with attributes. Routing, model binding and response serialization belong to ASP.NET Core. Use host-level exception handling or an explicit boundary like the full API example to turn infrastructure failures into safe responses.
Dependency injection and ownership
Constructor dependencies are ordinary .NET dependencies. Microsoft DI can instantiate the application classes when their effective constructors are public and their dependencies are registered.
The API host registers a singleton connection factory, scoped repository and scoped service. The factory creates owned connections; the service borrows its repository. Lifetime choice follows the actual dependency graph.
DirectSharp's needs and provide offer language-level dependency declarations and service scopes where useful. They do not force application code to replace Microsoft DI or wrap every existing library. A dependency borrowed from a provider is not automatically owned by its consumer.
Avoid capturing scoped dependencies in singletons. Background work must create and release an appropriate scope rather than retaining a request's service after the request has ended. A DbContext or connection shared merely for convenience can introduce both lifetime and concurrency errors.
Configuration and logging
Use normal .NET configuration providers for settings and secrets. Read settings at the host boundary, validate required values, and pass typed dependencies or options into application classes. Avoid ambient configuration lookups scattered through domain actions.
Logging is another ordinary dependency:
import Global.System
import Global.Microsoft.Extensions.Logging
public class CustomerAudit
private readonly logger as ILogger <of CustomerAudit>
public constructor(log as ILogger <of CustomerAudit>)
require log is not nothing
logger = log
end constructor
public action Registered(id as Guid)
logger.LogInformation("Customer {CustomerId} registered", id)
end action
end class
The single braces are a .NET logging message template. DirectSharp interpolation uses double braces, so this template reaches the logger as written. Structured values allow providers to retain CustomerId as a field instead of flattening everything into a string.
Choose levels and personal-data policies deliberately. A customer identifier may still be sensitive in an application's context; a validator's email failure does not justify logging the original address.
Observability, workers and UI
OpenTelemetry instruments the same CLR application and framework APIs. Register tracing and metrics in the .NET host; keep exporter endpoints and credentials in configuration. Gate acquisition and external-service latency can be useful application measurements, but a language construct does not automatically choose trace names or metric cardinality.
Aspire orchestrates and configures application resources through its .NET hosting model. A DirectSharp application assembly can participate through its ordinary host; Aspire is not a source-language deployment keyword.
Blazor components can call the same application services from Razor/C#. Keep UI validation results local to the form and pass cancellation when work can outlive the component. A DirectSharp business class does not need to become a component to serve a UI.
BackgroundService implementations likewise call ordinary async service contracts. The worker's stopping token should flow into those calls. Create a DI scope for scoped services and release it after the batch. If several items run concurrently, give each independent operation compatible dependencies rather than sharing one scoped DbContext across them.
Testing application behavior
Test outcomes at each boundary: validation properties, order amounts and state, SQL mapping/constraints, HTTP status and response models. Mocking every method call is less useful than proving that invalid input does not reach storage or that a rejected order cannot be processed again.
The API controller test uses xUnit against the ordinary CLR contracts and checks both property errors and zero storage access. The reference checks also exercise order overflow and cancellation before connection acquisition. Those checks validate the selected C# integration behavior; they do not establish DirectSharp compiler conformance.
Use a real database fixture for provider behavior and an HTTP test host for middleware, routing and serialization. A direct controller method test exercises neither of those systems. Prefer deterministic assertions over localized validation messages, racing completion order or machine-dependent string sorting.
When an adapter helps
Ordinary classes, extension methods and class kinds cover most application organization. Use feature for a library integration whose pipeline structure benefits from a compile-time adapter, or wrap for a deliberate source transformation. Keep them optional and explain the specific friction they remove.
A validator, SQL repository, controller or logger does not need an adapter merely to call an existing library. Start with the library's own API so its behavior, documentation and ecosystem remain available to the reader.