Skip to content

Commit 3396340

Browse files
authored
Backport 6.1 | SqlDecimal Extract Data (#3467)
* Replace non-compliant code with compliant code * Add unit tests for netfx SqlTypeWorkarounds
1 parent df35633 commit 3396340

File tree

2 files changed

+253
-137
lines changed

2 files changed

+253
-137
lines changed

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs

Lines changed: 46 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
using System;
66
using System.Data.SqlTypes;
77
using System.Reflection;
8-
using System.Reflection.Emit;
9-
using System.Runtime.Serialization;
108
using Microsoft.Data.SqlClient;
119

1210
namespace Microsoft.Data.SqlTypes
@@ -20,7 +18,10 @@ namespace Microsoft.Data.SqlTypes
2018
internal static partial class SqlTypeWorkarounds
2119
{
2220
#region Work around inability to access SqlMoney.ctor(long, int) and SqlMoney.ToSqlInternalRepresentation
23-
private static readonly Func<long, SqlMoney> s_sqlMoneyfactory = CtorHelper.CreateFactory<SqlMoney, long, int>(); // binds to SqlMoney..ctor(long, int) if it exists
21+
// Documentation for internal ctor:
22+
// https://learn.microsoft.com/en-us/dotnet/framework/additional-apis/system.data.sqltypes.sqlmoney.-ctor
23+
private static readonly Func<long, SqlMoney> s_sqlMoneyfactory =
24+
CtorHelper.CreateFactory<SqlMoney, long, int>(); // binds to SqlMoney..ctor(long, int) if it exists
2425

2526
/// <summary>
2627
/// Constructs a SqlMoney from a long value without scaling. The ignored parameter exists
@@ -70,6 +71,11 @@ internal static SqlMoneyToLongDelegate GetSqlMoneyToLong()
7071

7172
private static SqlMoneyToLongDelegate GetFastSqlMoneyToLong()
7273
{
74+
// Note: Although it would be faster to use the m_value member variable in
75+
// SqlMoney, but because it is not documented, we cannot use it. The method
76+
// we are calling below *is* documented, despite it being internal.
77+
// Documentation for internal method:
78+
// https://learn.microsoft.com/en-us/dotnet/framework/additional-apis/system.data.sqltypes.sqlmoney.tosqlinternalrepresentation
7379
MethodInfo toSqlInternalRepresentation = typeof(SqlMoney).GetMethod("ToSqlInternalRepresentation",
7480
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.ExactBinding,
7581
null, CallingConventions.Any, new Type[] { }, null);
@@ -113,145 +119,45 @@ private static long FallbackSqlMoneyToLong(ref SqlMoney value)
113119
}
114120
#endregion
115121

116-
#region Work around inability to access SqlDecimal._data1/2/3/4
117-
internal static void SqlDecimalExtractData(SqlDecimal d, out uint data1, out uint data2, out uint data3, out uint data4)
118-
{
119-
SqlDecimalHelper.s_decompose(d, out data1, out data2, out data3, out data4);
120-
}
122+
#region Work around SqlDecimal.WriteTdsValue not existing in netfx
121123

122-
private static class SqlDecimalHelper
124+
/// <summary>
125+
/// Implementation that mimics netcore's WriteTdsValue method.
126+
/// </summary>
127+
/// <remarks>
128+
/// Although calls to this method could just be replaced with calls to
129+
/// <see cref="SqlDecimal.Data"/>, using this mimic method allows netfx and netcore
130+
/// implementations to be more cleanly switched.
131+
/// </remarks>
132+
/// <param name="value">SqlDecimal value to get data from.</param>
133+
/// <param name="data1">First data field will be written here.</param>
134+
/// <param name="data2">Second data field will be written here.</param>
135+
/// <param name="data3">Third data field will be written here.</param>
136+
/// <param name="data4">Fourth data field will be written here.</param>
137+
internal static void SqlDecimalExtractData(
138+
SqlDecimal value,
139+
out uint data1,
140+
out uint data2,
141+
out uint data3,
142+
out uint data4)
123143
{
124-
internal delegate void Decomposer(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4);
125-
internal static readonly Decomposer s_decompose = GetDecomposer();
126-
127-
private static Decomposer GetDecomposer()
128-
{
129-
Decomposer decomposer = null;
130-
try
131-
{
132-
decomposer = GetFastDecomposer();
133-
}
134-
catch
135-
{
136-
// If an exception occurs for any reason, swallow & use the fallback code path.
137-
}
138-
139-
return decomposer ?? FallbackDecomposer;
140-
}
141-
142-
private static Decomposer GetFastDecomposer()
143-
{
144-
// This takes advantage of the fact that for [Serializable] types, the member fields are implicitly
145-
// part of the type's serialization contract. This includes the fields' names and types. By default,
146-
// [Serializable]-compliant serializers will read all the member fields and shove the data into a
147-
// SerializationInfo dictionary. We mimic this behavior in a manner consistent with the [Serializable]
148-
// pattern, but much more efficiently.
149-
//
150-
// In order to make sure we're staying compliant, we need to gate our checks to fulfill some core
151-
// assumptions. Importantly, the type must be [Serializable] but cannot be ISerializable, as the
152-
// presence of the interface means that the type wants to be responsible for its own serialization,
153-
// and that member fields are not guaranteed to be part of the serialization contract. Additionally,
154-
// we need to check for [OnSerializing] and [OnDeserializing] methods, because we cannot account
155-
// for any logic which might be present within them.
156-
157-
if (!typeof(SqlDecimal).IsSerializable)
158-
{
159-
SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal isn't Serializable. Less efficient fallback method will be used.");
160-
return null; // type is not serializable - cannot use fast path assumptions
161-
}
162-
163-
if (typeof(ISerializable).IsAssignableFrom(typeof(SqlDecimal)))
164-
{
165-
SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal is ISerializable. Less efficient fallback method will be used.");
166-
return null; // type contains custom logic - cannot use fast path assumptions
167-
}
168-
169-
foreach (MethodInfo method in typeof(SqlDecimal).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
170-
{
171-
if (method.IsDefined(typeof(OnDeserializingAttribute)) || method.IsDefined(typeof(OnDeserializedAttribute)))
172-
{
173-
SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | SqlDecimal contains custom serialization logic. Less efficient fallback method will be used.");
174-
return null; // type contains custom logic - cannot use fast path assumptions
175-
}
176-
}
177-
178-
// GetSerializableMembers filters out [NonSerialized] fields for us automatically.
179-
180-
FieldInfo fiData1 = null, fiData2 = null, fiData3 = null, fiData4 = null;
181-
foreach (MemberInfo candidate in FormatterServices.GetSerializableMembers(typeof(SqlDecimal)))
182-
{
183-
if (candidate is FieldInfo fi && fi.FieldType == typeof(uint))
184-
{
185-
if (fi.Name == "m_data1")
186-
{ fiData1 = fi; }
187-
else if (fi.Name == "m_data2")
188-
{ fiData2 = fi; }
189-
else if (fi.Name == "m_data3")
190-
{ fiData3 = fi; }
191-
else if (fi.Name == "m_data4")
192-
{ fiData4 = fi; }
193-
}
194-
}
195-
196-
if (fiData1 is null || fiData2 is null || fiData3 is null || fiData4 is null)
197-
{
198-
SqlClientEventSource.Log.TryTraceEvent("SqlTypeWorkarounds.SqlDecimalHelper.GetFastDecomposer | Info | Expected SqlDecimal fields are missing. Less efficient fallback method will be used.");
199-
return null; // missing one of the expected member fields - cannot use fast path assumptions
200-
}
201-
202-
Type refToUInt32 = typeof(uint).MakeByRefType();
203-
DynamicMethod dm = new(
204-
name: "sqldecimal-decomposer",
205-
returnType: typeof(void),
206-
parameterTypes: new[] { typeof(SqlDecimal), refToUInt32, refToUInt32, refToUInt32, refToUInt32 },
207-
restrictedSkipVisibility: true); // perf: JITs method at delegate creation time
208-
209-
ILGenerator ilGen = dm.GetILGenerator();
210-
ilGen.Emit(OpCodes.Ldarg_1); // eval stack := [UInt32&]
211-
ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal]
212-
ilGen.Emit(OpCodes.Ldfld, fiData1); // eval stack := [UInt32&] [UInt32]
213-
ilGen.Emit(OpCodes.Stind_I4); // eval stack := <empty>
214-
ilGen.Emit(OpCodes.Ldarg_2); // eval stack := [UInt32&]
215-
ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal]
216-
ilGen.Emit(OpCodes.Ldfld, fiData2); // eval stack := [UInt32&] [UInt32]
217-
ilGen.Emit(OpCodes.Stind_I4); // eval stack := <empty>
218-
ilGen.Emit(OpCodes.Ldarg_3); // eval stack := [UInt32&]
219-
ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal]
220-
ilGen.Emit(OpCodes.Ldfld, fiData3); // eval stack := [UInt32&] [UInt32]
221-
ilGen.Emit(OpCodes.Stind_I4); // eval stack := <empty>
222-
ilGen.Emit(OpCodes.Ldarg_S, (byte)4); // eval stack := [UInt32&]
223-
ilGen.Emit(OpCodes.Ldarg_0); // eval stack := [UInt32&] [SqlDecimal]
224-
ilGen.Emit(OpCodes.Ldfld, fiData4); // eval stack := [UInt32&] [UInt32]
225-
ilGen.Emit(OpCodes.Stind_I4); // eval stack := <empty>
226-
ilGen.Emit(OpCodes.Ret);
227-
228-
return (Decomposer)dm.CreateDelegate(typeof(Decomposer), null /* target */);
229-
}
230-
231-
// Used in case we can't use a [Serializable]-like mechanism.
232-
private static void FallbackDecomposer(SqlDecimal value, out uint data1, out uint data2, out uint data3, out uint data4)
233-
{
234-
if (value.IsNull)
235-
{
236-
data1 = default;
237-
data2 = default;
238-
data3 = default;
239-
data4 = default;
240-
}
241-
else
242-
{
243-
int[] data = value.Data; // allocation
244-
data4 = (uint)data[3]; // write in reverse to avoid multiple bounds checks
245-
data3 = (uint)data[2];
246-
data2 = (uint)data[1];
247-
data1 = (uint)data[0];
248-
}
249-
}
144+
// Note: Although it would be faster to use the m_data[1-4] member variables in
145+
// SqlDecimal, we cannot use them because they are not documented. The Data property
146+
// is less ideal, but is documented.
147+
int[] data = value.Data;
148+
data1 = (uint)data[0];
149+
data2 = (uint)data[1];
150+
data3 = (uint)data[2];
151+
data4 = (uint)data[3];
250152
}
153+
251154
#endregion
252155

253156
#region Work around inability to access SqlBinary.ctor(byte[], bool)
254-
private static readonly Func<byte[], SqlBinary> s_sqlBinaryfactory = CtorHelper.CreateFactory<SqlBinary, byte[], bool>(); // binds to SqlBinary..ctor(byte[], bool) if it exists
157+
// Documentation of internal constructor:
158+
// https://learn.microsoft.com/en-us/dotnet/framework/additional-apis/system.data.sqltypes.sqlbinary.-ctor
159+
private static readonly Func<byte[], SqlBinary> s_sqlBinaryfactory =
160+
CtorHelper.CreateFactory<SqlBinary, byte[], bool>();
255161

256162
internal static SqlBinary SqlBinaryCtor(byte[] value, bool ignored)
257163
{
@@ -270,7 +176,10 @@ internal static SqlBinary SqlBinaryCtor(byte[] value, bool ignored)
270176
#endregion
271177

272178
#region Work around inability to access SqlGuid.ctor(byte[], bool)
273-
private static readonly Func<byte[], SqlGuid> s_sqlGuidfactory = CtorHelper.CreateFactory<SqlGuid, byte[], bool>(); // binds to SqlGuid..ctor(byte[], bool) if it exists
179+
// Documentation for internal constructor:
180+
// https://learn.microsoft.com/en-us/dotnet/framework/additional-apis/system.data.sqltypes.sqlguid.-ctor
181+
private static readonly Func<byte[], SqlGuid> s_sqlGuidfactory =
182+
CtorHelper.CreateFactory<SqlGuid, byte[], bool>();
274183

275184
internal static SqlGuid SqlGuidCtor(byte[] value, bool ignored)
276185
{

0 commit comments

Comments
 (0)