Summary#
using is C#'s, and it means what C# says it means: the resource's Dispose() runs on every way out of the
block — falling off the end, return, break, continue, or an exception on its way out — and a using var
declaration lasts to the end of its enclosing block, with several of them disposed last declared first.
The platform owns the lifetime of everything it hands you — rows, HTTP calls, files — so none of those need a
using. You reach for one when your class holds something outside Osy#: a session opened on an external
database, a multipart upload started on a bucket, a lease taken on a remote lock. That class implements
IDisposable with its own Dispose():
app Warehouse {
model "model/**/*.osy";
use Osysharp.Http; // the manifest dependency that makes `Http.*` available
}
class WarehouseSession : IDisposable {
public string Id = "";
public bool Closed;
public static WarehouseSession Open() {
var r = Http.Post("https://warehouse.example.com/sessions", "{}", "application/json");
return new WarehouseSession { Id = r.Body };
}
public void Dispose() {
if (Closed) { return; }
Http.Delete($"https://warehouse.example.com/sessions/{Id}");
Closed = true;
}
}
int CountRows(string table) {
using (var session = WarehouseSession.Open()) {
var r = Http.Get($"https://warehouse.example.com/sessions/{session.Id}/count/{table}");
if (!r.IsSuccess) { return 0; } // the session is closed here too
return int.Parse(r.Body);
} // …and here
}Signature#
using (var r = <expression>) <statement-or-block> // the statement form
using (<Type> a = <expression>, b = <expression>) { … } // several of one type — b is disposed before a
using (<expression>) { … } // dispose a value you already hold
using var r = <expression>; // the declaration form — to the end of the blockDescription#
What it lowers to#
Exactly C#'s lowering, onto the try/finally the language already runs on the server and in the browser:
using (var r = Open()) { Work(r); }
// is
{ var r = Open(); try { Work(r); } finally { r?.Dispose(); } }So the resource is created before the try — if creating it throws, there is nothing to dispose — and a null
resource is skipped rather than dereferenced. Everything finally promises, using promises: see
try / catch / finally.
The declaration form, and the order#
using var r = …; is scoped to the end of the block it is in. Two of them dispose in reverse order, because the
second one is nested inside the first — the newest resource is released before the one it may depend on:
class Step : IDisposable {
public string Name = "";
public Trail Into = new Trail();
public void Dispose() { Into.Log = Into.Log + Name; }
}
class Trail {
public string Log = "";
public void Run() {
using var first = new Step { Name = ",1", Into = this };
using var second = new Step { Name = ",2", Into = this };
Log = "body";
} // second is disposed, then first
}
string Order() {
var trail = new Trail();
trail.Run();
return trail.Log; // "body,2,1"
}A using declaration needs a block to end in, so the body of a brace-less if cannot be one — C# refuses it too
(CS1023). Write braces, or the statement form.
IDisposable is a real contract#
IDisposable is an ordinary interface with one member, void Dispose(), and a class takes it the way it takes any
contract — class Lease : IDisposable { public void Dispose() { … } } (Interfaces — a contract several types can satisfy). using over a value
that is not an IDisposable is refused, as C# refuses it (CS1674): there would be nothing to call.
class Note { public string Text = ""; }
using (var n = new Note()) { … } // ✗ 'Note' is not IDisposable, so `using` has nothing to dispose (CS1674)If there is genuinely nothing to release, there should be no using — keep the declaration and drop the word.
The one Osy# rule: no durable wait inside#
A body in Osy# can park: an awaited Workflow.Run, a saga step (await saga.Run(…), await saga.DisposeUnwind()), a join (await Workflow.WhenAll/WhenAny/When(…)), or a Workflow.Once(…, retry: …) can
suspend the run and resume it later on another process — after a restart, a deploy, days later. A resource held
across that gap is not one anybody can rely on, so a using scope may not contain one:
using (var lease = Lease.Take()) {
await Workflow.Run("ship", order); // ✗ a disposable can't be held across a durable wait;
} // close it before waiting, or open it again afterClose it before the wait, or open it again after. Ordinary calls inside a using — HTTP, reads, writes, other
functions — are fine; only the waits that suspend the run are refused. As in .NET, a process that dies mid-block
skips Dispose(): write Dispose() so the far side expires what it leaked (a session timeout, a lease's TTL).
Not the using at the top of a file#
using Osysharp.Http; at the top of a file is the namespace import it looks like — a different construct that
happens to share the word. And await using var saga = Workflow.BeginSaga(); is the saga's compensation scope, not
resource disposal; see the workflow docs.
See also#
- try / catch / finally — the
finallyausinglowers to, and what it guarantees - Interfaces — a contract several types can satisfy — implementing a contract,
IDisposableincluded - The C# Osy# leaves out — and why — the C# that Osy# leaves out, and why
- Classes — classes are reference types, which is why
Dispose()can update the object you passed in