TedToolkit.Annotations
2026.7.15.2
Depends on the fucntions you want, find the TedToolkit.Annotations.* things.
dotnet add package TedToolkit.Annotations --version 2026.7.15.2
NuGet\Install-Package TedToolkit.Annotations -Version 2026.7.15.2
<PackageReference Include="TedToolkit.Annotations" Version="2026.7.15.2" />
<PackageVersion Include="TedToolkit.Annotations" Version="2026.7.15.2" />
<PackageReference Include="TedToolkit.Annotations" />
paket add TedToolkit.Annotations --version 2026.7.15.2
#r "nuget: TedToolkit.Annotations, 2026.7.15.2"
#:package TedToolkit.Annotations@2026.7.15.2
#addin nuget:?package=TedToolkit.Annotations&version=2026.7.15.2
#tool nuget:?package=TedToolkit.Annotations&version=2026.7.15.2
TedToolkit.Annotations
Lightweight attributes for making contracts, behavior, and planned maintenance visible in C# source. Documentation annotations describe code behavior. BehaviorCaseAttribute is emitted only when ANNOTATIONS_BEHAVIOR_CASE is defined; maintenance annotations are emitted only when ANNOTATIONS_MAINTENANCE is defined. These source-oriented annotations are intended for source readers and analyzers rather than runtime reflection.
The NuGet package also installs the bundled Roslyn analyzer. See the analyzer diagnostic catalog for rule severities, supported analysis, code fixes, and .editorconfig configuration.
Installation
dotnet add package TedToolkit.Annotations
Documentation annotations
Use documentation annotations when important behavior would otherwise remain implicit in implementation comments. They do not change runtime behavior; most are descriptive, while the bundled analyzer enforces ConstAttribute. Define ANNOTATIONS_BEHAVIOR_CASE when tooling needs individual behavior cases in assembly metadata.
using TedToolkit.Annotations.Documentations;
[Assumption("The caller holds the cache lock.")]
[Invariant("Entries are ordered by expiration time.")]
public sealed class Cache
{
[Precondition("key is not null.")]
[Postcondition("The returned entry belongs to key.")]
[SideEffect("Updates the entry's last-access timestamp.")]
[BehaviorCase("key is absent", "Returns null.", hasUnitTest: true)]
public Entry? Get(string key) => null;
}
| Attribute | Use it when |
|---|---|
PreconditionAttribute |
The caller must satisfy a condition before calling a member; it can record the exception type for a failed condition. |
PostconditionAttribute |
A member guarantees a condition after it completes successfully. |
InvariantAttribute |
A condition must remain true throughout a type's lifetime. |
BehaviorCaseAttribute |
A specific input condition has important expected behavior, especially a boundary case; it can record an expected exception. Use BehaviorCaseAttribute<TException> on C# 11+ for type-safe exception metadata. |
AssumptionAttribute |
Code relies on an external fact or convention that it does not verify. |
SideEffectAttribute |
A member causes an observable state change beyond its return value. |
IdempotentAttribute |
Repeating an operation has no additional observable effect. |
ThreadSafetyAttribute |
A type or member has a documented thread-safety guarantee or synchronization requirement. |
OwnershipAttribute |
A disposable value is borrowed or its ownership is transferred across an API boundary or into a field. |
CallbackLifetimeAttribute |
A callback parameter is invoked immediately, retained for deferred invocation, or retained as a subscription. |
ConstAttribute |
A parameter, method, or property accessor must not mutate selected object-graph depths. |
MayBlockAttribute |
An operation can block the calling thread; document the condition that causes it. |
ThreadAffinityAttribute |
A type or member requires a particular thread or synchronization context. |
For a parameter-level precondition, optionally record the exception type. The generic form is available to C# 11 or later consumers and guarantees that the supplied type derives from Exception.
public void Reserve(
[Precondition<ArgumentOutOfRangeException>("Must be greater than zero.")]
int quantity)
{
}
Use the non-generic form when supporting an earlier C# language version:
[Precondition("Must be greater than zero.", typeof(ArgumentOutOfRangeException))]
int quantity
DocumentationAttribute is the shared abstract base and is not applied directly.
Typed PreconditionAttribute<TException> annotations also drive the hidden TTA200 code fix, which adds missing XML <exception> entries to methods and constructors.
Describe const object-graph depth
ConstAttribute describes the object-graph depths that code must not mutate. Its ConstDepth mask has one uint bit per depth and defaults to ConstDepth.ALL, protecting all 32 depths.
Const checks are opt-in. Add the following to the consuming project's .csproj before relying on const diagnostics:
<PropertyGroup>
<TedToolkitEnableConstAnalysis>true</TedToolkitEnableConstAnalysis>
</PropertyGroup>
On a parameter, DEPTH0 prevents reassignment of the parameter itself; DEPTH1 protects its direct fields and properties; DEPTH2 protects members of those members; and so on. On an instance method, DEPTH0 protects direct fields and properties of this, DEPTH1 protects their members, and so on. On a static method or property, the declaring type's static state is the root: DEPTH0 protects its direct static fields and properties, and DEPTH1 protects their members. Applying Const to a type supplies this contract as a default for its static members; an explicit member contract overrides it. Use DEPTHn_OR_GREATER when a depth and every deeper depth must be protected.
The attribute can also annotate a property or an individual accessor. An accessor annotation takes precedence over the property annotation. When neither is present, a getter protects every depth, while a setter or init accessor protects DEPTH1_OR_GREATER: it may write any direct member of the current instance, but not a member below that level.
The bundled analyzer reports TTA300 as an error when a supported write reaches a protected depth of an annotated parameter, method, property accessor, or local variable. It uses control-flow-aware may-alias tracking across branches, loops, exception handlers, conditional/coalescing expressions, deconstruction, and foreach. Reference-type aliases preserve the contract. Value-type copies allow writes to copied value fields while retaining contracts for shared objects reached through reference fields, and ref aliases follow ref reassignment. Contracts declared by overridden or interface members and parameters are combined by their implementations.
It covers assignments (including ??= and deconstruction), increment/decrement, event subscription changes, array elements, and ref/out arguments. Calling a method on a protected receiver or passing a protected value by value requires a compatible ConstAttribute contract on the target method or parameter. An incompatible source method reports error TTA304; unverifiable external metadata reports informational TTA305. ConstAttribute is invalid on an out parameter and reports TTA301. Static methods and properties may be annotated.
Use Explicit.Const to apply the same contract to a local variable. It returns its input unchanged and is aggressively inlined. The ref overload preserves aliasing for ref locals. The call must directly initialize a local variable and use a compile-time constant depth mask; invalid calls report TTA302.
var local = Explicit.Const(node, ConstDepth.DEPTH1_OR_GREATER);
ref var alias = ref Explicit.Const(ref node, ConstDepth.DEPTH1_OR_GREATER);
Use Explicit.Box to state that allocation-producing boxing is intentional. Use its target-type overload for interfaces and other reference views:
object boxed = Explicit.Box(42);
IComparable comparable = Explicit.Box<IComparable, int>(42);
object? optional = Explicit.Box((int?)null);
Nullable values preserve normal boxing semantics: a value boxes its underlying value, while an empty nullable produces null. The bundled analyzer reports other boxing conversions as the informational diagnostic TTA201 and provides a code fix that rewrites them to Explicit.Box.
using TedToolkit.Annotations.Documentations;
public sealed class Node
{
public Node? Next { get; set; }
public int Value { get; set; }
[Const(ConstDepth.DEPTH0_OR_GREATER)]
public void Inspect([Const] Node node)
{
// node = new Node(); // TTA300: parameter depth 0
// node.Value = 1; // TTA300: parameter depth 1
// node.Next!.Value = 1; // TTA300: parameter depth 2
// Value = 1; // TTA300: method depth 0
}
}
out parameters cannot receive ConstAttribute, because the method creates their value rather than accepting a value from the caller:
public void Create([Const] out Node node) // TTA301
{
node = new Node();
}
Document concurrency, retries, and ownership
[ThreadSafety("Concurrent reads are supported; writes require external synchronization.")]
public sealed class Cache
{
[Idempotent]
public void Clear() { }
public void Attach([Ownership(OwnershipKind.TRANSFERRED)] Stream stream) { }
public void Enqueue(
[CallbackLifetime(CallbackLifetimeKind.DEFERRED)] Func<Task> work) { }
}
ThreadSafetyAttribute describes whether concurrent calls are safe and what synchronization they require. ThreadAffinityAttribute instead describes where code must run. MayBlockAttribute describes whether a synchronous operation can block its caller and why.
Ownership contracts
Ownership checks are opt-in. Add the following to the consuming project's .csproj before relying on ownership diagnostics:
<PropertyGroup>
<TedToolkitEnableOwnershipAnalysis>true</TedToolkitEnableOwnershipAnalysis>
</PropertyGroup>
OwnershipAttribute documents who is responsible for ultimately releasing an IDisposable or IAsyncDisposable. It is valid on a disposable value or on a value that structurally carries disposable resources through generic arguments, arrays, or source-defined instance fields; the analyzer reports TTA010 as an error only when no such resource is present.
OwnershipAttribute always requires an OwnershipKind argument. In other words, the attribute constructor has no optional kind. OwnershipKind.UNCHANGED has the underlying enum value 0 and is therefore the CLR default enum value, but it is not a universal analyzer default for every API boundary:
| Kind | Meaning |
|---|---|
OwnershipKind.TRANSFERRED |
The receiver becomes responsible for disposing or transferring the resource. |
OwnershipKind.UNCHANGED |
The receiver only borrows the resource and must not dispose it. |
When OwnershipAttribute is absent, the analyzer applies boundary-specific ownership conventions. OwnershipFlow is optional when the attribute is present and selects which direction its explicit kind describes. The complete default relationship is:
| Boundary or source | Default flow | Default ownership kind when unannotated | Effect |
|---|---|---|---|
| Object creation | — | TRANSFERRED |
The code receiving the new disposable owns it. |
| Method return value | OUTPUT |
TRANSFERRED |
The caller owns the returned disposable. |
| Property getter | OUTPUT |
UNCHANGED |
The caller borrows the returned property value. |
Ordinary or in parameter |
INPUT |
UNCHANGED |
The member borrows the caller's resource. |
out parameter |
OUTPUT |
TRANSFERRED |
The caller owns the resource assigned by the member. |
ref parameter |
none | none | Both directions are possible; declare INPUT and/or OUTPUT explicitly. |
| Property setter | INPUT |
UNCHANGED |
The setter borrows the assigned value unless an INPUT + TRANSFERRED contract says it takes ownership. |
| Field | DEFAULT |
none | A field is storage, not an API boundary. Mark it explicitly or let constructor assignment inference determine ownership. |
For a property annotation, omitted flow means OUTPUT, so it describes the getter. Use an explicit INPUT annotation to describe the setter. For a ref parameter, both flow and kind must be stated for each direction that participates in ownership transfer.
For example, a factory transfers a new resource to its caller, while a shared resource is returned as borrowed:
public sealed class ResourceFactory
{
[return: Ownership(OwnershipKind.TRANSFERRED)]
public Stream Create() => new MemoryStream();
[return: Ownership(OwnershipKind.UNCHANGED)]
public Stream Shared => _shared;
private readonly Stream _shared = new MemoryStream();
}
A property can receive an owned resource yet expose only a borrowed reference:
[Ownership(OwnershipKind.TRANSFERRED, OwnershipFlow.INPUT)]
[Ownership(OwnershipKind.UNCHANGED, OwnershipFlow.OUTPUT)]
public Stream Current { get; set; }
Fields describe the object's internal responsibility. Mark a field explicitly when its ownership cannot be inferred:
public sealed class Session : IDisposable
{
[Ownership(OwnershipKind.TRANSFERRED)]
private readonly Stream _stream;
public Session([Ownership(OwnershipKind.TRANSFERRED)] Stream stream)
=> _stream = stream;
public void Dispose() => _stream.Dispose();
}
An owned container transfers responsibility for every disposable resource it structurally carries. The container itself does not need to implement a disposal contract, but its owner must release the contained resources:
public sealed class PartOwner : IDisposable
{
[Ownership(OwnershipKind.TRANSFERRED)]
private readonly Dictionary<Guid, ICollection<IObjectPart>> _parts = new();
public void Dispose()
{
foreach (var collection in _parts.Values)
{
foreach (var part in collection)
{
part.Dispose();
}
}
_parts.Clear();
}
}
The analyzer also infers field ownership when a constructor assigns an INPUT + TRANSFERRED parameter to an instance disposable field. A type with owned members must implement the compatible synchronous or asynchronous disposal contract (TTA008) and must release or transfer every owned field or property from its disposal method (TTA009). A member marked UNCHANGED is borrowed and must not be disposed by its receiver (TTA006). Static fields are outside an individual instance's lifetime.
Conflicting ownership kinds for the same effective flow, unsupported flow directions, and a missing explicit flow on ref parameters report TTA014.
Asynchronous resources are tracked through await using and observed DisposeAsync calls. The returned ValueTask must be awaited, configured and awaited, or returned to the caller; otherwise the analyzer reports TTA013.
public sealed class AsyncSession : IAsyncDisposable
{
[Ownership(OwnershipKind.TRANSFERRED)]
private readonly IAsyncDisposable _resource;
public AsyncSession([Ownership(OwnershipKind.TRANSFERRED)] IAsyncDisposable resource)
=> _resource = resource;
public ValueTask DisposeAsync() => _resource.DisposeAsync();
}
[ThreadAffinity("Must be called from the UI thread.")]
public void UpdateView() { }
[MayBlock("Performs synchronous disk I/O.")]
public void Flush() { }
Document parameters and return values
Attributes can describe the contract at the point where it matters most: a parameter or return value.
using TedToolkit.Annotations.Documentations;
public sealed class Inventory
{
[return: Postcondition("The result is non-negative.")]
public int Reserve([Precondition("quantity is greater than zero.")] int quantity)
{
return quantity;
}
}
Capture assumptions and observable effects
using TedToolkit.Annotations.Documentations;
public sealed class SessionService
{
[Assumption("The caller has authenticated the request.")]
[SideEffect("Revokes all refresh tokens for the user.")]
[BehaviorCase("The user has no active sessions", "Completes without changes.", hasUnitTest: true)]
public void SignOutEverywhere(Guid userId) { }
}
Maintenance annotations
Apply these annotations to constructors or methods. Every annotation requires a concise reason and can optionally state the condition for removal with RemoveWhen.
using TedToolkit.Annotations.Maintenances;
public sealed class MaintenanceExamples
{
[Workaround("Serializer drops required members in version 4.2.",
RemoveWhen = "Serializer 4.3 is the minimum supported version")]
public void Serialize() { }
[TemporaryImplementation("Use the legacy client until OAuth flow is available.",
RemoveWhen = "OAuthClient is production-ready")]
public void Connect() { }
[TechnicalDebt(TechnicalDebtKind.Design,
"Keep the parser and transport coupled until the protocol stabilizes.",
RemoveWhen = "Protocol version 2 is released")]
public void ProcessRequest() { }
[CleanupRequired("Merge the duplicate validation branches.",
RemoveWhen = "The legacy request format is removed")]
public void Validate() { }
}
| Attribute | Use it when |
|---|---|
WorkaroundAttribute |
Code compensates for an external defect or limitation. |
TemporaryImplementationAttribute |
The implementation is intentionally incomplete and will be replaced. |
TechnicalDebtAttribute |
An intentional trade-off should be repaid; use its kind to classify the affected area. |
CleanupRequiredAttribute |
Correct code should later be simplified, removed, or reorganized. |
MaintenanceAttribute is the shared abstract base and is not applied directly. Do not use maintenance annotations for API deprecation; use System.ObsoleteAttribute instead.
Keep maintenance context out of normal builds
Maintenance attributes are emitted only when ANNOTATIONS_MAINTENANCE is defined. Add it to the projects or build configuration where tooling needs to inspect this context:
<PropertyGroup>
<DefineConstants>$(DefineConstants);ANNOTATIONS_MAINTENANCE</DefineConstants>
</PropertyGroup>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 is compatible. net48 is compatible. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.7.2
- No dependencies.
-
.NETFramework 4.8
- No dependencies.
-
.NETStandard 2.0
- No dependencies.
-
.NETStandard 2.1
- No dependencies.
-
net10.0
- No dependencies.
-
net6.0
- No dependencies.
-
net7.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|