Skip to content

v3.1.0

Choose a tag to compare

@windischb windischb released this 17 Apr 15:51
· 47 commits to develop since this release

Cocoar.JsEval v3.1.0

Optional chaining, nullish coalescing, and a DI lifetime fix.

A focused follow-up to v3.0.0 based on authoring friction surfaced by a real-world ABAC-style authorization project built on Cocoar.JsEval.Linq. The script-syntax reference in that project's docs claimed ?. and ?? were supported — they weren't. Rather than correct the docs, we fixed the library. Both features slot into the translator without touching the hot path, and they make null-safe predicates authorable rather than boilerplate.

Highlights

  • Optional chaining (?.) in predicates. Each ?. step short-circuits the rest of the chain to null if the guarded target is null. Works in both SQL translation and in-memory Expression.Compile().
  • Nullish coalescing (??) in predicates. Maps to Expression.Coalesce. Composes naturally with ?..
  • JsEngine DI lifetime is now scoped (was transient). One engine per DI scope, shared across collaborators — matches the "Jint is not thread-safe" contract.

Null-safe predicates, without the ceremony

Before v3.1, writing a null-safe predicate meant explicit != null && … chains:

// v3.0 — verbose but explicit
(u) => u.Address != null && u.Address.City != null && u.Address.City.startsWith('V')

With v3.1:

// Optional chaining short-circuits the rest of the chain on null
(u) => u.Address?.City.startsWith('V') === true

// Nullish coalescing for fallback values
(u) => (u.Address?.City ?? '') === 'Vienna'

// Both combine with every existing translator feature — linq.*, enums, method maps, closures, etc.
todos.where(t => (t.Customer?.Tier ?? 'none') === user.Tier)

What the translator emits

  • u.Address?.City → u.Address == null ? null : u.Address.City (result type nullable)
  • Nested chains like a?.b?.c wrap outermost-first: a == null ? null : (a.b == null ? null : a.b.c) — so the outer guard short-circuits past the inner
  • Non-nullable value-type guards are skipped (the target can never be null — e.g. u.Id?.ToString() where Id is a Guid) — no wasted Condition node
  • ?? uses Expression.Coalesce directly; left side must be a reference type or Nullable<T> (CLR contract)

For in-memory evaluation (auto-membership recalc, unit-test shims, small IEnumerable filtering), this replaces a try/catch-around-NRE safety net with actual null-safe navigation.

DI lifetime: Scoped, not Transient

Before v3.1, AddJsEval registered JsEngine as transient. Every GetRequiredService<JsEngine>() produced a fresh engine. Two services depending on JsEngine in the same HTTP request would get different engines — and any globals set via SetValue on one would be invisible to the other.

// v3.1: one engine per scope, shared across collaborators
public class AccessPolicyEngine(JsEngine engine) { /* sets globals, runs scripts */ }
public class AutoMembershipRecalc(JsEngine engine) { /* same engine, sees same globals */ }

Jint engines are not thread-safe — so scoped is the correct lifetime: shared within a request pipeline, isolated across concurrent requests.

Migration from v3.0

If you relied on each resolve producing a fresh engine, either:

  • Wrap the work in its own DI scope: using var scope = sp.CreateScope(); var engine = scope.ServiceProvider.GetRequiredService<JsEngine>();
  • Construct explicitly: new JsEngine(sp, moduleRegistry, options, logger)

Most consumers want the new default — this flips the footgun into the safer shape.