Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,24 @@ Correctness is a must. The other goals are strong defaults and win in this order
- Files are stored with LF in git and checked out per platform (`* text=auto`); `.editorconfig` asks for CRLF and tabs. Raw string literals therefore contain the checkout's line endings, so do not assert on exact newlines in generated text.
- Most files have no UTF-8 BOM, a few do. Keep a file's existing BOM state: PowerShell rewrites strip or add BOMs, so prefer careful editors and check `git diff` for a stray U+FEFF.
- Comments are rare and explain why; prefer XML docs. In tests, put the reason into `.Because(...)` instead of a comment.
- No em-dashes in docs, XML docs or comments. `Docs/pages/` is published to docs.testably.org; keep it user-facing and describe the current behaviour, not its history.
- No em-dashes in XML docs or comments.

## Documentation

Pages under `Docs/pages` are published on docs.testably.org and follow the [docs style guide](https://github.com/Testably/Testably.Site/blob/main/Docs/style-guide.md). The key rules:

- US English, sentence case for headings and labels, no em dashes, one line per paragraph.
- A `description` in the front matter of every page; the H1 is the title; order with unique `NN-` prefixes.
- Relative file links within the repository, site-absolute `/Library/...` links across libraries.
- Install instructions only through `<PackageInstall package="..." />`, never hand-written commands or NuGet badges.
- Examples check results with `await That(...)`; complete examples use TUnit `[Test]`.

In this repository:

- Keep the pages user-facing and describe the current behavior, not its history.
- Do not set `collapsed` in `_category_.json`.
- Most examples show container usage rather than tests; add a test only where an example checks a result. The coffee shop is the running example.
- `Tests/Awaiten.Docs.Tests` compiles every `csharp` block of a page; the fence meta `no-compile` and `expect=AWTnnn` and the `Scaffold/` files are described in [Docs/agents/workflows.md](Docs/agents/workflows.md#documentation).

## Git workflow

Expand Down
2 changes: 1 addition & 1 deletion Docs/agents/design-goals.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ When goals conflict, the earlier one wins.
6. **One consistent declaration language.** Registration reads the same for every lifetime: one attribute per lifetime (`[Singleton]`, `[Scoped]`, `[Transient]`) with the same shape and options. Registration and import attributes offer the generic and `typeof` forms side by side; the `typeof` form also names open generics, which C# does not allow as a type argument. An attribute that takes a static class (`[Import]` of a `[Module]`) has only the `typeof` form, because C# does not allow a static class as a type argument either. Public types are named by role (see `CONTRIBUTING.md`): the container and host-facing types carry the `Awaiten` prefix, the declaration attributes, their enums and the types users implement or name do not. Every public member has XML documentation.
7. **Domain code stays free of Awaiten.** Configuration belongs on the composition root. Prefer a container-side form (`[ImportService<T>]`, `WhenInjectedInto`, a factory `[Arg]`, `[InjectProperty<T>]`) over an attribute a user would have to put on their own class; the consumer-side attributes (`[Arg]`, `[FromKey]`, `[Inject]`) stay escape hatches. Extension points are narrow and dependency-free seams such as `IExternalResolver`, not hooks into the generator.
8. **Fast resolution, simply.** Resolution is the hot path: generated resolve code avoids allocations, LINQ, closures and boxing. Simplicity still wins over a micro-optimization: take a performance change only when a benchmark before and after shows a measurable gain, and prefer the simpler generated code when the gain is marginal.
9. **Documented.** A public feature or a new diagnostic is not done until it is on the docs site (`Docs/pages/`), with examples that `Tests/Awaiten.ExampleTests` covers where they matter. Feature pages that let users paper over a design problem (keyed services, scanning, context-aware factories, property injection) keep their "When not to reach for this" note. Use the Docusaurus features the existing pages use: front matter, admonitions (`:::tip`, `:::warning`, `:::danger` for the error diagnostics) and tabs where alternatives sit side by side.
9. **Documented.** A public feature or a new diagnostic is not done until it is on the docs site (`Docs/pages/`), with examples that `Tests/Awaiten.ExampleTests` covers where they matter. Feature pages that let users paper over a design problem (keyed services, scanning, context-aware factories, property injection) keep their "When not to reach for this" note. Follow the [docs style guide](https://github.com/Testably/Testably.Site/blob/main/Docs/style-guide.md) (see the Documentation section in `AGENTS.md`): front matter with a `description`, admonitions with the meaning the guide gives each type, and tabs where alternatives sit side by side.

## Code

Expand Down
2 changes: 1 addition & 1 deletion Docs/agents/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Always run in Release and compare before and after on the same machine.

- `Docs/pages/` is the user documentation (Docusaurus front matter and admonitions such as `:::tip`). It is not built here: after a successful `Build` on `main`, `notify-docs-site.yml` asks `Testably/Testably.Site` to rebuild docs.testably.org/Awaiten.
- `Docs/pages/10-benchmarks.mdx` renders `data.js`/`limited-data.js` that `PublishBenchmarkReport` commits to the `benchmarks` branch.
- `Docs/pages/09-diagnostics.md` has one `### AWTnnn` section per diagnostic; other pages link to it as `./diagnostics#awtnnn`.
- `Docs/pages/09-diagnostics.md` has one `### AWTnnn` section per diagnostic; other pages link to it by its file, e.g. `../09-diagnostics.md#awtnnn` from a page in a subfolder.
- The `README.md` is packed into the NuGet packages; `UpdateReadme` rewrites badges for a release.
- Code fences in the docs are compiled by `Tests/Awaiten.Docs.Tests`, whose `CodeBlocks_ShouldCompile` test (one per page, in the explicit `Slow` category) runs the generator and the analyzers over every ```` ```csharp ```` block of a page: `dotnet test --project Tests/Awaiten.Docs.Tests -f net10.0 --treenode-filter "/*/*/*/*[Category=Slow]"`. Keep examples that matter at run time covered by `Tests/Awaiten.ExampleTests` too.
- All blocks of a page compile together in the namespace `Snippets`. A type that only one block declares is shared by the page; a type that several blocks declare (variants of `CoffeeShop`, say) stays local to each block. A block that declares no variant of its own continues the block before it, so its statements see that block's container. Statements become the body of a method, and registration attributes shown without a class are checked on a placeholder class.
Expand Down
53 changes: 19 additions & 34 deletions Docs/pages/00-index.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
---
title: Awaiten
sidebar_position: -1
description: Awaiten is an async-first, compile-time dependency injection container for .NET that wires your object graph with a source generator.
---

# Awaiten

<img align="right" width="200" src="https://raw.githubusercontent.com/Testably/Awaiten/main/Docs/Awaiten_256x256.png" alt="Awaiten logo" />

The async-first dependency injection container for .NET.

Awaiten is a Roslyn source generator. It wires your object graph at build time, so there is no runtime reflection and the generated code is plain, readable C#. Your container is native-AOT clean and trim-safe out of the box.

The configuration is verified by the compiler. Missing, cyclic, ambiguous, and lifetime-mismatched registrations are build errors, not surprises at startup. Awaiten ships more than 100 diagnostics for this.

Its headline feature is **async initialization**. Some services need asynchronous setup after construction, like an espresso machine heating its boiler or a payment terminal connecting to the bank. Awaiten tracks those through the graph, and reaching one synchronously before it is ready is a compile error.

## A first shot
The async-first dependency injection container for .NET. Awaiten wires your object graph at build time and checks it with the compiler, including services that need asynchronous setup.

Think of the container as a coffee shop. The espresso machine is shared, so it is a singleton. Each order gets its own scope. A cup is made fresh every time, so it is transient.

Expand All @@ -28,13 +19,19 @@ using Awaiten;
[Transient<Cup>]
public static partial class CoffeeShop;

await using var shop = new CoffeeShop.Root();
await using CoffeeShop.Root shop = new();
await shop.InitializeAsync(); // the espresso machine warms up

var cup = shop.Resolve<Cup>(); // a fresh cup, wired and ready
Cup cup = shop.Resolve<Cup>(); // a fresh cup, wired and ready
```

## Why Awaiten
## Why Awaiten?

Awaiten is a Roslyn source generator. It wires your object graph at build time, so there is no runtime reflection and the generated code is plain, readable C#. Your container is native-AOT clean and trim-safe out of the box.

The configuration is verified by the compiler. Missing, cyclic, ambiguous, and lifetime-mismatched registrations are build errors, not surprises at startup. Awaiten ships more than 100 diagnostics for this.

Its headline feature is **async initialization**. Some services need asynchronous setup after construction, like an espresso machine heating its boiler or a payment terminal connecting to the bank. Awaiten tracks those through the graph, and reaching one synchronously before it is ready is a compile error.

| | Reflection-based containers | Awaiten |
|---|---|---|
Expand All @@ -43,25 +40,13 @@ var cup = shop.Resolve<Cup>(); // a fresh cup, wired and ready
| Async setup | Manual, easy to forget | Tracked, enforced by the compiler |
| Native AOT / trimming | Needs care, often breaks | Clean by construction |

## Getting started

1. [Install the package](./getting-started).
2. Declare a `[Container]` and register your services.
3. Create a `Root` and start resolving.

## Where to go next

- [Getting started](./getting-started) builds your first container end to end.
- [Complete example](./complete-example) assembles a full coffee shop.
- [Lifetimes](./core-concepts/lifetimes) explains singleton, scoped, and transient.
- [Async initialization](./core-concepts/async-initialization) covers the headline feature.
- [MS.DI bridge](./hosting/msdi-bridge) runs a container under ASP.NET Core and the generic host.
- [Design principles & the composition root](./design-principles) is the design behind the mechanics; read it once you know the basics.
- [Diagnostics](./diagnostics) lists every compile-time check.

## Packages

| Package | Description |
|---|---|
| `Awaiten` | The core: attributes, the source generator, and the runtime seams. No third-party dependencies; on netstandard2.0 only `Microsoft.Bcl.AsyncInterfaces`. |
| `Awaiten.Extensions.DependencyInjection` | Microsoft.Extensions.DependencyInjection interop for ASP.NET Core, the generic host, and other containers. See the [MS.DI bridge](./hosting/msdi-bridge). |
- [Getting started](./01-getting-started.md) builds your first container end to end.
- [Complete example](./02-complete-example.md) assembles a full coffee shop.
- [Lifetimes](./03-core-concepts/01-lifetimes.md) explains singleton, scoped, and transient.
- [Async initialization](./03-core-concepts/03-async-initialization.md) covers the headline feature.
- [MS.DI bridge](./07-hosting/01-msdi-bridge.md) runs a container under ASP.NET Core and the generic host.
- [Design principles & the composition root](./08-design-principles.md) is the design behind the mechanics; read it once you know the basics.
- [Diagnostics](./09-diagnostics.md) lists every compile-time check.
- [Benchmarks](./10-benchmarks.mdx) compares Awaiten with other .NET containers.
67 changes: 40 additions & 27 deletions Docs/pages/01-getting-started.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,47 @@
---
description: Install Awaiten, declare your first container, resolve services and let the compiler check your wiring.
---

import PackageInstall from '@site/src/components/PackageInstall';

# Getting started

This page opens a coffee shop from scratch. By the end you have a working container, you know how to resolve services, and you have seen the compiler catch a mistake.

## Install
:::note
Where an example checks a result in a test, it uses [aweXpect](/aweXpect/) in the form `await That(...)`. aweXpect is optional; Awaiten works with any test framework and assertion library.
:::

## Requirements

Add the package to your project:
:::info[.NET 10.0.300 SDK and C# 11]
- Build with the .NET SDK 10.0.300 or later. The source generator needs the C# compiler that ships with it.
- Write C# 11 or later. The registration attributes in these docs, such as `[Singleton<T>]`, are generic attributes.
- Target `net10.0`, `net8.0` or any framework that supports `netstandard2.0`, including .NET Framework 4.8. The package has no third-party dependencies; on netstandard2.0 it references only `Microsoft.Bcl.AsyncInterfaces`, for async disposal.
:::

## Install the package

<PackageInstall package="Awaiten" />

Awaiten targets `net10.0`, `net8.0`, and `netstandard2.0`, so it runs on modern .NET and on older toolchains. The package has no third-party dependencies; on netstandard2.0 it references only `Microsoft.Bcl.AsyncInterfaces`, for async disposal.
| Package | Description |
|---|---|
| `Awaiten` | The core: attributes, the source generator, and the runtime seams. No third-party dependencies; on netstandard2.0 only `Microsoft.Bcl.AsyncInterfaces`. |
| `Awaiten.Extensions.DependencyInjection` | Microsoft.Extensions.DependencyInjection interop for ASP.NET Core, the generic host, and other containers. See the [MS.DI bridge](./07-hosting/01-msdi-bridge.md). |

## Build your first container

## Declare a container
In short: declare a `[Container]`, register your services on it, then create its `Root` and start resolving.

A container is a `static partial` class marked with `[Container]`. You register services with lifetime attributes on that class. Nothing else is needed. The source generator does the rest at build time.

```csharp
```csharp title="Program.cs"
using Awaiten;

using CoffeeShop.Root shop = new();

Cup cup = shop.Resolve<Cup>();

public sealed class EspressoMachine;
public sealed class Grinder;
public sealed class Cup(EspressoMachine machine, Grinder grinder);
Expand All @@ -30,17 +53,7 @@ public sealed class Cup(EspressoMachine machine, Grinder grinder);
public static partial class CoffeeShop;
```

The generator emits a nested `Root` type for you. The `Root` is your composition root. It owns the singletons and it is the outermost scope.

## Create the root and resolve

Create the `Root`, then ask it for services with `Resolve<T>()`:

```csharp
using var shop = new CoffeeShop.Root();

Cup cup = shop.Resolve<Cup>();
```
The generator emits a nested `Root` type for you. The `Root` is your composition root. It owns the singletons and it is the outermost scope. You create it, then ask it for services with `Resolve<T>()`.

Awaiten built the `Cup` for you and passed in the shared `EspressoMachine` and `Grinder`. You wrote no factory code.

Expand Down Expand Up @@ -71,15 +84,15 @@ Some things belong to a single order, like the running tab. Register them as `[S
[Scoped<Order>]
public static partial class CoffeeShop;

using var shop = new CoffeeShop.Root();
using CoffeeShop.Root shop = new();

using (IAwaitenScope order = shop.CreateScope())
{
Order o = order.Resolve<Order>();
} // the order and everything it owns is disposed here
```

Inside one scope you get the same `Order` every time. A different scope gets a different one. See [Scopes](./core-concepts/scopes) for the full picture.
Inside one scope you get the same `Order` every time. A different scope gets a different one. See [Scopes](./03-core-concepts/02-scopes.md) for the full picture.

## Let the compiler check your wiring

Expand All @@ -93,15 +106,15 @@ public sealed class Cup(Grinder grinder);
public static partial class CoffeeShop;
```

> ```
> AWT101: 'Cup' needs 'Grinder', but nothing registers it
> ```
```text title="Build error"
error AWT101: 'Cup' cannot be resolved: 'Cup' requires 'Grinder', which is not registered on the container
```

Cyclic graphs, captive dependencies, and async services touched synchronously are caught the same way. The full list is on the [Diagnostics](./diagnostics) page.
Cyclic graphs, captive dependencies, and async services touched synchronously are caught the same way. The full list is on the [Diagnostics](./09-diagnostics.md) page.

## Where to go next
## Next steps

- [Complete example](./complete-example) for a full container that uses the main features together.
- [Lifetimes](./core-concepts/lifetimes) for singleton, scoped, and transient in depth.
- [Resolving services](./resolution/resolving-services) for the full resolve surface.
- [Async initialization](./core-concepts/async-initialization) once a service needs async setup.
- [Complete example](./02-complete-example.md) for a full container that uses the main features together.
- [Lifetimes](./03-core-concepts/01-lifetimes.md) for singleton, scoped, and transient in depth.
- [Resolving services](./05-resolution/01-resolving-services.md) for the full resolve surface.
- [Async initialization](./03-core-concepts/03-async-initialization.md) once a service needs async setup.
Loading
Loading