The meaning of Raven features
Raven features are modeling tools, not merely alternative spellings for C# constructs. Read Raven code by asking what distinction a construct communicates about the program. Write Raven code by identifying that distinction before choosing syntax.
This guide is intentionally compact enough to use as context for people and coding agents. It describes the semantic intent of common Raven features, the choice each feature makes visible, and the boundary where an ordinary .NET shape may still be more appropriate.
The central rule is:
Model the meaning first. Select the Raven construct that represents that meaning second.
Expression-oriented does not mean expression-only
Raven makes expressions the usual way to compose computations. Blocks and
control flow such as if and match can produce values, so a decision can
appear directly in an initializer, argument, or return value.
The language still distinguishes expressions from statements in both syntax
and semantics. Similar-looking if or match constructs may be parsed as
different expression and statement nodes depending on their position. Loops
are statements in the current language. A lexical binding is also necessarily
a statement:
let x = 2 + a
let description = if count == 0 { "empty" } else { "populated" }
In the first line, 2 + a is an expression, but let x = 2 + a is a
declaration statement with no equivalent binding-expression form. In the
second line, the whole line is likewise a lexical-binding statement; its
initializer is an if expression, and each branch produces the value bound to
description.
Expression-oriented programming reduces the need for temporary mutation and statement-only control flow; it does not erase declarations, effects, jumps, or other inherently statement-shaped operations.
For larger examples and established pattern names, continue with Domain modeling in Raven. The Raven for C# developers guide compares common starting shapes.
Start with the distinction the program needs
Before designing a type or function, identify which of these questions the program must answer:
| Question | Meaning to represent | Good Raven starting point |
|---|---|---|
| Is this one value, or may it be absent? | Meaningful presence or absence | Optional<T> |
| Did an expected operation succeed or fail? | A typed success or failure channel | Result<T, E> |
| Can a value be one of several known alternatives? | A closed semantic domain | union |
| Are the alternatives only names without data? | A closed set of constants | enum |
| Does data have structural value meaning? | Equality by represented value | record struct or record class |
| Does something have identity, state, or a lifecycle? | An object that persists through change | class |
| Is this behavior just a transformation? | An operation without an owning object | Plain function |
| Is a dependency one operation? | A single required capability | Function parameter |
| Must outside code add implementations? | An open behavioral contract | interface |
| Must every known alternative be handled? | A complete interpretation | Exhaustive match |
These are starting points, not mechanical rules. A real application normally uses several shapes together.
Bindings communicate whether a name changes
let means that a lexical binding will not be reassigned. var means that
reassignment is part of the algorithm.
let requestId = Guid.NewGuid()
var attempts = 0
attempts = attempts + 1
This is a statement about the binding, not deep immutability. A let
binding may refer to a mutable object, and that object may still change.
Use let by default because a stable name gives readers and tools a stronger
local fact. Use var when changing the binding itself expresses the intended
state transition.
Type inference has a related meaning: an omitted annotation says that the initializer already determines the type without losing useful intent. Raven remains statically typed; inference does not make a value dynamic.
Records represent values
A record says that the represented data is the important identity of the value. Records are good starting points for immutable data, messages, DTOs, and domain value objects.
record struct Money(Amount: decimal, Currency: Currency)
record class OrderSummary(
val Id: Guid,
val Total: Money)
Choose a record struct when value-type storage and semantics fit. Choose a record class when reference storage fits while structural value behavior remains meaningful.
A record wrapper can also give a primitive a domain identity:
record struct CustomerId private (Value: Guid) {
static func From(value: Guid) -> CustomerId => CustomerId(value)
}
CustomerId means more than Guid, even though it uses a Guid as its .NET
representation. It prevents unrelated identifiers from being exchanged merely
because their storage types happen to match.
Do not use a record for an object whose identity must remain stable while its state changes independently of structural equality. That meaning belongs to a class.
Classes represent identity, ownership, and lifecycle
A class says that an object boundary matters. It may own mutable state, a resource, an invariant, or an identity that persists as its data changes.
class Customer(
val Id: CustomerId,
var DisplayName: string) {
func Rename(displayName: string) {
DisplayName = displayName
}
}
Raven does not treat classes as an interoperability fallback. They remain the right model for entities, stateful services, connections, caches, UI components, and resource owners.
What Raven removes is the requirement to place every operation inside a class. Create an object when the problem contains an object boundary, not merely to provide a home for a function.
Nominal unions represent closed alternatives
A nominal union says that a value belongs to one of a known set of alternatives. Each case may carry exactly the data that is valid for that alternative.
union DeliveryStatus {
case Pending
case Delivered(at: DateTimeOffset)
case Failed(reason: string)
}
Nominal unions come in the case-declaration form shown above and a parenthesized
form that gives a union of existing types a declared name. When the alternatives
do not need a new domain name, write an ad-hoc union directly, such as
string | null.
Case payloads may instead be positional when their names add no domain meaning,
for example case Some(T) or case Pair(int, string). Positional cases remain
distinct generated record-like case types with public Value or
Item1/Item2 members for .NET interoperability. Keep names when they explain
the domain; omit them when the case name and position already communicate the
shape. Ordinary records continue to require names because their promoted
properties are the product type's public meaning.
This is not just a shorter enum or class hierarchy. It encodes a closed semantic domain:
Pendinghas no delivery time or failure reason.Deliveredmust have a delivery time.Failedmust have a reason.- no value can accidentally combine incompatible fields.
Use an enum when the cases are only named constants. Use an interface or open class hierarchy when external code must add new alternatives. Use a sealed hierarchy when the alternatives form a closed family but benefit from separate .NET type identities and implementations.
Target-typed case construction such as .Delivered(now) does not change that
meaning. The leading dot says that the surrounding expected type supplies the
union whose case is being constructed:
let status: DeliveryStatus = .Delivered(DateTimeOffset.UtcNow)
It is contextual construction, not a globally scoped case name.
match represents a complete interpretation
A match gives meaning to the alternatives of a value. With a closed domain,
exhaustiveness checking connects the decision directly to the model.
func Describe(status: DeliveryStatus) -> string {
return status match {
.Pending => "Pending"
.Delivered(let at) => "Delivered at $at"
.Failed(let reason) => "Failed: $reason"
}
}
This is more than concise branching. It states that Describe interprets the
whole DeliveryStatus domain. If another status is added, the compiler can
identify decisions that have not assigned it meaning.
Patterns also establish facts. A successful pattern may prove a case, extract its payload, narrow a type, or prove a value non-null. Prefer a pattern when subsequent code depends on one of those facts rather than repeating casts, property checks, or null suppression.
Optional<T> represents meaningful absence
Optional<T> says that producing no value is an ordinary, expected result:
func FindCustomer(id: CustomerId) -> Optional<Customer> {
// A customer may legitimately be absent.
return None
}
Some(customer) and None make presence and absence explicit alternatives.
The caller must decide what absence means in its context.
Use T? when nullability is part of a .NET API, storage representation, or
object model. Use Optional<T> in domain-facing APIs when absence is a meaningful
outcome that callers should handle explicitly. Raven supports conversions at
appropriate nullable interoperability boundaries; the two forms should not be
treated as conceptually identical.
Nullability is an explicit interoperability boundary
Raven does not expect nullable storage to organize an application. At a .NET boundary, accept the platform signature faithfully, then eliminate the nullable state as close to that boundary as practical:
func FindRequiredCustomer(id: CustomerId) -> Result<Customer, LookupError> {
let customer = DotNetStore.Find(id)
match customer {
null => .Failure(.MissingCustomer(id))
let value => .Ok(value)
}
}
After that conversion, domain code should normally use exhaustive patterns over
Optional, Result, unions, or other explicit states. This keeps absence and
failure visible in the model instead of carrying a nullable reference through
unrelated code.
Null is a state that Raven code handles explicitly. Prefer a typed conditional binding when the branch needs the present value:
func Inspect(x: string?) -> unit {
if let str: string = x {
let len = str.Length
}
}
A direct type pattern is also explicit about the successful type and binding:
if x is string str {
let len = str.Length
}
Raven also supports the familiar direct check as a condition:
if x is not null {
// x remains string?
}
That last form is valid, but it does not implicitly refine the checked storage.
Documentation and examples present binding forms first because they introduce
a distinct non-nullable str: string local and make the successful value
explicit. Raven retains .NET-compatible escape hatches such as postfix ! for
interop, but they do not replace explicit handling in idiomatic Raven code.
Raven keeps four responsibilities distinct:
- Reachability analysis is core control-flow semantics. It determines whether a block, branch, arm, or statement can execute and which paths can complete normally. Other analyses may consume that graph, but they should not independently invent reachability rules.
- Assignment analysis is core variable semantics. It determines whether a variable is definitely assigned before use and whether an immutable binding is assigned again.
- Explicit nullness, dereference checks, and .NET metadata are core type
semantics. At every source
position where null is intentionally admitted—including locals and
parameters—the semantic type for
T?remains a nullable wrapper aroundT. The compiler preserves that unified representation, validates conversions and constraints, rejects unsafe dereferences known directly from the declared state, and imports and emits the corresponding platform annotations. - Patterns produce values with explicit types. A successful typed binding introduces a new non-null value within its scope. It does not change the type of the original storage location.
Raven deliberately has no C#-style nullable flow state layered on top of these
rules. A member-access decision follows directly from the receiver's static
type. This applies equally to locals, parameters, properties, and mutable
storage and keeps TypeInfo independent of query order and control-flow state.
A practical adoption sequence is:
- preserve and enforce the existing .NET nullable annotations;
- introduce local patterns that handle every nullable outcome;
- project recurring domain outcomes into
Optional,Result, or a union; - keep null concentrated near the remaining interop and storage boundaries.
The canonical user-facing policy is in Nullability and absence.
unit has a different meaning again: it represents no meaningful return value,
not an absent value.
At a callable boundary, a unit-returning function still discards a
non-unit tail value. The RAV9034 analyzer diagnostic warns by default when
that value looks like a result. Assign the expression to _ when
discarding it is deliberate:
func Refresh(cache: Cache) {
_ = cache.TryRefresh()
}
This makes unit an explicit statement about value flow rather than only
another spelling for CLR void.
Result<T, E> represents expected failure
Result<T, E> says that failure is an expected outcome of a correctly used
operation and that callers may need to make a decision based on it.
union RegistrationError {
case DuplicateEmail(email: string)
case InvalidName
}
func Register(request: RegistrationRequest)
-> Result<Customer, RegistrationError> {
// ...
return Failure(.InvalidName)
}
Use an error union when different failures carry different meaning. An exception remains appropriate for unexpected faults, broken invariants, and .NET APIs whose contract is exceptional.
The ? propagation operator means: if this result is successful, continue with
its value; if it contains the compatible failure channel, return that failure
from the current function.
func CreateCustomer(request: RegistrationRequest)
-> Result<Customer, RegistrationError> {
let validated = Validate(request)?
return Save(validated)
}
The operator keeps the success path linear, but it does not erase the failure.
The function signature continues to expose it. Use an explicit match when
recovery, logging, translation, or branching gives the failure new meaning.
Pattern declarations establish invariants
A pattern declaration with else means that the remainder of the current
scope may rely on the pattern having succeeded. The else branch must leave
that flow.
In an ASP.NET Core endpoint, a nullable database result can become a proven non-null local:
let vehicle: VehicleEntity =
await context.Vehicles.SingleOrDefaultAsync(
candidate => candidate.Id == id,
cancellationToken) else {
return Results.NotFound()
}
return Results.Ok(MapVehicle(vehicle))
The important meaning is not terseness. The declaration establishes the
invariant “from here onward, vehicle exists,” while the failure path remains
at the boundary where it is understood.
Expressions let decisions produce values
Raven control flow is expression-oriented. An if or match can produce the
value assigned or returned by its surrounding context:
let label =
if total == 0 {
"empty"
} else {
"ready"
}
This means the branches are alternative ways to compute one value. Prefer that shape when the branches answer one question. Prefer statement-oriented control flow when the branches primarily perform effects or require early exits.
An if let expression combines that meaning with a successful pattern:
let option: Optional<int> = Some(42)
let value = if let Some(x) = option {
x
} else {
0
}
This says that both outcomes compute the same conceptual value, while x
exists only in the outcome where the pattern proved that the option contains
one. Use it for a local two-outcome decision. Use match when several cases
each carry distinct meaning or should be checked exhaustively.
Functions represent operations without artificial ownership
A plain function says that an operation does not need object identity or owned state:
func CalculateTotal(lines: OrderLine[]) -> Money {
// ...
}
A function parameter says that a caller must provide one capability:
async func LoadSummary(
load: (Guid) -> Task<Optional<Order>>,
id: Guid) -> Task<Optional<OrderSummary>> {
// ...
}
Use a method when the behavior belongs to a type's vocabulary or requires its encapsulated state. Use an interface when the dependency is an open protocol with several related operations. Function-based dependency injection should remove accidental ceremony, not conceal a meaningful service or lifecycle.
Extensions adapt vocabulary
An extension makes an operation readable as part of an existing type's vocabulary without changing that type:
extension HttpClientOperations for HttpClient {
async func Fetch(url: string) -> Task<Result<string, FetchError>> {
// ...
}
}
The meaning is adaptation and composition. An extension does not reopen the original type, grant access to its private state, or create new stored state. Use it for coherent operations that read naturally from the receiver. Keep a plain function when receiver syntax would imply ownership that the operation does not have.
async represents asynchronous execution, not failure
Task<T> and async describe when and how an operation completes. Optional<T>
and Result<T, E> describe what its completion means.
For example:
async func FindCustomer(id: CustomerId)
-> Task<Optional<Customer>> {
// Asynchronous operation with ordinary absence.
}
async func Register(request: RegistrationRequest)
-> Task<Result<Customer, RegistrationError>> {
// Asynchronous operation with expected failure.
}
Do not use null or exceptions merely because an operation is asynchronous.
Execution and outcome are separate modeling dimensions.
Likewise, use communicates ownership of a disposable lifetime. In an async
context it prefers asynchronous disposal when available. It means that the
current scope is responsible for ending the resource's lifetime.
Choose metaprogramming by the structure you need
Metaprogramming mechanisms are not interchangeable. Choose one by identifying which representation must be available and when it must be available:
| Mechanism | Structure it exposes | Typical phase | Choose it when |
|---|---|---|---|
Raven.CodeAnalysis |
Raven syntax and compiler semantics | Build time or tooling time | A tool must analyze, diagnose, navigate, or transform Raven code |
| .NET expression trees | Standardized, typed System.Linq.Expressions operations |
Language conversion into a runtime API value | A library must inspect, rewrite, translate, or compile a supplied operation |
| .NET reflection | Emitted types, members, attributes, and metadata | Runtime | Code must discover or invoke compiled program structure |
| Raven macros | Authored syntax transformed through compiler expansion | Compile time | A reusable language abstraction must generate or reshape the program itself |
Raven compile! |
Raven expression syntax compiled to a delegate | Macro expansion at compile time; syntax construction and compilation at runtime | A program must generate or alter Raven syntax and then execute it as a typed delegate |
Raven's macro system is currently a work in progress. Use it for experimentation with compile-time transformations, but expect the model and APIs to evolve. See Macros for its current status and intended use.
Prefer the least powerful representation that directly matches the use case. An ordinary generic, function, delegate, interface, or attribute is clearer when no code or program structure needs to be inspected or transformed.
Expression trees represent typed operations as data
An ordinary API call passes values and asks the callee to perform behavior. Reflection inspects compiled types, members, and metadata. A .NET expression tree occupies a useful middle position: it is an API value containing a typed graph of operations that another component can inspect, rewrite, translate, or compile.
let onlyActiveAdults: Expression<Func<User, bool>> =
user => user.IsActive && user.Age >= minimumAge
The target type asks the Raven compiler to convert the lambda's supported
operations into an Expression<Func<User, bool>> object instead of emitting
only an opaque delegate. An API such as IQueryable can inspect that operation
graph and translate the predicate into another language, such as SQL. The tree
can also be compiled into a Func<User, bool> delegate and executed by the
running program. This is why an expression-tree parameter means something
different from a delegate parameter, even though both describe a predicate
with the same input and result types.
Expression trees are also distinct from Raven macros. A macro transforms authored syntax during compilation and emits ordinary program syntax or bound behavior. An expression tree converts supported operations into a structured runtime value that the receiving API controls. Reflection starts from emitted program structure; an expression tree starts from an operation deliberately converted by the language compiler.
An expression tree is not a Raven syntax tree and does not preserve syntax.
Raven.CodeAnalysis represents Raven's own authored and semantic structure for
compiler tooling: syntax nodes, symbols, types, diagnostics, and operations. An
expression tree instead uses the standardized System.Linq.Expressions
concepts understood by .NET libraries. It contains no Raven syntax nodes,
tokens, trivia, or original source form.
That standardized operation vocabulary is intentionally narrower than Raven's language semantics, and narrower than the complete semantics of any other .NET language. It captures a shared set of common programming operations together with .NET-specific concepts. A compiler can convert only language constructs that have a valid representation in that object model. Use the code-analysis APIs to understand Raven code; use expression trees to exchange supported abstract operations with a .NET API.
Use an ordinary function or delegate when the receiver only needs to execute behavior. Use an expression tree when the receiver must understand or translate the behavior. Use reflection when the problem is discovering or operating on compiled program structure. Use a macro when the program itself must be transformed during compilation.
Reflection does not by itself make an untrusted assembly safe. When inspection
is the goal, prefer metadata-only facilities such as MetadataLoadContext or
System.Reflection.Metadata, which do not load the assembly for execution.
Loading an assembly into the application's execution context, invoking a
reflected member, or instantiating attributes crosses into executing its code
inside the current process. Treat that boundary with the same care as runtime
source compilation, and isolate or reject untrusted inputs according to the
application's threat model.
Raven's expression-tree support is currently incremental rather than complete. Consult the repository's expression-tree support status before relying on a particular body shape or operator.
Expression trees and #quote quote different representations
A target-typed expression-tree lambda is a form of compiler-integrated quotation. The programmer writes an ordinary lambda, and the compiler produces an object graph of its supported operations:
let predicate: Expression<Func<User, bool>> =
user => user.IsActive
Raven's quote! intrinsic applies the same broad idea to Raven syntax. The
programmer writes a Raven fragment, and the compiler produces the
ExpressionSyntax construction needed to recreate that fragment:
let syntax: ExpressionSyntax = quote! {
left + right
}
Both mechanisms avoid starting with source text, invoking a parser, or manually assembling the entire initial object graph. Both produce ordinary objects that code can traverse and rewrite. Because the object models are immutable, “modifying” a quoted tree means constructing a new tree from the original, through visitors, replacement APIs, factories, or quote holes.
The representations and destinations are different:
| Quotation | Result | Retains | Primary destination |
|---|---|---|---|
| Expression-tree conversion | Expression<TDelegate> operation graph |
Supported standardized .NET operations and types | Runtime libraries that translate, inspect, or compile operations |
Raven #quote |
Raven ExpressionSyntax tree |
Raven syntax, tokens, and trivia | Macros and tools that generate or transform Raven program structure |
#quote is therefore more expressive for Raven program generation than
expression trees: it is not limited to the shared
System.Linq.Expressions operation vocabulary. That does not make expression
trees obsolete. A .NET API expecting Expression<TDelegate> needs precisely
that portable operation representation, while a Raven macro normally needs
Raven syntax.
The authored quote surface can support only the syntax categories and splice
forms implemented by the compiler. Once a syntax object exists, however, macro
code can inspect it and construct a transformed tree using the full compatible
Raven.CodeAnalysis syntax API. Syntax quotation does not bind names or types;
the generated syntax acquires semantic meaning only after insertion into a
compilation and normal binding.
compile! reconnects Raven syntax to executable behavior
quote! deliberately stops at Raven syntax. compile<TDelegate>! uses the
same quotation and hole model, then asks the Raven runtime compiler to bind and
emit that syntax as a strongly typed delegate:
let increment = compile<System.Func<int, int>>! {
value => #(SyntaxFactory.IdentifierName("value")) + 1
}
This is useful when Raven syntax itself is the representation that must be
generated or transformed. Unlike an expression tree, the intermediate value
can retain Raven tokens and trivia and can represent Raven syntax outside
System.Linq.Expressions' standardized operation vocabulary. Unlike a normal
compile-time macro expansion, the final binding and emission occur while the
program runs.
The stages provide different assurances. Quotation parses the authored macro
body immediately, so malformed or incomplete Raven structure is rejected
during the containing compilation. compile! then takes the final syntax,
including runtime hole values, through Raven parsing, binding, and emission.
It returns the requested delegate only when that code is semantically
consistent; otherwise it throws a RavenCompilationException containing the
compiler diagnostics.
That extra power has a cost. Runtime compilation produces and loads an assembly, so compile once and reuse the returned delegate. It is also a code execution boundary: do not compile syntax influenced by untrusted input without an application-specific security boundary. Prefer an ordinary delegate when only execution matters, and prefer an expression tree when a .NET API expects its portable operation model.
Preserve ordinary .NET boundaries
Raven code should remain native to the .NET ecosystem. Do not replace a framework's public vocabulary solely to make every boundary look Raven-specific.
A useful architecture is:
- Accept the shapes expected by ASP.NET Core, Entity Framework Core, and other .NET libraries.
- Translate boundary representations into honest domain types.
- Use Raven records, unions, options, results, patterns, and functions inside the domain and application workflow.
- Exhaustively translate domain outcomes back to framework results.
For example, an ASP.NET endpoint may accept a mutable request model required by
model binding, create validated record values, call a function returning a
domain Result, and translate its error union into Results.BadRequest,
Results.NotFound, or Results.Conflict.
An EF entity may remain a conventional mutable class while its domain status is a Raven union. Persistence then uses an explicit conversion when the provider cannot store that union directly. This preserves both the framework contract and the domain meaning.
Decision rules for coding agents
When generating or changing Raven code:
- Describe the domain distinction before selecting a construct.
- Identify the .NET or storage boundaries before changing their types.
- Use
Optionalfor meaningful absence andResultfor expected failure; do not treat them as stylistic replacements for every nullable value or exception. - Use a union only when the alternative set is intentionally closed.
- Use exhaustive
matchto interpret closed alternatives. Avoid a catch-all when named cases should force future code changes. - Prefer records for values and classes for identity, lifecycle, or encapsulated mutation.
- Prefer plain functions for ownerless operations and function parameters for single capabilities.
- Keep framework-facing signatures recognizable to .NET unless a documented Raven projection or adapter applies.
- Before choosing metaprogramming, identify the required structure and phase: Raven source and semantics for CodeAnalysis, standardized operations for expression trees, emitted metadata for reflection, or compile-time transformation for macros.
- Do not infer Raven support from similar C#, F#, Rust, or Swift syntax. Consult the relevant feature guide and compile the code.
- Treat compiler diagnostics as semantic feedback. Fix the modeled mismatch instead of hiding it with casts, null suppression, or a broad catch-all.
The desired result is not code that uses the largest number of Raven features. It is code whose types and control flow make the problem's meaning visible.
Working examples
The following repository samples demonstrate these meanings in ordinary .NET workloads:
- ASP.NET Core Minimal API uses unions as request and response shapes with OpenAPI generation.
- EF Core vehicle costs combines ASP.NET Core, EF Core entities, domain value records, union state, pattern declarations, and explicit persistence adapters.
- EF Core expression
trees
demonstrates a typed, inspectable predicate passed through an ordinary
IQueryableAPI. - Raven Core documents the precise
Optional<T>andResult<T, E>contracts and framework API projections. - Language features provides focused explanations and examples for everyday syntax and behavior.