Test and coverage badges are the last green CI run on master. CI fails the PR if any test is skipped, line coverage drops below 77%, or branch coverage drops below 100%.
F# operators for the Dynamic Language Runtime. target?Name, target?Name <- value, and !?target are the F# spelling of C# dynamic, with piping and 'T option lookups.
Docs: fsprojects.github.io/FSharp.Interop.Dynamic
This library sits on Dynamitey.Community 4.0.0. It is the F# surface, not Dynamitey itself.
F# has no dynamic keyword. Use this when the member is not known at compile time: Expando/JSON bags, optional fields, method names from config, C# APIs that return dynamic, pythonnet, COM. Use ordinary F# when the type is in your project. Full argument: Why this library.
7.0.0 is the current package, and the last one that supports .NET Standard 2.0. That target is what reaches .NET Framework 4.6.1 through 4.8.1. It also builds for net10.0, on Dynamitey.Community 4.0.0. The namespace is still Dynamitey. The assembly is Dynamitey.Community. Dyn.namedArg, Dyn.staticContext, and Dyn.staticTarget return types from that assembly. A project that also references the Dynamitey 3.0.3 package gets CS0433.
A Framework application stays on 7.0.0. Framework 4.8.1 is still serviced with Windows. It does not get new BCL APIs. .NET Standard 2.0 freezes this library on the Framework 4.6.1 surface. A newer API is either skipped, or written twice. That is the tax. Dynamitey.Community #95 is the same decision for the dependency: 4.0.0 is its last netstandard2.0 release, and 5.0.0 (after .NET 11 is generally available) targets net10.0 and net11.0 only.
8.0.0 (#108, milestone) follows that. The target date is 10 November 2026, the scheduled .NET 11 general availability. It targets net10.0 and net11.0 and drops .NET Standard 2.0, once Dynamitey.Community 5.0.0 has dropped the same target. The gains are in that library, and this package takes them by referencing it:
- One copy of Dynamitey's call-site code. The
netstandard2.0branch,Guard.NotNull, and the attribute polyfills go away.ArgumentNullException.ThrowIfNullis on every target. - Ordinal hashing and
string.Contains(string, StringComparison). The Framework build cannot call those overloads, so it uses a different hash and threeIndexOfcalls. Microsoft.CSharpandSystem.Reflection.Emitleave Dynamitey's package graph. They are in the shared framework on .NET 10 and .NET 11. The more-than-14-argument path still uses Emit. It stops being a separate package.[RequiresUnreferencedCode]and[RequiresDynamicCode]are the real framework attributes on every Dynamitey build. The library stays DLR-based. It does not become trim-safe.
A warm dynamic call does not get faster. It is already a cached delegate.
6.0.0 was netstandard2.0 + net10.0 on Dynamitey 3.0.3, with Dyn.tryGet / Dyn.exists. That was a TFM break from 5.0.1.268 (net45 / netstandard1.6 / netstandard2.0).
dotnet add package FSharp.Interop.Dynamicopen FSharp.Interop.Dynamic
open FSharp.Interop.Dynamic.Operators // optional: ?+?, ?=?, …open System.Dynamic
open FSharp.Interop.Dynamic
let o = ExpandoObject()
o?Name <- "Ada"
let name: string = o?Name
let hello: string = "HelloWorld"?Substring(0, 5)Annotate the result type when F# cannot infer it. Void CLR methods need unit:
let items = ResizeArray<string>()
let _: unit = items?Add("x")let present: string option = o |> Dyn.tryGet "Name" // Some "Ada"
let missing: string option = o |> Dyn.tryGet "NoSuch" // None
o |> Dyn.exists "Name" // true
o |> Dyn.exists "NoSuch" // falseLookup is Dynamitey InvokeGet. A present null is still present. A present value that cannot convert to 'T still throws — that is not a miss.
o |> Dyn.set "Name" "Ada"
let name: string = o |> Dyn.get "Name"
let hello: string = "HelloWorld" |> Dyn.invokeMember "Substring" (0, 5)Target is last so piping works.
open FSharp.Interop.Dynamic.Operators
let n: int = 5 ?+? 4
let f: float = 5 ?+? 3.5
let s: string = "Hello" ?+? " World"
[1; 2; 3; 4] |> List.reduce (?+?)let add3: int -> int = !?(+) 3
add3 4 // 7| You write | DLR |
|---|---|
target?Name when 'T is not a function |
InvokeGet |
target?Name when 'T is a function, then apply |
InvokeMember |
target?Name <- value |
InvokeSet |
!?target |
Invoke on the target itself |
That is why target?Foo(1, 2) is a method call: application forces a function type.
7.0.0 references Dynamitey.Community 4.0.0, from dynamitey-community/dynamitey. The Dynamitey package id stopped at 3.0.3 on 8 November 2023. The namespace is still Dynamitey. More: Dynamitey.
You can still open Dynamitey for Build, Dynamic.Curry, DynamicObjects.Dictionary.
- The DLR cannot see explicit interface members (same as C#
dynamic). - Not trim-safe or NativeAOT-safe.
- 15 or more arguments threw
TypeLoadExceptionon 6.0.0 (Dynamitey 3.0.3). 7.0.0 takes the fix in Dynamitey.Community 4.0.0. - C# optional parameters must all be passed. The DLR does not fill defaults.
tryGet/existson a null target areNone/false.Dyn.seton a null target still throwsNullReferenceException.- Do not build member names from untrusted input. SECURITY.md.
- Historical: .NET Core 2.0.0–2.0.2 broke
dynamicon nested types inside generics (#11). Current TFMs are fine.
Full list: Caveats.
Every F# snippet above is a test in Tests/ReadmeExamples.fs. pythonnet and SignalR sketches live on the why page and are not compiled here.
Requires the .NET 10 SDK.
dotnet restore
dotnet build -c Release -warnaserror
dotnet test --project Tests/Tests.fsproj -c ReleaseDocs: dotnet tool install -g docfx --version 2.78.5, then dotnet build FSharp.Interop.Dynamic/FSharp.Interop.Dynamic.fsproj -c Release && docfx docfx/docfx.json.
Release: add the version's section to CHANGELOG.md, then tag v7.0.0 and push. Releasing.
- Bugs and feature requests: open an issue. Include the F# and .NET versions and a minimal snippet.
- Security problems: do not open a public issue. Follow SECURITY.md.
- Code and docs: pull requests are welcome. CONTRIBUTING.md covers the process and what a PR needs before it merges.
fsprojects default: @fsprojectsgit.