Skip to content

Commit 4729ae9

Browse files
committed
Made the SqlClient Simulated* mimic types public with strong typing where possible.
1 parent d7622c5 commit 4729ae9

19 files changed

Lines changed: 410 additions & 198 deletions

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Auto-loaded orientation. `README.md` is for humans.
44

55
## Identity
66

7-
`SqlServerSimulator`: in-process .NET 10 SQL Server simulation. It's an **ADO.NET stand-in for `Microsoft.Data.SqlClient`** — consumers create a `Simulation`, get a `SimulatedDbConnection` via `CreateDbConnection()`, and use it with (for example) `Microsoft.EntityFrameworkCore.SqlServer` instead of going through SqlClient over the wire. Public surface is intentionally minimal so internals stay free to refactor; `QualityTests.PublicApiWhitelist` is the authoritative list and fails the build on any unintended expansion — resist adding to it.
7+
`SqlServerSimulator`: in-process .NET 10 SQL Server simulation. It's an **ADO.NET stand-in for `Microsoft.Data.SqlClient`** — consumers create a `Simulation`, get a `SimulatedDbConnection` via `CreateDbConnection()`, and use it with (for example) `Microsoft.EntityFrameworkCore.SqlServer` instead of going through SqlClient over the wire. The full ADO.NET concrete-pipeline chain (`SimulatedDb{Connection,Command,Parameter,ParameterCollection,DataReader,Transaction}` + `SimulatedSqlException` + the info-message family) is public with `new`-shadowed strongly-typed returns, mirroring `Microsoft.Data.SqlClient`'s shape so consumers can downcast and reach concrete properties identically. Public surface beyond that chain is intentionally minimal so internals stay free to refactor; `QualityTests.PublicApiWhitelist` is the authoritative list and fails the build on any unintended expansion — resist adding to it.
88

99
`SqlServerSimulator.EFCore` is a sibling package whose only public method is `UseSqlServerSimulator(DbContextOptionsBuilder, DbConnection)`. EF Core's SqlServer provider keeps emitting SQL-Server-flavored SQL; the adapter just registers an `IRelationalTypeMappingSourcePlugin` for the (CLR, store) pairs whose default mappings downcast to `SqlParameter` (since the simulator's connection isn't a `SqlConnection`).
1010

SqlServerSimulator.Tests/Extensions.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,14 @@ public static IEnumerable<DbDataReader> EnumerateRecords(this DbDataReader reade
113113

114114
/// <summary>
115115
/// Verifies that <paramref name="commandText"/> against this simulation raises a
116-
/// <see cref="DbException"/> whose SQL Server error number matches
117-
/// <paramref name="errorNumber"/>. Returns the exception for further assertions.
116+
/// <see cref="SimulatedSqlException"/> whose
117+
/// <see cref="SimulatedSqlException.Number"/> matches <paramref name="errorNumber"/>.
118+
/// Returns the exception for further assertions.
118119
/// </summary>
119-
public static DbException AssertSqlError(this Simulation simulation, string commandText, int errorNumber)
120+
public static SimulatedSqlException AssertSqlError(this Simulation simulation, string commandText, int errorNumber)
120121
{
121-
var ex = Assert.Throws<DbException>(() => simulation.ExecuteScalar(commandText));
122-
Assert.AreEqual(errorNumber.ToString(), ex.Data["HelpLink.EvtID"]);
122+
var ex = Assert.Throws<SimulatedSqlException>(() => simulation.ExecuteScalar(commandText));
123+
Assert.AreEqual(errorNumber, ex.Number);
123124
return ex;
124125
}
125126

SqlServerSimulator.Tests/ParameterTests.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,21 +79,23 @@ public void DbType_UnsupportedValueType_ThrowsArgumentException()
7979
}
8080

8181
[TestMethod]
82-
public void SourceColumn_ThrowsNotImplemented()
82+
public void SourceColumn_DefaultsToEmptyAndRoundTrips()
8383
{
8484
using var cmd = new Simulation().CreateOpenConnection().CreateCommand();
8585
var p = cmd.CreateParameter();
86-
_ = Throws<NotImplementedException>(() => _ = p.SourceColumn);
87-
_ = Throws<NotImplementedException>(() => p.SourceColumn = "x");
86+
AreEqual("", p.SourceColumn);
87+
p.SourceColumn = "x";
88+
AreEqual("x", p.SourceColumn);
8889
}
8990

9091
[TestMethod]
91-
public void SourceColumnNullMapping_ThrowsNotImplemented()
92+
public void SourceColumnNullMapping_DefaultsToFalseAndRoundTrips()
9293
{
9394
using var cmd = new Simulation().CreateOpenConnection().CreateCommand();
9495
var p = cmd.CreateParameter();
95-
_ = Throws<NotImplementedException>(() => _ = p.SourceColumnNullMapping);
96-
_ = Throws<NotImplementedException>(() => p.SourceColumnNullMapping = true);
96+
IsFalse(p.SourceColumnNullMapping);
97+
p.SourceColumnNullMapping = true;
98+
IsTrue(p.SourceColumnNullMapping);
9799
}
98100

99101
[TestMethod]

SqlServerSimulator.Tests/QualityTests.cs

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,74 @@ public void PublicApiWhitelist()
3939
nameof(SimulatedDbConnection.ChangeDatabase),
4040
nameof(SimulatedDbConnection.Close),
4141
nameof(SimulatedDbConnection.Open),
42+
nameof(SimulatedDbConnection.CreateCommand),
43+
nameof(SimulatedDbConnection.BeginTransaction),
44+
],
45+
[typeof(SimulatedDbCommand)] = [
46+
nameof(SimulatedDbCommand.CommandText),
47+
nameof(SimulatedDbCommand.CommandTimeout),
48+
nameof(SimulatedDbCommand.CommandType),
49+
nameof(SimulatedDbCommand.DesignTimeVisible),
50+
nameof(SimulatedDbCommand.UpdatedRowSource),
51+
nameof(SimulatedDbCommand.Cancel),
52+
nameof(SimulatedDbCommand.ExecuteNonQuery),
53+
nameof(SimulatedDbCommand.ExecuteScalar),
54+
nameof(SimulatedDbCommand.Prepare),
55+
nameof(SimulatedDbCommand.CreateParameter),
56+
nameof(SimulatedDbCommand.Parameters),
57+
nameof(SimulatedDbCommand.Connection),
58+
nameof(SimulatedDbCommand.Transaction),
59+
nameof(SimulatedDbCommand.ExecuteReader),
60+
],
61+
[typeof(SimulatedDbTransaction)] = [
62+
nameof(SimulatedDbTransaction.IsolationLevel),
63+
nameof(SimulatedDbTransaction.Commit),
64+
nameof(SimulatedDbTransaction.Rollback),
65+
nameof(SimulatedDbTransaction.Connection),
66+
],
67+
[typeof(SimulatedDbDataReader)] = [
68+
"Item",
69+
nameof(SimulatedDbDataReader.Depth),
70+
nameof(SimulatedDbDataReader.FieldCount),
71+
nameof(SimulatedDbDataReader.HasRows),
72+
nameof(SimulatedDbDataReader.IsClosed),
73+
nameof(SimulatedDbDataReader.RecordsAffected),
74+
nameof(SimulatedDbDataReader.GetBoolean),
75+
nameof(SimulatedDbDataReader.GetByte),
76+
nameof(SimulatedDbDataReader.GetBytes),
77+
nameof(SimulatedDbDataReader.GetChar),
78+
nameof(SimulatedDbDataReader.GetChars),
79+
nameof(SimulatedDbDataReader.GetDataTypeName),
80+
nameof(SimulatedDbDataReader.GetDateTime),
81+
nameof(SimulatedDbDataReader.GetDecimal),
82+
nameof(SimulatedDbDataReader.GetDouble),
83+
nameof(SimulatedDbDataReader.GetEnumerator),
84+
nameof(SimulatedDbDataReader.GetFieldType),
85+
nameof(SimulatedDbDataReader.GetFloat),
86+
nameof(SimulatedDbDataReader.GetGuid),
87+
nameof(SimulatedDbDataReader.GetInt16),
88+
nameof(SimulatedDbDataReader.GetInt32),
89+
nameof(SimulatedDbDataReader.GetInt64),
90+
nameof(SimulatedDbDataReader.GetName),
91+
nameof(SimulatedDbDataReader.GetOrdinal),
92+
nameof(SimulatedDbDataReader.GetString),
93+
nameof(SimulatedDbDataReader.GetValue),
94+
nameof(SimulatedDbDataReader.GetFieldValue),
95+
nameof(SimulatedDbDataReader.GetValues),
96+
nameof(SimulatedDbDataReader.IsDBNull),
97+
nameof(SimulatedDbDataReader.NextResult),
98+
nameof(SimulatedDbDataReader.Read),
99+
],
100+
[typeof(SimulatedSqlException)] = [
101+
nameof(SimulatedSqlException.ErrorCode),
102+
nameof(SimulatedSqlException.IsTransient),
103+
nameof(SimulatedSqlException.Number),
104+
nameof(SimulatedSqlException.Class),
105+
nameof(SimulatedSqlException.State),
106+
nameof(SimulatedSqlException.Errors),
107+
nameof(SimulatedSqlException.LineNumber),
108+
nameof(SimulatedSqlException.Procedure),
109+
nameof(SimulatedSqlException.Server),
42110
],
43111
[typeof(SimulatedError)] = [
44112
nameof(SimulatedError.Class),
@@ -63,14 +131,34 @@ public void PublicApiWhitelist()
63131
nameof(SimulatedInfoMessageEventArgs.Message),
64132
nameof(SimulatedInfoMessageEventArgs.Source),
65133
],
66-
// C# 14 lowers the extension(DbParameter) block to static
67-
// accessor methods on the host class plus a compiler-generated
68-
// <G>$... nested type carrying the cross-assembly metadata. The
69-
// nested type is filtered as compiler-generated; the accessors
70-
// surface as regular (non-special-name) methods here.
71-
[typeof(TableValuedParameterExtensions)] = [
72-
"get_TypeName",
73-
"set_TypeName",
134+
[typeof(SimulatedDbParameter)] = [
135+
".ctor",
136+
nameof(SimulatedDbParameter.DbType),
137+
nameof(SimulatedDbParameter.Direction),
138+
nameof(SimulatedDbParameter.IsNullable),
139+
nameof(SimulatedDbParameter.ParameterName),
140+
nameof(SimulatedDbParameter.Size),
141+
nameof(SimulatedDbParameter.SourceColumn),
142+
nameof(SimulatedDbParameter.SourceColumnNullMapping),
143+
nameof(SimulatedDbParameter.Value),
144+
nameof(SimulatedDbParameter.TypeName),
145+
nameof(SimulatedDbParameter.ResetDbType),
146+
],
147+
[typeof(SimulatedDbParameterCollection)] = [
148+
".ctor",
149+
"Item",
150+
nameof(SimulatedDbParameterCollection.Count),
151+
nameof(SimulatedDbParameterCollection.SyncRoot),
152+
nameof(SimulatedDbParameterCollection.Add),
153+
nameof(SimulatedDbParameterCollection.AddRange),
154+
nameof(SimulatedDbParameterCollection.Clear),
155+
nameof(SimulatedDbParameterCollection.Contains),
156+
nameof(SimulatedDbParameterCollection.CopyTo),
157+
nameof(SimulatedDbParameterCollection.GetEnumerator),
158+
nameof(SimulatedDbParameterCollection.IndexOf),
159+
nameof(SimulatedDbParameterCollection.Insert),
160+
nameof(SimulatedDbParameterCollection.Remove),
161+
nameof(SimulatedDbParameterCollection.RemoveAt),
74162
],
75163
[typeof(BacpacImportOptions)] = [
76164
".ctor",

SqlServerSimulator.Tests/TableValuedParameterTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -461,10 +461,10 @@ public void Structured_IDataReader_BindsViaCommand()
461461
AreEqual(80, rdr.GetInt32(1));
462462
}
463463

464-
// ---- TypeName extension property ----
464+
// ---- TypeName property ----
465465

466466
[TestMethod]
467-
public void TypeNameExtension_DefaultsToEmptyString()
467+
public void TypeName_DefaultsToEmptyString()
468468
{
469469
var simulation = new Simulation();
470470
using var con = simulation.CreateDbConnection();
@@ -474,7 +474,7 @@ public void TypeNameExtension_DefaultsToEmptyString()
474474
}
475475

476476
[TestMethod]
477-
public void TypeNameExtension_SetThenGet_RoundTrips()
477+
public void TypeName_SetThenGet_RoundTrips()
478478
{
479479
var simulation = new Simulation();
480480
using var con = simulation.CreateDbConnection();
@@ -485,7 +485,7 @@ public void TypeNameExtension_SetThenGet_RoundTrips()
485485
}
486486

487487
[TestMethod]
488-
public void TypeNameExtension_SetEmpty_Clears()
488+
public void TypeName_SetEmpty_Clears()
489489
{
490490
var simulation = new Simulation();
491491
using var con = simulation.CreateDbConnection();

SqlServerSimulator/Errors/SimulatedSqlException.cs

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@
55
namespace SqlServerSimulator;
66

77
/// <summary>
8-
/// Describes a simulated SQL exception. The Msg-specific factory methods
9-
/// live in topical partial files in the same directory:
8+
/// Describes a simulated SQL exception. Mirrors enough of
9+
/// <c>Microsoft.Data.SqlClient.SqlException</c> that consumers who catch
10+
/// <see cref="DbException"/> can downcast and read
11+
/// <see cref="Number"/> / <see cref="Class"/> / <see cref="State"/> /
12+
/// <see cref="Errors"/> the same way they would against a real
13+
/// <c>SqlException</c>.
14+
/// </summary>
15+
/// <remarks>
16+
/// The Msg-specific factory methods live in topical partial files in the same
17+
/// directory:
1018
/// <list type="bullet">
1119
/// <item><c>SimulatedSqlException.TypeErrors.cs</c> — type lookup, size,
1220
/// CAST / CONVERT, conversion, arithmetic.</item>
@@ -21,42 +29,45 @@ namespace SqlServerSimulator;
2129
/// <item><c>SimulatedSqlException.SyntaxErrors.cs</c> — generic parse-time
2230
/// errors.</item>
2331
/// </list>
24-
/// </summary>
25-
[SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "Only thrown internally; the public-API surface doesn't need standard exception constructors.")]
26-
internal sealed partial class SimulatedSqlException : DbException
32+
/// </remarks>
33+
[SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "Constructors are private — instances are only built via the topical factory methods on this partial class.")]
34+
public sealed partial class SimulatedSqlException : DbException
2735
{
36+
private const string SourceName = "Core Microsoft SqlClient Data Provider";
37+
2838
private SimulatedSqlException(string message, int number, byte @class, byte state)
29-
: this(message, new SimulatedSqlError(message, number, @class, state))
39+
: this(message, new SimulatedError(@class, lineNumber: 0, message, number, procedure: "", server: "", source: SourceName, state))
3040
{
3141
}
3242

33-
private SimulatedSqlException(string message, params ReadOnlySpan<SimulatedSqlError> errors)
43+
private SimulatedSqlException(string message, params ReadOnlySpan<SimulatedError> errors)
3444
: base(message)
3545
{
3646
base.HResult = unchecked((int)0x80131904);
37-
base.Source = "Core Microsoft SqlClient Data Provider";
47+
base.Source = SourceName;
3848

49+
SimulatedError first;
3950
if (errors.Length == 0)
4051
{
41-
this.Errors = [new SimulatedSqlError(base.Message, 0, 0, 0)];
42-
43-
return;
52+
first = new SimulatedError(@class: 0, lineNumber: 0, base.Message, number: 0, procedure: "", server: "", source: SourceName, state: 0);
53+
this.Errors = new SimulatedErrorCollection([first]);
54+
}
55+
else
56+
{
57+
first = errors[0];
58+
this.Errors = new SimulatedErrorCollection([.. errors]);
4459
}
4560

46-
this.Errors = [.. errors];
47-
48-
var firstError = errors[0];
49-
50-
this.Number = firstError.Number;
51-
this.Class = firstError.Class;
52-
this.State = firstError.State;
61+
this.Number = first.Number;
62+
this.Class = first.Class;
63+
this.State = first.State;
5364

5465
var data = this.Data;
5566

5667
data.Add("HelpLink.ProdName", "Microsoft SQL Server");
5768
data.Add("HelpLink.ProdVer", "99.00.1000");
5869
data.Add("HelpLink.EvtSrc", "MSSQLServer");
59-
data.Add("HelpLink.EvtID", firstError.Number.ToString(CultureInfo.InvariantCulture));
70+
data.Add("HelpLink.EvtID", first.Number.ToString(CultureInfo.InvariantCulture));
6071
data.Add("HelpLink.BaseHelpUrl", "https://go.microsoft.com/fwlink");
6172
data.Add("HelpLink.LinkId", "20476");
6273
}
@@ -69,25 +80,38 @@ private SimulatedSqlException(string message, params ReadOnlySpan<SimulatedSqlEr
6980

7081
/// <summary>
7182
/// An error number as described by https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors .
83+
/// Mirrors <c>SqlException.Number</c>.
7284
/// </summary>
73-
public readonly int Number;
85+
public int Number { get; }
7486

7587
/// <summary>
7688
/// A value from 1 to 25 that indicates the severity level of the error. The default is 0.
89+
/// Mirrors <c>SqlException.Class</c>.
7790
/// </summary>
7891
/// <remarks>
7992
/// The severity indicates how serious the error is.
8093
/// Errors that have a low severity, such as 1 or 2, are information messages or low-level warnings.
8194
/// Errors that have a high severity indicate problems that should be addressed as soon as possible.
8295
/// </remarks>
83-
public readonly byte Class;
96+
public byte Class { get; }
8497

8598
/// <summary>
8699
/// Some error messages can be raised at multiple points in the code for the Database Engine.
87100
/// For example, an 1105 error can be raised for several different conditions.
88101
/// Each specific condition that raises an error assigns a unique state code.
102+
/// Mirrors <c>SqlException.State</c>.
89103
/// </summary>
90-
public readonly byte State;
104+
public byte State { get; }
105+
106+
/// <summary>Collection of one or more <see cref="SimulatedError"/> entries. Mirrors <c>SqlException.Errors</c>.</summary>
107+
public SimulatedErrorCollection Errors { get; }
108+
109+
/// <summary>1-based line number of the first error. Shortcut for <c>Errors[0].LineNumber</c>; mirrors <c>SqlException.LineNumber</c>.</summary>
110+
public int LineNumber => this.Errors[0].LineNumber;
111+
112+
/// <summary>Name of the procedure or trigger generating the first error, or empty string. Shortcut for <c>Errors[0].Procedure</c>; mirrors <c>SqlException.Procedure</c>.</summary>
113+
public string Procedure => this.Errors[0].Procedure;
91114

92-
public readonly SimulatedSqlError[] Errors;
115+
/// <summary>Server name carried by the first error. Shortcut for <c>Errors[0].Server</c>; mirrors <c>SqlException.Server</c>.</summary>
116+
public string Server => this.Errors[0].Server;
93117
}

0 commit comments

Comments
 (0)