.NET contracts
DirectSharp uses ordinary CLR classes, interfaces, generics, attributes, delegates, tasks and disposal protocols. Library names remain library symbols. Calling FluentValidation or Entity Framework does not introduce domain-specific language keywords.
Imported calls and overloads
let builder = new Global.System.Text.StringBuilder()
builder.AppendLine("Invoice summary")
let summary = builder.ToString()
The receiver is a normal StringBuilder. Member binding selects its actual CLR overloads; evaluation runs once in source order. The object can be passed to C# without wrapping it.
Reference C#:
var builder = new global::System.Text.StringBuilder();
builder.AppendLine("Invoice summary");
var summary = builder.ToString();
Imports enable unqualified names and applicable extension lookup. They do not change exception behavior, force deferred LINQ evaluation or make a missing overload valid. Use qualification to resolve ambiguity rather than choosing an arbitrary imported candidate.
DirectSharp case-insensitive binding is still required at the boundary. Imported names colliding under that rule require an unambiguous binding; source casing alone must not silently choose different runtime methods.
Delegates and expression trees
customers.Where(.IsActive)
validator.RuleFor(customer: customer.Email)
These are expressions with existing application objects and appropriate namespace imports. The parameter target determines whether the selector becomes a delegate/closure or an expression tree. For FluentValidation RuleFor, the selector is ordinarily an expression-tree contract. For Enumerable.Where it is ordinarily a delegate.
Reference C# forms:
customers.Where(customer => customer.IsActive);
validator.RuleFor(customer => customer.Email);
The same source selector syntax can serve both targets. A selector that cannot be represented by the required target is diagnosed; the compiler must not silently execute it as a delegate or inline a named action into an expression tree. IQueryable translation, deferred execution and provider restrictions remain ordinary .NET behavior.
Generic method inference uses applicable call arguments and relationships. Otherwise-unbound return parameters require explicit generic arguments. A convenient expected target is not permission to invent a different inference model.
Public members and metadata
Public stored members infer properties; private stored members infer fields, while protected/internal members infer properties unless explicitly declared as fields. Ordinary actions, including public actions, may infer async implementation from required suspension and normally use Task-equivalent ABI. Optional explicit async fixes that contract and provides its declared cancellation context. Interface/base contracts and overrides preserve their actual signatures, including an imported Task/ValueTask distinction.
Parameter metadata follows the complete parameter declaration:
query as String with attributes FromQuery()
This is a parameter fragment with the attribute type imported. Return metadata likewise follows returns T with attributes .... C# bracket shorthand is supported on parameters for interoperability; it does not become the general declaration attribute syntax.
Assembly attributes belong in the project declaration:
project
assembly attributes
Global.System.Reflection.AssemblyTitleAttribute("Acme.Payments")
end attributes
end project
Reference C#:
[assembly: global::System.Reflection.AssemblyTitleAttribute("Acme.Payments")]
Attribute constructors and named arguments bind against actual CLR metadata. Invalid target/application, inaccessible constructor and incompatible argument type are compile-time errors.
Services and ownership
needs declares dependencies rather than ownership. Consumers borrow them. provide explicitly supplies singleton, scoped or transient registration lifetime. Composition definitions can contribute to an existing Microsoft DI host instead of building an unrelated second provider.
| Construction / retrieval | Object ownership | Dependency ownership |
|---|---|---|
new T(arguments) with ... |
Caller | Explicitly supplied values keep their existing ownership |
new T(arguments) from scope |
Caller | Scope/provider-supplied needs remain borrowed |
Scope/provider GetService <of T>() |
Service environment | Service environment |
| Main-provider appointment | Creator/host retains ownership | Unchanged |
DI-assisted new creates a fresh object. It does not resolve an existing T registration, and T's registration lifetime does not secretly change this operation. Explicit need overrides take precedence; disposing the new object must not dispose borrowed provider-owned needs.
A singleton graph cannot reach a scoped dependency, even through transient intermediaries. Scoped resolution uses explicit disposable scopes; no ambient current scope exists. Root direct retrieval is restricted to root-safe services. Exact keys remain case-sensitive data even though language identifiers are case-insensitive.
Provider GetService is required lookup: missing service fails immediately. GetAllServices permits an empty collection. Mapping to Microsoft DI must preserve this distinction rather than blindly copying nullable GetService behavior. See standard service abstractions.
Required singular lookup of an unregistered service throws DirectSharp.ServiceNotFoundException, an InvalidOperationException subclass with read-only ServiceType and ServiceKey properties. The properties retain the requested Type and exact key, or null for unkeyed lookup. Constructor/activation failures, disposed-provider failures and root lifetime violations are separate failures. Keyed calls overload the same ordinary generic names, for example scope.GetService <of ILogger>("audit"); GetAllServices and HasService have matching keyed overloads. Explicit null keys throw ArgumentNullException before lookup/activation; the no-key overload selects unkeyed identity.
GetAllServices resolves at the call and returns IReadOnlyList of the selected service type, present empty if no matches. Repeated traversal reuses those instances. It does not extend a scope, transfer ownership or roll back objects already created if a later activation fails. See lookup declarations.
Native DI integration requirements
An ordinary ASP.NET Core application can register DirectSharp-generated classes directly in Microsoft DI, as the Customer host demonstrates. A portable DirectSharp service bridge has additional obligations. A native API that looks similar is not automatically an equivalent implementation.
| DirectSharp operation | Useful native building block | Required adaptation |
|---|---|---|
| Composition Register | Existing host IServiceCollection | Contribute descriptors to that registry; do not build or appoint another provider during registration |
| Ordinary provide | ServiceDescriptor and registration metadata | Replace the effective registration and lifetime for the exact service identity; repeatedly appending descriptors is insufficient |
| provide another | Additive native registration | Retain existing contributors and each registration's lifetime |
| Required GetService | Native resolution plus registration information | Distinguish unregistered identity from activation failure; preserve ServiceNotFoundException's selected identity |
| HasService | IServiceProviderIsService or keyed availability capability, where supplied | Inspect availability without activating the service; an arbitrary IServiceProvider alone supplies no complete inspection contract |
| GetAllServices | Native multiple-service resolution | Complete resolution and collection at the call; return IReadOnlyList, not a deferred activation recipe |
| Explicit scope creation | Native scope factory and scope | Preserve independent scoped instances and bounded ownership; match cleanup to the statically supported sync/async contract |
| Root retrieval | Registration/lifetime graph metadata | Enforce DirectSharp root-safety restrictions even when the native container would accept the resolution |
| Main-provider appointment / Native access | Existing provider handle | Borrow the host-owned provider; neither appointment nor native access transfers shutdown ownership |
For example, native GetService may return null for an unregistered service, and native GetRequiredService normally uses InvalidOperationException. DirectSharp requires its dedicated exception for an unregistered required identity. Catching every InvalidOperationException and translating it would also misclassify a constructor that throws that same exception. Registration knowledge and activation failure must remain distinct. A registered service can fail to construct; HasService is not a promise of successful construction.
Similarly, implementing HasService by resolving a service would run constructors/factories, allocate transients and potentially capture disposables. Availability inspection must not have those effects. A backend adapter needs the relevant native inspection capability or equivalent registration metadata. The concrete capability/error reporting for an opaque provider is backend API work; guessing false or probing activation does not establish the portable contract.
Integration cases
| Setup / operation | Observable requirement |
|---|---|
| Register the composition into a configured host | The same host builds and owns its provider; registration alone activates no construction recipe |
| Register IClock as A, then ordinarily provide IClock as B | B becomes the effective ordinary registration, with its selected lifetime |
| Replace the identity keyed by audit | The identity keyed by Audit and the unkeyed identity are unaffected |
| Inspect an existing registration whose factory increments a counter | HasService leaves the activation counter unchanged |
| Inspect a scoped registration through the root | Availability can be true; root retrieval remains forbidden |
| Resolve a registered service whose constructor throws | Preserve that activation failure; do not report missing registration |
| Resolve several services and the later activation fails | Return no successful partial list; do not invent rollback or transfer ownership of earlier instances |
| Enumerate a successfully collected plural result twice | Reuse the resolved instances; do not activate another set of transients |
| Dispose one of two independently created scopes | Release only that scope's owned resources; the other scope retains its own instances |
| Dispose an object created with new from scope | Its borrowed needs remain owned by the scope; the created object is caller-owned |
This matrix follows the service rules. It specifies what a bridge must preserve, rather than claiming that a bridge is implemented or selecting its native handle/constructor signatures. External registry calls and opaque factories can hide dependency edges; native validation and runtime failures remain necessary where the compiler cannot establish a complete graph. The library plan separates those integration cases from the ordinary application path.
Resources and native behavior
Synchronous resources use IDisposable; async disposal follows IAsyncDisposable in an appropriate async context. Cleanup is structured and reverse-ordered. Imported exceptions preserve their normal identity and failure timing; try_get suppresses normally catchable failures only where explicitly written.
A DirectSharp contract is not a replacement for an external library's own contract. EF Core tracking, database transactions, Dapper buffering, stream positioning, HTTP cancellation and JSON serialization policy remain properties of the chosen APIs and application configuration. The .NET integration guide demonstrates direct usage before optional abstractions.