Skip to content

Repository files navigation

FSharp.Interop.Dynamic

NuGet CI Tests Line coverage Branch coverage License

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.


Version story

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.0 branch, Guard.NotNull, and the attribute polyfills go away. ArgumentNullException.ThrowIfNull is on every target.
  • Ordinal hashing and string.Contains(string, StringComparison). The Framework build cannot call those overloads, so it uses a different hash and three IndexOf calls.
  • Microsoft.CSharp and System.Reflection.Emit leave 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).


Install (7.0.0)

dotnet add package FSharp.Interop.Dynamic
open FSharp.Interop.Dynamic
open FSharp.Interop.Dynamic.Operators   // optional: ?+?, ?=?, …

Quick start

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")

Check a member without throwing

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"  // false

Lookup is Dynamitey InvokeGet. A present null is still present. A present value that cannot convert to 'T still throws — that is not a miss.

Pipe through Dyn

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.

Binary operators

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 (?+?)

Direct invoke

let add3: int -> int = !?(+) 3
add3 4  // 7

What ? does

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.

More: Operators, Dyn, tryGet.


Dynamitey

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.


Caveats

  • The DLR cannot see explicit interface members (same as C# dynamic).
  • Not trim-safe or NativeAOT-safe.
  • 15 or more arguments threw TypeLoadException on 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 / exists on a null target are None / false. Dyn.set on a null target still throws NullReferenceException.
  • Do not build member names from untrusted input. SECURITY.md.
  • Historical: .NET Core 2.0.0–2.0.2 broke dynamic on 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.


Build

Requires the .NET 10 SDK.

dotnet restore
dotnet build -c Release -warnaserror
dotnet test --project Tests/Tests.fsproj -c Release

Docs: 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.


Feedback and contributing

  • 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.

Maintainers

fsprojects default: @fsprojectsgit.

About

DLR interop for F# -- works like dynamic keyword in C#

Resources

Contributing

Security policy

Stars

98 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages