# Oracle to PostgreSQL: Concurrent Transaction Handling ## Contents - Overview - The Core Difference - Common Error Symptoms - Problem Scenarios - Solutions — materialize results, separate connections, single query - Detection Strategy - Error Messages to Watch For - Comparison Table - Best Practices - Migration Checklist ## Overview When migrating from Oracle to PostgreSQL, a critical difference exists in how **concurrent operations on a single database connection** are handled. Oracle's ODP.NET driver allows multiple active commands and result sets on the same connection simultaneously, while PostgreSQL's Npgsql driver enforces a strict **one active command per connection** rule. Code that worked seamlessly in Oracle will throw runtime exceptions in PostgreSQL if concurrent operations share a connection. ## The Core Difference **Oracle Behavior:** - A single connection can have multiple active commands executing concurrently - Opening a second `DataReader` while another is still open is permitted - Nested or overlapping database calls on the same connection work transparently **PostgreSQL Behavior:** - A connection supports only **one active command at a time** - Attempting to execute a second command while a `DataReader` is open throws an exception - Lazy-loaded navigation properties or callback-driven reads that trigger additional queries on the same connection will fail ## Common Error Symptoms When migrating Oracle code without accounting for this difference: ``` System.InvalidOperationException: An operation is already in progress. ``` ``` Npgsql.NpgsqlOperationInProgressException: A command is already in progress: ``` These occur when application code attempts to execute a new command on a connection that already has an active `DataReader` or uncommitted command in flight. --- ## Problem Scenarios ### Scenario 1: Iterating a DataReader While Executing Another Command ```csharp using (var reader = command1.ExecuteReader()) { while (reader.Read()) { // PROBLEM: executing a second command on the same connection // while the reader is still open using (var command2 = new NpgsqlCommand("SELECT ...", connection)) { var value = command2.ExecuteScalar(); // FAILS } } } ``` ### Scenario 2: Lazy Loading / Deferred Execution in Data Access Layers ```csharp // Oracle: works because ODP.NET supports concurrent readers var items = repository.GetItems(); // returns IEnumerable backed by open DataReader foreach (var item in items) { // PROBLEM: triggers a second query on the same connection var details = repository.GetDetails(item.Id); // FAILS on PostgreSQL } ``` ### Scenario 3: Nested Stored Procedure Calls via Application Code ```csharp // Oracle: ODP.NET handles multiple active commands command1.ExecuteNonQuery(); // starts a long-running operation command2.ExecuteScalar(); // FAILS on PostgreSQL — command1 still in progress ``` --- ## Solutions ### Solution 1: Materialize Results Before Issuing New Commands (Recommended) Close the first result set by loading it into memory before executing subsequent commands on the same connection. ```csharp // Load all results into a list first var items = new List(); using (var reader = command1.ExecuteReader()) { while (reader.Read()) { items.Add(MapItem(reader)); } } // reader is closed and disposed here // Now safe to execute another command on the same connection foreach (var item in items) { using (var command2 = new NpgsqlCommand("SELECT ...", connection)) { command2.Parameters.AddWithValue("id", item.Id); var value = command2.ExecuteScalar(); // Works } } ``` For LINQ / EF Core scenarios, force materialization with `.ToList()`: ```csharp // Before (fails on PostgreSQL — deferred execution keeps connection busy) var items = dbContext.Items.Where(i => i.Active); foreach (var item in items) { var details = dbContext.Details.FirstOrDefault(d => d.ItemId == item.Id); } // After (materializes first query before issuing second) var items = dbContext.Items.Where(i => i.Active).ToList(); foreach (var item in items) { var details = dbContext.Details.FirstOrDefault(d => d.ItemId == item.Id); } ``` ### Solution 2: Use Separate Connections for Concurrent Operations When operations genuinely need to run concurrently, open a dedicated connection for each. ```csharp using (var reader = command1.ExecuteReader()) { while (reader.Read()) { // Use a separate connection for the nested query using (var connection2 = new NpgsqlConnection(connectionString)) { connection2.Open(); using (var command2 = new NpgsqlCommand("SELECT ...", connection2)) { var value = command2.ExecuteScalar(); // Works — different connection } } } } ``` ### Solution 3: Restructure to a Single Query Where possible, combine nested lookups into a single query using JOINs or subqueries to eliminate the need for concurrent commands entirely. ```csharp // Before: two sequential queries on the same connection var order = GetOrder(orderId); // query 1 var details = GetOrderDetails(orderId); // query 2 (fails if query 1 reader still open) // After: single query with JOIN using (var command = new NpgsqlCommand( "SELECT o.*, d.* FROM orders o JOIN order_details d ON o.id = d.order_id WHERE o.id = @id", connection)) { command.Parameters.AddWithValue("id", orderId); using (var reader = command.ExecuteReader()) { // Process combined result set } } ``` --- ## Detection Strategy ### Code Review Checklist - [ ] Search for methods that open a `DataReader` and call other database methods before closing it - [ ] Look for `IEnumerable` return types from data access methods that defer execution (indicate open readers) - [ ] Identify EF Core queries without `.ToList()` / `.ToArray()` that are iterated while issuing further queries - [ ] Check for nested stored procedure calls in application code that share a connection ### Common Locations to Search - Data access layers and repository classes - Service methods that orchestrate multiple repository calls - Code paths that iterate query results and perform lookups per row - Event handlers or callbacks triggered during data iteration ### Search Patterns ```regex ExecuteReader\(.*\)[\s\S]*?Execute(Scalar|NonQuery|Reader)\( ``` ```regex \.Where\(.*\)[\s\S]*?foreach[\s\S]*?dbContext\. ``` --- ## Error Messages to Watch For | Error Message | Likely Cause | |---------------|--------------| | `An operation is already in progress` | Second command executed while a `DataReader` is open on the same connection | | `A command is already in progress: ` | Npgsql detected overlapping command execution on a single connection | | `The connection is already in state 'Executing'` | Connection state conflict from concurrent usage | --- ## Comparison Table: Oracle vs. PostgreSQL | Aspect | Oracle (ODP.NET) | PostgreSQL (Npgsql) | |--------|------------------|---------------------| | **Concurrent commands** | Multiple active commands per connection | One active command per connection | | **Multiple open DataReaders** | Supported | Not supported — must close/materialize first | | **Nested DB calls during iteration** | Transparent | Throws `InvalidOperationException` | | **Deferred execution safety** | Safe to iterate and query | Must materialize (`.ToList()`) before issuing new queries | | **Connection pooling impact** | Lower connection demand | May need more pooled connections if using Solution 2 | --- ## Best Practices 1. **Materialize early** — Call `.ToList()` or `.ToArray()` on query results before iterating and issuing further database calls. This is the simplest and most reliable fix. 2. **Audit data access patterns** — Review all repository and data access methods for deferred-execution return types (`IEnumerable`, `IQueryable`) that callers iterate while issuing additional queries. 3. **Prefer single queries** — Where feasible, combine nested lookups into JOINs or subqueries to eliminate the concurrent-command pattern entirely. 4. **Isolate connections when necessary** — If concurrent operations are genuinely required, use separate connections rather than attempting to share one. 5. **Test iterative workflows** — Integration tests should cover scenarios where code iterates result sets and performs additional database operations per row, as these are the most common failure points. ## Migration Checklist - [ ] Identify all code paths that execute multiple commands on a single connection concurrently - [ ] Locate `IEnumerable`-backed data access methods that defer execution with open readers - [ ] Add `.ToList()` / `.ToArray()` materialization where deferred results are iterated alongside further queries - [ ] Refactor nested database calls to use separate connections or combined queries where appropriate - [ ] Verify EF Core navigation properties and lazy loading do not trigger concurrent connection usage - [ ] Update integration tests to cover iterative data access patterns - [ ] Load-test connection pool sizing if Solution 2 (separate connections) is used extensively ## References - [Npgsql Documentation: Basic Usage](https://www.npgsql.org/doc/basic-usage.html) - [PostgreSQL Documentation: Concurrency Control](https://www.postgresql.org/docs/current/mvcc.html) - [Npgsql GitHub: Multiple Active Result Sets Discussion](https://github.com/npgsql/npgsql/issues/462)