Skip to content

Commit 7bfbd5a

Browse files
author
MPCoreDeveloper
committed
push all current changes for SQLITE / Postgre Dialect and the new Functional SQL engine
1 parent 5f78caa commit 7bfbd5a

69 files changed

Lines changed: 4601 additions & 299 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
- Use `xunit.runner.visualstudio` 3.1.5+ for test discovery.
1616
- If you encounter any project referencing `xunit` (without `.v3`), migrate it to `xunit.v3` immediately.
1717
- Test runner: `Microsoft.NET.Test.Sdk` 18.3.0+ (latest stable for .NET 10).
18-
- Prefer targeted, fast test runs instead of broad/long-running full-suite test execution during iterative work.
18+
- Prefer targeted, fast test runs instead of broad/long-running full-suite test execution during iterative work. Query failed tests directly instead of performing broad test scans, especially when a single hanging test is the bottleneck.
1919

2020
## Code Style
2121
- Use specific formatting rules.

Examples/Web/Orchardcore/SharpCoreDb.Orchardcore/SharpCoreDb.Orchardcore.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121
</ItemGroup>
2222
<ItemGroup>
2323
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
24-
<PackageReference Include="MimeKit" Version="4.15.1" />
24+
<PackageReference Include="MailKit" Version="4.16.0" />
25+
<PackageReference Include="MimeKit" Version="4.16.0" />
2526
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.5.0" />
2627
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.6" />
27-
<PackageReference Include="AWSSDK.Core" Version="4.0.3.29" />
28+
<PackageReference Include="AWSSDK.Core" Version="4.0.3.30" />
2829
</ItemGroup>
2930

3031
<ItemGroup Condition="'$(UseLocalSharpCoreDbSources)' == 'true'">

SharpCoreDB.sln

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
Microsoft Visual Studio Solution File, Format Version 12.00
23
# Visual Studio Version 18
34
VisualStudioVersion = 18.1.11312.151
@@ -211,6 +212,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCoreDB.Identity", "src
211212
EndProject
212213
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCoreDB.CrudApp", "Examples\Web\SharpCoreDB.CrudApp\SharpCoreDB.CrudApp.csproj", "{FED5C1D6-0A80-DD7D-6C55-1AD989A759CA}"
213214
EndProject
215+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCoreDB.Functional.Tests", "tests\SharpCoreDB.Functional.Tests\SharpCoreDB.Functional.Tests.csproj", "{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}"
216+
EndProject
214217
Global
215218
GlobalSection(SolutionConfigurationPlatforms) = preSolution
216219
Debug|Any CPU = Debug|Any CPU
@@ -869,6 +872,18 @@ Global
869872
{FED5C1D6-0A80-DD7D-6C55-1AD989A759CA}.Release|x64.Build.0 = Release|Any CPU
870873
{FED5C1D6-0A80-DD7D-6C55-1AD989A759CA}.Release|x86.ActiveCfg = Release|Any CPU
871874
{FED5C1D6-0A80-DD7D-6C55-1AD989A759CA}.Release|x86.Build.0 = Release|Any CPU
875+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
876+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
877+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Debug|x64.ActiveCfg = Debug|Any CPU
878+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Debug|x64.Build.0 = Debug|Any CPU
879+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Debug|x86.ActiveCfg = Debug|Any CPU
880+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Debug|x86.Build.0 = Debug|Any CPU
881+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
882+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Release|Any CPU.Build.0 = Release|Any CPU
883+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Release|x64.ActiveCfg = Release|Any CPU
884+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Release|x64.Build.0 = Release|Any CPU
885+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Release|x86.ActiveCfg = Release|Any CPU
886+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA}.Release|x86.Build.0 = Release|Any CPU
872887
EndGlobalSection
873888
GlobalSection(SolutionProperties) = preSolution
874889
HideSolutionNode = FALSE
@@ -942,6 +957,7 @@ Global
942957
{DC85D720-0F4B-4904-8369-753A9365765F} = {A1B2C3D4-E5F6-4A7B-8C9D-0E1F2A3B4C5D}
943958
{09550D6E-8D1A-F43A-0C06-B03F7A2A6193} = {F8B5E3A4-1C2D-4E5F-8B9A-1D2E3F4A5B6C}
944959
{FED5C1D6-0A80-DD7D-6C55-1AD989A759CA} = {50B921FA-F081-4B7B-A8E0-AF3DCC098CCC}
960+
{87E3DF4B-8BF3-45CC-85A0-218418ED54AA} = {A1B2C3D4-E5F6-4A7B-8C9D-0E1F2A3B4C5D}
945961
EndGlobalSection
946962
GlobalSection(ExtensibilityGlobals) = postSolution
947963
SolutionGuid = {F40825F5-26A1-4E85-9D0A-B0121A7ED5F8}

SharpCoreDB.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
<Project Path="tests/SharpCoreDB.DemoJoinsSubQ/SharpCoreDB.DemoJoinsSubQ.csproj" />
125125
<Project Path="tests/SharpCoreDB.EntityFrameworkCore.Tests/SharpCoreDB.EntityFrameworkCore.Tests.csproj" />
126126
<Project Path="tests/SharpCoreDB.EventSourcing.Tests/SharpCoreDB.EventSourcing.Tests.csproj" />
127+
<Project Path="tests/SharpCoreDB.Functional.Tests/SharpCoreDB.Functional.Tests.csproj" />
127128
<Project Path="tests/SharpCoreDB.Profiling/SharpCoreDB.Profiling.csproj" />
128129
<Project Path="tests/SharpCoreDB.Projections.Tests/SharpCoreDB.Projections.Tests.csproj" />
129130
<Project Path="tests/SharpCoreDB.Provider.Sync.Tests/SharpCoreDB.Provider.Sync.Tests.csproj" />

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- `ColumnInfo.IsHidden` property for metadata-driven schema tools.
1717
- `PersistenceConstants.InternalRowIdColumnName` constant (`"_rowid"`).
1818
- 9 dedicated tests for the Auto-ROWID feature in `AutoRowIdTests.cs`.
19+
- **GRAPH_RAG SQL clause**: New top-level `GRAPH_RAG` SELECT clause with `LIMIT`, `WITH SCORE > X`, `WITH CONTEXT`, and `TOP_K` options, plus provider-based execution integration via `IGraphRagProvider`.
20+
- **OPTIONALLY SQL projection mode**: New `OPTIONALLY` keyword after SELECT list enabling `Option<T>` mapping in ADO.NET readers, integrated with `SharpCoreDB.Functional`.
21+
- **SOME/NONE predicates**: New `IS SOME` and `IS NONE` predicates (and NOT variants) supported in parser and runtime evaluators.
1922

2023
### Fixed
2124
- Unified `IS NULL` / `IS NOT NULL` behavior across runtime scan, join-helper, and compiled predicate paths.

docs/FEATURE_MATRIX_v1.7.0.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This page consolidates the major SharpCoreDB capabilities by package for quick d
66

77
| Package | Purpose | Key capabilities in v1.7.0 |
88
|---|---|---|
9-
| `SharpCoreDB` | Embedded core engine | AES-256-GCM encryption, SQL engine, ACID + WAL, indexing, FTS, SIMD optimizations, metadata durability fixes, compiled-query parser fixes |
9+
| `SharpCoreDB` | Embedded core engine | AES-256-GCM encryption, SQL engine, ACID + WAL, indexing, FTS, SIMD optimizations, metadata durability fixes, compiled-query parser fixes, `GRAPH_RAG` SQL clause, `OPTIONALLY` + `IS SOME`/`IS NONE` optional SQL semantics |
1010
| `SharpCoreDB.Server` | Network server runtime | gRPC-first (HTTP/2 + HTTP/3), REST, WebSocket, JWT/RBAC, optional mTLS, multi-database hosting, health/metrics |
1111
| `SharpCoreDB.Client` | .NET client | ADO.NET-style commands/readers, async access, parameterized execution, server connectivity |
1212

docs/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This folder contains the maintained documentation set for SharpCoreDB (`v1.7.0`)
1313
## Core Areas
1414

1515
- `server/` - Server operation, security, APIs, and protocols.
16+
- `sql/` - SQL dialect capabilities, extensions, and compatibility guidance.
1617
- `scdb/` - Storage engine and SCDB guidance.
1718
- `serialization/` - Binary format and serialization internals.
1819
- `analytics/` - Analytics capabilities and usage.
@@ -22,6 +23,12 @@ This folder contains the maintained documentation set for SharpCoreDB (`v1.7.0`)
2223
- `sync/` - Dotmim.Sync provider usage.
2324
- `migration/` - Migration and interoperability guides.
2425

26+
## SQL extension docs (v1.7.0)
27+
28+
- `sql/SQL_DIALECT_EXTENSIONS_v1.7.0.md` - SharpCoreDB-specific SQL extensions including `GRAPH_RAG`, `OPTIONALLY`, and `IS SOME`/`IS NONE`.
29+
- `graphrag/GRAPH_RAG_SINGLE_SQL.md` - Single-statement GraphRAG SQL syntax and DI integration.
30+
- `functional/OPTIONALLY_SQL_OPTION_SUPPORT_v1.7.0.md` - Option<T> mapping semantics and usage patterns.
31+
2532
## Package Documentation
2633

2734
Per-package docs are maintained in `src/*/README.md` and `src/*/NuGet.README.md`, aligned to `v1.7.0`.

docs/compatibility/SQLITE_POSTGRESQL_AGGREGATE_SYNTAX_v1.7.0.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
| `BETWEEN … AND …` |||| |
9999
| `IN (…)` / `NOT IN (…)` |||| |
100100
| `LIKE` (% and _ wildcards) |||| **New in v1.7.0** — scalar function-form `LIKE(value, pattern)` supported in literal SELECT path |
101-
| `GLOB` ||| | Gap — SQLite-specific |
101+
| `GLOB` ||| | Implemented v1.7.0 |
102102
| `REGEXP` | ✅ extension ||| **New in v1.7.0** — AST WHERE evaluation supports `REGEXP` / `NOT REGEXP` |
103103
| `ORDER BY col [ASC\|DESC]` |||| |
104104
| `ORDER BY ordinal position` (e.g. `ORDER BY 2`) |||| |
@@ -118,13 +118,13 @@
118118
| `WITH RECURSIVE` CTE |||| Gap |
119119
| `CASE WHEN … THEN … ELSE … END` ||| ⚠️ | Evaluated in AST executor; not in legacy DML path |
120120
| `CAST(… AS type)` ||| ⚠️ | TypeConverter handles common casts |
121-
| `COALESCE(…)` ||| | Gap |
122-
| `IFNULL(a, b)` / `NULLIF(a, b)` ||| | Gap |
123-
| `IIF(cond, t, f)` ||| | Gap |
124-
| `TYPEOF(x)` ||| | Gap |
121+
| `COALESCE(…)` ||| | **New in v1.7.0** — AST scalar function dispatch |
122+
| `IFNULL(a, b)` / `NULLIF(a, b)` ||| | **New in v1.7.0** — AST scalar function dispatch |
123+
| `IIF(cond, t, f)` ||| | **New in v1.7.0** — AST scalar function dispatch |
124+
| `TYPEOF(x)` ||| | **New in v1.7.0** — SQLite type names (`integer`, `real`, `text`, `blob`, `null`) |
125125
| `EXISTS (subquery)` / `NOT EXISTS` ||| ⚠️ | Partial |
126-
| `UNION` / `UNION ALL` ||| | Gap |
127-
| `INTERSECT` / `EXCEPT` ||| | Gap |
126+
| `UNION` / `UNION ALL` ||| | Implemented v1.7.0 |
127+
| `INTERSECT` / `EXCEPT` ||| | Implemented v1.7.0 |
128128
| `EXPLAIN` / `EXPLAIN QUERY PLAN` ||| ⚠️ | Text output; not structured rows |
129129

130130
---
@@ -181,10 +181,10 @@
181181
| `TRIM(s)` / `LTRIM(s)` / `RTRIM(s)` |||| **New in v1.7.0** — AST scalar function dispatch |
182182
| `REPLACE(s, from, to)` |||| **New in v1.7.0** — AST scalar function dispatch |
183183
| `INSTR(s, sub)` |||| **New in v1.7.0** — SQLite-compatible 1-based index |
184-
| `LIKE(s, pattern)` ||| | Gap — function form |
184+
| `LIKE(s, pattern)` ||| | **New in v1.7.0** — scalar function-form supported |
185185
| `HEX(x)` / `UNHEX(x)` |||| `HEX(x)` and `UNHEX(x)` both supported |
186186
| `QUOTE(x)` |||| **New in v1.7.0** — returns SQL literal text with proper single-quote escaping |
187-
| `RANDOM()` ||| | Gap |
187+
| `RANDOM()` ||| | Implemented v1.7.0 |
188188
| `COALESCE(a, b, …)` |||| **New in v1.7.0** — AST scalar function dispatch |
189189
| `IFNULL(a, b)` |||| **New in v1.7.0** — SQLite alias of COALESCE |
190190
| `NULLIF(a, b)` |||| **New in v1.7.0** — AST scalar function dispatch |
@@ -266,11 +266,11 @@ These are the most impactful missing features for full SQLite compatibility, ord
266266
|---|---|---|
267267
| 🔴 P0 | `COALESCE` / `IFNULL` scalar functions | Missing scalar function |
268268
| 🔴 P0 | `INSERT OR REPLACE` / `INSERT OR IGNORE` | Missing DML variant |
269-
| 🟠 P1 | `UNION` / `UNION ALL` / `INTERSECT` / `EXCEPT` | Missing set operations |
269+
| ✅ Done | `UNION` / `UNION ALL` / `INTERSECT` / `EXCEPT` | Implemented v1.7.0 |
270270
| 🟠 P1 | `WITH RECURSIVE` (recursive CTE) | Missing CTE variant |
271-
| 🟠 P1 | `GLOB` operator | Missing WHERE operator |
271+
| ✅ Done | `GLOB` operator | Implemented v1.7.0 |
272272
| 🟠 P1 | String functions: `LIKE()` function-form, `UNHEX`, `QUOTE`, `CHAR`, `UNICODE` | Missing scalar functions |
273-
| 🟠 P1 | Numeric functions: scalar `MAX/MIN`, `POW/POWER`, `SQRT`, `MOD`, `RANDOM` | Missing scalar functions |
273+
| ✅ Done | Numeric functions: scalar `MAX/MIN`, `POW/POWER`, `SQRT`, `MOD`, `RANDOM` | Implemented v1.7.0 |
274274
| 🟡 P2 | `CHANGES()` / `TOTAL_CHANGES()` full semantics | Missing scalar function semantics |
275275
| 🟡 P2 | `CHECK` constraint enforcement at DML time | Constraint enforcement |
276276
| 🟡 P2 | `SAVEPOINT` / `RELEASE` / `ROLLBACK TO` | Missing transaction feature |

docs/compatibility/SQLITE_POSTGRESQL_COMPATIBILITY_TODO.md

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
> **Goal:** Full SQLite syntax parity + key PostgreSQL additions in SharpCoreDB.
44
> **Reference matrix:** `docs/compatibility/SQLITE_POSTGRESQL_AGGREGATE_SYNTAX_v1.7.0.md`
5-
> **Last updated:** v1.7.0
6-
> **Estimated total effort:** ~19 weeks solo / ~10 weeks with AI pair-programming.
5+
> **Last updated:** v1.7.0 — 2025-07-03 (W3-5 DEFAULT expression marked done; AstExecutor VisitSetOperation implemented)
76
87
---
98

@@ -81,17 +80,17 @@
8180
## 🟠 Wave 2 — P1 High Impact (~8 weeks)
8281

8382
### W2-1 · Set operations
84-
- [ ] Parse `UNION` between two SELECT arms
85-
- [ ] Parse `UNION ALL`
86-
- [ ] Parse `INTERSECT`
87-
- [ ] Parse `EXCEPT`
88-
- [ ] Add `SetOperationNode` to `SqlAst.Nodes.cs`
89-
- [ ] Execute UNION (deduplicate)
90-
- [ ] Execute UNION ALL (no dedup)
91-
- [ ] Execute INTERSECT
92-
- [ ] Execute EXCEPT
93-
- [ ] `ORDER BY` / `LIMIT` on outer set result
94-
- [ ] Tests for all four set operations
83+
- [x] Parse `UNION` between two SELECT arms
84+
- [x] Parse `UNION ALL`
85+
- [x] Parse `INTERSECT`
86+
- [x] Parse `EXCEPT`
87+
- [x] Add `SetOperationNode` to `SqlAst.Nodes.cs`
88+
- [x] Execute UNION (deduplicate)
89+
- [x] Execute UNION ALL (no dedup)
90+
- [x] Execute INTERSECT
91+
- [x] Execute EXCEPT
92+
- [x] `ORDER BY` / `LIMIT` on outer set result
93+
- [x] Tests for all four set operations
9594

9695
### W2-2 · String scalar functions (extended)
9796
- [x] `SUBSTR(s, start)` — 1-based index, SQLite convention
@@ -107,20 +106,20 @@
107106
- [x] Tests for implemented string functions
108107

109108
### W2-3 · Numeric scalar functions (extended)
110-
- [ ] `MAX(a, b)` — 2-arg scalar form (distinct from aggregate MAX)
111-
- [ ] `MIN(a, b)` — 2-arg scalar form
112-
- [ ] `POW(x, y)` / `POWER(x, y)`
113-
- [ ] `SQRT(x)`
114-
- [ ] `MOD(x, y)` / `%` operator
109+
- [x] `MAX(a, b)` — 2-arg scalar form (distinct from aggregate MAX)
110+
- [x] `MIN(a, b)` — 2-arg scalar form
111+
- [x] `POW(x, y)` / `POWER(x, y)`
112+
- [x] `SQRT(x)`
113+
- [x] `MOD(x, y)` / `%` operator
115114
- [x] `SIGN(x)`
116-
- [ ] `RANDOM()` — returns random integer (SQLite: 64-bit signed)
115+
- [x] `RANDOM()` — returns random integer (SQLite: 64-bit signed)
117116
- [x] Tests for implemented numeric functions
118117

119118
### W2-4 · GLOB operator
120-
- [ ] Parse `col GLOB pattern` in expression parser
121-
- [ ] Implement glob→regex conversion (`*``.*`, `?``.`, case-sensitive)
122-
- [ ] Support character classes `[A-Z]`
123-
- [ ] Tests for GLOB with various patterns
119+
- [x] Parse `col GLOB pattern` in expression parser
120+
- [x] Implement glob→regex conversion (`*``.*`, `?``.`, case-sensitive)
121+
- [x] Support character classes `[A-Z]`
122+
- [x] Tests for GLOB with various patterns
124123

125124
### W2-5 · `WITH RECURSIVE` CTE
126125
- [ ] Detect `RECURSIVE` keyword in `WITH` clause
@@ -171,9 +170,9 @@
171170
- [ ] `LAST_INSERT_ROWID()` — verify works in all execution paths
172171

173172
### W3-5 · DEFAULT expression evaluation
174-
- [ ] At INSERT time, evaluate `DEFAULT (expr)` for omitted columns
175-
- [ ] Support `DEFAULT (strftime('%Y-%m-%d', 'now'))`
176-
- [ ] Tests for DEFAULT expression columns
173+
- [x] At INSERT time, evaluate `DEFAULT (expr)` for omitted columns
174+
- [x] Support `DEFAULT (strftime('%Y-%m-%d', 'now'))`
175+
- [x] Tests for DEFAULT expression columns
177176

178177
### W3-6 · Correlated subquery (full support)
179178
- [ ] Pass outer row context into inner executor on each iteration
@@ -242,11 +241,11 @@
242241
| Wave | Total items | Done | Remaining |
243242
|---|---|---|---|
244243
| Quick Wins | 14 | 14 | 0 |
245-
| W1 — P0 | 18 | 2 | 16 |
246-
| W2 — P1 | 34 | 8 | 26 |
247-
| W3 — P2 | 28 | 2 | 26 |
248-
| W4 — P3 | 24 | 1 | 23 |
249-
| **Total** | **118** | **27** | **91** |
244+
| W1 — P0 | 31 | 31 | 0 |
245+
| W2 — P1 | 42 | 35 | 7 |
246+
| W3 — P2 | 38 | 5 | 33 |
247+
| W4 — P3 | 27 | 1 | 26 |
248+
| **Total** | **152** | **86** | **66** |
250249

251250
---
252251

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# OPTIONALLY SQL + Option<T> support (v1.7.0)
2+
3+
SharpCoreDB now supports native optional-value query shaping for functional applications.
4+
5+
## SQL syntax
6+
7+
```sql
8+
SELECT id, name, email OPTIONALLY
9+
FROM users
10+
WHERE email IS SOME;
11+
```
12+
13+
Supported predicates:
14+
15+
- `IS SOME` → matches non-null values
16+
- `IS NONE` → matches null/DBNull values
17+
18+
## Behavior
19+
20+
When `OPTIONALLY` is used in `SELECT`, the ADO.NET provider maps projected values to `Option<T>` from `SharpCoreDB.Functional`:
21+
22+
- Non-null value → `Option.Some<T>(value)`
23+
- Null value → `Option.None<T>()`
24+
25+
Type inference rules:
26+
27+
- Uses first non-null value in each column to infer inner `T`
28+
- If all values are null, defaults to `Option<object>`
29+
30+
## CQRS / Event Sourcing advantage
31+
32+
`OPTIONALLY` reduces defensive null-checks in handlers and projections.
33+
34+
### Without OPTIONALLY
35+
36+
```csharp
37+
var emailObj = reader["email"];
38+
if (emailObj is null || emailObj is DBNull)
39+
{
40+
// fallback path
41+
}
42+
else
43+
{
44+
var email = emailObj.ToString();
45+
// normal path
46+
}
47+
```
48+
49+
### With OPTIONALLY
50+
51+
```csharp
52+
var emailOpt = (Option<string>)reader["email"];
53+
var normalized = emailOpt.Match(
54+
Some: email => email.Trim().ToLowerInvariant(),
55+
None: () => "(missing)");
56+
```
57+
58+
This aligns naturally with functional pipelines used in CQRS command handlers, read models, and event upcasters.

0 commit comments

Comments
 (0)