oracle / oracle/dotnet-db-samples
Oracle.EntityFrameworkCore leaks ref cursors when a non-first statement in a SaveChanges batch fails → eventual ORA-01000
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 434
- Forks
- 191
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 1
Description
Environment
- Oracle.EntityFrameworkCore: reproduced on both
10.23.26200(EF Core 10 / .NET 10) and8.23.90(EF Core 8 / .NET 8) - ODP.NET: managed, default pooling and statement caching (no custom connection-string parameters)
- Oracle Database: "21c Express Edition Release 21.0.0.0.0 - Production, Version 21.3.0.0.0" and "Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production Version 19.27.0.0.0"
Summary
When SaveChanges batches multiple DML statements into a single anonymous PL/SQL block (entities with
ValueGeneratedOnAdd / identity columns, so generated values are returned via OPEN <refcursor> FOR SELECT :B1 FROM DUAL)
and a statement after the first one in the block raises (e.g. ORA-12899: value too large for column):
- the ref cursors already opened by the preceding statements of the block are never received by the
client and stay open on the session indefinitely; - they are not released by rollback,
DbContext.Dispose(), or
OracleConnection.PurgeStatementCache()— only by closing the physical session (ClearPool); - with pooling the session is reused indefinitely, so each failed batch adds cursors until the session
reachesopen_cursors→ORA-01000: maximum open cursors exceeded.
v$open_cursor for the affected SID shows the leaked cursors, all with sql_text = 'SELECT :B1 FROM DUAL',
growing by one more per failed SaveChanges.
Why it matters
Once the pooled session hits open_cursors, every subsequent SaveChanges on that session fails with
ORA-01000 instead of the original error — a single recurring data error (here ORA-12899) progressively
poisons a pooled session until it starts failing statements that would otherwise succeed. In long-running
services, unrelated requests begin failing on a "good" connection.
Key evidence — position dependence
- Failing statement first in the block → nothing leaks (no ref cursor opened yet).
- Failing statement later → exactly the cursors of the preceding statements leak.
MaxBatchSize(1)makes the leak structurally impossible (no predecessors in the block).
Reproduction
Minimal, self-contained repro using default pooling (no connection-string changes): each iteration uses a
fresh DbContext whose connection is returned to the pool after the failed SaveChanges; the pool
hands the same physical session back (constant SID), and the open-cursor count on it keeps climbing — so
returning the connection to the pool does not release the leak. Full details and expected output in the README:
https://github.com/mcauzzi/oracle-efcore-cursor-leak-repro
dotnet run -- "User Id=...;Password=...;Data Source=..." 1 200 # mode 1: LEAK
dotnet run -- "User Id=...;Password=...;Data Source=..." 2 200 # mode 2: control (MaxBatchSize=1)
Mode 1 leaks ~+1 cursor per iteration; mode 2 stays flat. Without a SELECT grant on v$open_cursor,
mode 1 still demonstrates the bug by eventually crashing the session with ORA-01000.
Expected behavior
The provider should close (or never abandon) the ref cursors already opened inside the PL/SQL block when a
later statement in the same block raises — e.g. wrap the block body in an exception handler that closes the
opened cursors before re-raising, or open the ref cursors only after all DML in the block has completed
successfully.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the linked reproduction README and run the mode 1 and mode 2 dotnet commands to confirm the cursor-count difference. Investigate the SaveChanges batching path for anonymous PL/SQL blocks and generated-value ref cursors. Done means a later DML failure does not leave prior cursors open, while the original error is still reported and the control behavior remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100