FSharp.Interop.Dlr 1.0.0-rc.2

This is a prerelease version of FSharp.Interop.Dlr.
dotnet add package FSharp.Interop.Dlr --version 1.0.0-rc.2
                    
NuGet\Install-Package FSharp.Interop.Dlr -Version 1.0.0-rc.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FSharp.Interop.Dlr" Version="1.0.0-rc.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FSharp.Interop.Dlr" Version="1.0.0-rc.2" />
                    
Directory.Packages.props
<PackageReference Include="FSharp.Interop.Dlr" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FSharp.Interop.Dlr --version 1.0.0-rc.2
                    
#r "nuget: FSharp.Interop.Dlr, 1.0.0-rc.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FSharp.Interop.Dlr@1.0.0-rc.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FSharp.Interop.Dlr&version=1.0.0-rc.2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=FSharp.Interop.Dlr&version=1.0.0-rc.2&prerelease
                    
Install as a Cake Tool

<img src="icon.png" alt="" width="96" align="right">

FSharp.Interop.Dlr

CI Tests Line coverage Branch coverage License

Experimental: not on NuGet yet, and the spelling of the API may still change.

C#'s dynamic, for F#. Inside a dlr { } block, x?Name, x?Name(a, b) and x?Name <- v compile to what C# emits for d.Name, d.Name(a, b) and d.Name = v on a dynamic: one Microsoft.CSharp call site per operation, created once, dispatching on the target's runtime type. Same binder, same behaviour: C# overload resolution, named arguments, implicit conversions, ExpandoObject / DynamicObject / IDynamicMetaObjectProvider, and RuntimeBinderException when a bind fails. A block costs a few nanoseconds over C# dynamic after its first call and allocates nothing itself (the numbers are under Measured).

open FSharp.Interop.Dlr
open Newtonsoft.Json.Linq

/// An order as Newtonsoft parsed it, and the pricing plugin the host loaded — both `obj`.
[<ReflectedDefinition>]                       // on the function that holds the blocks (see below)
let invoice (order: obj) (pricing: obj) =
    let customer: string = dlr { return order?customer?name }
    let city: string = dlr { return order?customer?address?city }
    let first: decimal = dlr { return order?lines |> Dlr.item 0 |> Dlr.get "price" }
    let total: decimal = dlr { return pricing?Total(order?lines, Dlr.named {| currency = "EUR" |}) }
    dlr { order?status <- "invoiced" }
    let discount: decimal =
        dlr {
            let mutable sum = 0m
            for line in (order?lines : JArray) do sum <- sum + (line?price : decimal)
            return pricing?Discount(customer, sum)
        }
    customer, city, first, total, discount

The recursive step can be dynamic too:

[<ReflectedDefinition>]
let depth (root: obj) : int =
    dlr {
        let rec depth (node: obj) : int = if isNull node then 0 else 1 + depth node?child
        return depth root
    }

Both run in Tests/Readme.fs; every form is in docs/syntax.md.

Targets netstandard2.0 and net10.0; depends on FSharp.Core ≥ 10.1.201 — its expression converter is the first that handles a block's statements. Build with .NET SDK 10: the SDK 8 and 9 compilers (both end of support in November 2026) are not supported — in Debug they warn FS3511 on every block. The app itself may run on anything netstandard2.0 covers, .NET Framework included.

The suite also runs against real dynamic targets — Newtonsoft JObject, IronPython, Python.NET, Dapper rows over SQLite, ClearScript V8, PowerShell PSObjects, COM IDispatch on Windows (Scripting.FileSystemObject, WScript.Shell, Scripting.Dictionary, ADO's Recordset events, Stream, and Connection against SQL Server LocalDB for an [out]), and NLua through a short DynamicObject adapter (Tests/NLua.fs, the pattern for any late-bound API without a DLR face) — and on browser-wasm.

Scope

dlr { } is C# dynamic for F#:

  1. Parity with C# dynamic: the same binders, so it binds what C# binds and fails where C# fails, restrictions included (restrictions).
  2. F# values C#'s binder does not understand (functions, F# optional parameters, structural equality): binding rules of its own only where C# would fail or bind against F#'s expectation (the seam).
  3. What F#'s spelling exposes: a member name or type arguments from a variable, a tuple as several arguments, each cached per site (syntax).
  4. Mindful of speed: C#'s call sites, bound once (benchmarks).

Outside the scope: reaching members the binder would not (a static-member-access API, private members beyond the accessibility rules), reflection conveniences, and language features dynamic has no counterpart for. If something is awkward in F# but C# dynamic cannot do it either, the answer is usually a static call.

Installing

dotnet add package FSharp.Interop.Dlr
dotnet add package FSharp.Interop.Dlr.Analyzers   # optional: build-time checks

Releases are on nuget.org. Every push to master also publishes a prerelease (<next>-alpha.0.<height>) to the ekonbenefits GitHub Packages feed, which needs a token even to read (a PAT with read:packages):

dotnet nuget add source https://nuget.pkg.github.com/ekonbenefits/index.json \
  --name ekonbenefits --username <github-user> --password <token> --store-password-in-clear-text
dotnet add package FSharp.Interop.Dlr --prerelease

Where the attribute goes

The library reads a block's body from the reflected definition of the function or member around it, so that function carries [<ReflectedDefinition>]:

[<ReflectedDefinition>]
let total (rows: obj) : decimal = dlr { return rows?Sum("Amount") }

type Report(data: obj) =
    [<ReflectedDefinition>]
    member _.Total: decimal = dlr { return data?Total }

On the function or member. A module- or type-level attribute also works, but it stores a quotation of everything under it, and ordinary F# often has no quotation form — inner generic functions, byrefs, Span — so prefer the binding. If the function around a block cannot be quoted, move the block into the smallest function that can.

Without the attribute the first call raises a DlrTranslationException that says so; the analyzer package reports it at build time instead, with a fix, along with the other misuses it can see (DLR002–DLR006).

One block per source line. Blocks in generic functions and members work (one site per instantiation); so do nested blocks, blocks inside task { } / async { }, and F# Interactive.

Syntax

The common forms; docs/syntax.md has every one, with what each binds to.

x?Name get, converted to the inferred type
x?Name(a, b) · x?Name() call; arguments keep their static types, box a dispatches on the runtime type
x?Name(a, Dlr.named {\| p = v \|}) · Dlr.namedOf kw · Dlr.argsOf xs named arguments, as a record literal or from data; positional ones from data
x?Name <- v set
x \|> Dlr.item i · x \|> Dlr.setItem (i, j) v indexers
x \|> Dlr.get "Name" · Dlr.invoke "Name" (a, b) · Dlr.set "Name" v the same three with the target last, for pipelines
x?TryGetValue(k, Dlr.out) · Dlr.outAs<T> () · x?Swap(Dlr.ref a, Dlr.ref b) out / ref parameters: outs returned as F# returns them (let (ok: bool), (v: int) = …; Dlr.outAs states an out's type where the shape is ambiguous), refs written back to a let mutable
x \|> Dlr.addAssign "Click" handler · Dlr.subtractAssign += / -=: an event handler (an F# function converts) or read-modify-write
?+? ?-? … ?=? ?<? … operators, converted to the inferred type (comparisons to bool)
Dlr.cast<T> x · Dlr.implicit x explicit / implicit conversion
Dlr.Static<T>.Overloads?Name(a) · Dlr.new'<T>(a) static overload / constructor chosen by the arguments' runtime types
let f: int -> int -> int = dlr { return x?Add } a member read as an F# function
Dlr.call f (a, b) · Dlr.call f typed A -> R invoke the value itself, the ? of values; read as a function, it is one
f \|> Dlr.apply (a, b) the same, target last for pipelines

A target need not be obj: let w = Widget() then w?Count upcasts it and binds on the runtime type as box w would. A member name may be a variable ((?) x name), and so may the type-argument list (Dlr.typeArgsOf ts); each distinct value gets its own call sites, cached per site. Around the markers, ordinary F#: let, let rec, let mutable, use, if, match, for, while, try, lambdas, sprintf. A RuntimeBinderException can be caught inside the block.

How it works

In Release the compiler turns each block into a struct state machine whose fields are its captured variables (in Debug, where it builds none, the block's closure plays that part: a little slower, the same results). On the first call the block's body is read from the enclosing [<ReflectedDefinition>], translated into an expression tree with one C# call site per operation, and compiled to a delegate over that struct; every later call is a field read and an invoke.

docs/internals.md indexes the full picture: every cache, every site and its argument flags, the F#-aware binders, and what the translator assumes about the compiler.

Measured

Steady state, Release, net10.0, Apple Silicon. Benchmarks/bench.sh docs regenerates this table and the full docs/benchmarks.md (every suite, allocations, real targets).

ns per call static C# dynamic dlr { } reflection (cached) FSharp.Interop.Dynamic
method call w.Add(i, 1) 1.2 7.4 11.5 35.7 7,485
property get w.Count 0 6.7 10.8 12 3,942
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  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 was computed.  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 was computed.  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 was computed.  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 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
1.0.0-rc.2 60 10/6/2026
1.0.0-rc.1 66 10/6/2026