ADO.NET — Connected & Disconnected Data Access
In short: ADO.NET is .NET's core data access library (System.Data + provider packages).
- Main objects:
Connection(connects to the DB),Command(executes SQL/procs),Parameter,DataReader(forward-only fast read),DataAdapter(fills/updates DataSets),DataSet/DataTable(in-memory data),Transaction. - For SQL Server use
Microsoft.Data.SqlClient(SqlConnection,SqlCommand...).
In short: Connected: the connection stays open while you read with a DataReader — forward-only, read-only, very fast, minimal memory.
- Disconnected: a
DataAdapterfills aDataSet/DataTable, then closes the connection; - you can navigate, edit and later push changes back with
Update()— more memory, more flexibility. - Modern apps mostly use the connected model (directly or via Dapper/EF).
In short: ExecuteReader returns a DataReader for one or more result sets.
ExecuteScalarreturns the first column of the first row (COUNT, SUM, new identity viaOUTPUT/SCOPE_IDENTITY()).ExecuteNonQueryruns INSERT/UPDATE/DELETE/DDL and returns the number of rows affected (-1 for statements that don't affect rows, or whenSET NOCOUNT ONis set).
In short: DataReader: connected, forward-only, read-only, one row at a time, fastest and lowest memory — use for streaming results to APIs.
- DataSet: disconnected, in-memory collection of DataTables with relations and constraints, random access and editable, can be serialised to XML — heavier;
- used in legacy WinForms/reporting.
- EF/Dapper map to objects instead.
In short: Always use parameters (@name placeholders + SqlParameter with explicit type/size) or stored procedures with parameters — never concatenate user input into SQL.
- Validate input, use least-privilege DB accounts, and for dynamic identifiers (table/column names) use an allow-list.
- ORMs (EF, Dapper) parameterise automatically.
using statement's role with SqlConnection?In short: SqlConnection, SqlCommand and SqlDataReader implement IDisposable.
usingguaranteesDispose()(which closes the connection and returns it to the pool) even if an exception occurs.- Forgetting to dispose leaks pooled connections and eventually causes pool-timeout errors.
In short: ADO.NET reuses open physical connections from a pool keyed by the exact connection string.
Open()borrows,Close()/Dispose()returns.- Default max is 100 per pool.
- Exhaustion symptoms: "Timeout expired… obtaining a connection from the pool".
- Causes: missing
using/dispose, connections held across long work orawaits of slow external calls, long transactions, or traffic beyond 100 concurrent DB operations. - Fix: dispose properly, shorten connection lifetimes, async I/O, raise
Max Pool Sizeonly after fixing leaks, monitor with performance counters/sys.dm_exec_sessions.
In short: var tx = conn.BeginTransaction(isolationLevel), assign tx to every command (new SqlCommand(sql, conn, tx)), execute, then tx.Commit();
- on exception
tx.Rollback(). - Disposing an uncommitted transaction rolls it back.
- For multiple connections/resources use
TransactionScope(async flow enabled), avoiding distributed escalation in the cloud. - Keep transactions short to reduce locking.
AddWithValue considered bad practice?In short: It infers the SQL type from the .NET value: strings become nvarchar with a length equal to the value, decimals get inferred precision.
- Mismatches with column types (e.g.
varcharcolumn vsnvarcharparameter) force implicit conversions that can turn index seeks into scans, and varying lengths create many cached plans. - Use
Parameters.Add("@p", SqlDbType.VarChar, 50).Value = xwith explicit type and size.
Set CommandType = CommandType.StoredProcedure, add input parameters, add an output parameter with Direction = ParameterDirection.Output (and size for strings), optionally a ReturnValue parameter, execute (ExecuteNonQuery), then read param.Value after execution (after closing the reader if you used ExecuteReader).
In short: A class that streams large volumes of rows into a SQL Server table using the bulk insert protocol — from a DataTable, DataReader or DataRow[].
- It's orders of magnitude faster than individual INSERTs.
- Configure
BatchSize,BulkCopyTimeout, column mappings andTableLock. - Typical use: ETL staging loads, imports from CSV/Excel.
- Load into a staging table, then MERGE into the final table.
In short: Multiple Active Result Sets (MultipleActiveResultSets=True in the connection string) lets you have more than one open DataReader (or execute commands while reading) on the same connection.
- Useful in some EF lazy-loading scenarios, but it adds overhead; usually better to restructure queries.
In short: Dapper is a micro-ORM: extension methods (Query<T>, Execute, QueryMultiple) on IDbConnection that map rows to objects using cached IL.
- Compared with raw ADO.NET it removes mapping boilerplate with almost the same speed;
- compared with EF Core it has no change tracking, LINQ, migrations or relationship management — you write all SQL.
- Use Dapper for read-heavy, performance-critical or complex SQL/stored procedures; EF Core for domain-rich write models.
- They combine well.
In short: Use retry logic for transient error numbers (e.g. 40613, 40501, 40197, 10928/10929, 4060, 49918–49920, timeouts): SqlClient's configurable retry logic (SqlRetryLogicBaseProvider via RetryLogicProvider on the connection/command), Polly with exponential backoff, or EF Core's EnableRetryOnFailure.
- Only retry idempotent operations or whole transactions, and keep
Connect Retry Countfor idle connection resiliency.
In short: Options: a table-valued parameter (TVP;
- best for SQL Server;
- plan-stable),
STRING_SPLITon a delimited string orOPENJSONon a JSON array parameter (EF Core 8 does this forContains), or bulk-insert into a temp/staging table and join. - Avoid building
IN (1,2,3,...)strings — injection risk, plan-cache pollution and the 2,100-parameter limit for individual parameters.
In short: Pool exhaustion.
- 1) Search for connections/readers not in
usingblocks and for code holding connections across slow calls (HTTP, file I/O) or long transactions. - 2) Check sync-over-async blocking that keeps connections open longer.
- 3) Check slow queries (Query Store) — slow queries hold connections.
- 4) Monitor active connections (
sys.dm_exec_sessionsby program name). - 5) Only then tune
Max Pool Size, and consider caching or fewer round trips (batching). - Also ensure one consistent connection string.
In short: Stream the file (don't load it all) with a CSV reader (CsvHelper) exposing an IDataReader or batches of DataTables;
SqlBulkCopyinto a staging table withTableLockand a batch size (10k–50k);- then a set-based stored procedure validates and
MERGEs/INSERTs into target tables inside a transaction; - log rejected rows;
- make it idempotent (truncate staging first, track file/batch IDs).
- Alternatives: SSIS or Azure Data Factory for ops-managed pipelines.
In short: Common causes: (1) different SET options — SSMS uses ARITHABORT ON, apps usually OFF, so they get different cached plans;
- (2) parameter sniffing — the app's cached plan was compiled for an atypical parameter value;
- (3) parameter type mismatch (nvarchar parameter vs varchar column → implicit conversion, scan);
- (4) app reading huge result sets slowly or network latency.
- Investigate with Query Store / actual execution plans, fix types, consider
OPTION (RECOMPILE)orOPTIMIZE FOR, update statistics, add proper indexes.