Skip to content
This repository was archived by the owner on Dec 24, 2022. It is now read-only.

Commit 551a763

Browse files
committed
Merge pull request #401 from BruceCowan-AI/FixThreadSafetyAndLoadReferencesTest
Fix Oracle provider thread safety issue, fix LoadReferencesJoinTest on O...
2 parents ce79733 + cfb28dd commit 551a763

File tree

4 files changed

+134
-109
lines changed

4 files changed

+134
-109
lines changed

src/ServiceStack.OrmLite.Oracle/OracleOrmLiteDialectProvider.cs

Lines changed: 15 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22
using System.Collections.Generic;
33
using System.Data;
44
using System.Data.Common;
5-
using System.Globalization;
65
using System.Linq;
76
using System.Linq.Expressions;
87
using System.Reflection;
98
using System.Text;
109
using System.Text.RegularExpressions;
11-
using System.Threading;
1210

1311
namespace ServiceStack.OrmLite.Oracle
1412
{
@@ -127,6 +125,9 @@ public override string ToPostCreateTableStatement(ModelDefinition modelDef)
127125
return null;
128126
}
129127

128+
private OracleTimestampConverter _timestampConverter;
129+
private readonly object _timestampLock = new object();
130+
130131
public override IDbConnection CreateConnection(string connectionString, Dictionary<string, string> options)
131132
{
132133
if (options != null)
@@ -135,7 +136,11 @@ public override IDbConnection CreateConnection(string connectionString, Dictiona
135136
}
136137

137138
var factory = DbProviderFactories.GetFactory(ClientProvider);
138-
InitializeOracleTimestampSetting(factory);
139+
lock (_timestampLock)
140+
{
141+
if (_timestampConverter == null)
142+
_timestampConverter = new OracleTimestampConverter(factory);
143+
}
139144
IDbConnection connection = factory.CreateConnection();
140145
if (connection != null) connection.ConnectionString = connectionString;
141146
return connection;
@@ -159,8 +164,8 @@ public override void SetDbValue(FieldDefinition fieldDef, IDataReader reader, in
159164
object convertedValue;
160165
if (fieldDef.FieldType == typeof(DateTimeOffset))
161166
{
162-
SetOracleTimestampTzFormat();
163-
convertedValue = ConvertTimestampTzToDateTimeOffset(reader, colIndex);
167+
_timestampConverter.SetOracleTimestampTzFormat();
168+
convertedValue = _timestampConverter.ConvertTimestampTzToDateTimeOffset(reader, colIndex);
164169
}
165170
else
166171
{
@@ -392,7 +397,7 @@ public override void SetParameterValue<T>(FieldDefinition fieldDef, IDataParamet
392397

393398
if (fieldDef.ColumnType == typeof(DateTimeOffset) || fieldDef.ColumnType == typeof(DateTimeOffset?))
394399
{
395-
SetOracleParameterTypeTimestampTz(p);
400+
_timestampConverter.SetOracleParameterTypeTimestampTz(p);
396401
}
397402
p.Value = value;
398403
}
@@ -411,101 +416,14 @@ protected override object GetValue<T>(FieldDefinition fieldDef, object obj)
411416
}
412417
if (fieldDef.FieldType == typeof(DateTimeOffset))
413418
{
414-
SetOracleTimestampTzFormat();
419+
_timestampConverter.SetOracleTimestampTzFormat();
415420
var timestamp = (DateTimeOffset)value;
416-
return timestamp.ToString(DateTimeOffsetOutputFormat, CultureInfo.InvariantCulture);
421+
return _timestampConverter.ConvertDateTimeOffsetToString(timestamp);
417422
}
418423
}
419424
return value;
420425
}
421426

422-
private string DateTimeOffsetOutputFormat { get; set; }
423-
private string DateTimeOffsetInputFormat { get; set; }
424-
private string TimestampTzFormat { get; set; }
425-
private readonly Dictionary<int, bool> _threadFormatSet = new Dictionary<int, bool>();
426-
private const BindingFlags InvokeStaticPublic = BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod;
427-
private Assembly OracleAssembly { get; set; }
428-
private object ClientInfo { get; set; }
429-
private MethodInfo SetThreadInfo { get; set; }
430-
private MethodInfo SetOracleDbType { get; set; }
431-
private object TimestampTz { get; set; }
432-
private MethodInfo GetOracleValue { get; set; }
433-
434-
private void InitializeOracleTimestampSetting(DbProviderFactory factory)
435-
{
436-
OracleAssembly = factory.GetType().Assembly;
437-
var globalizationType = OracleAssembly.GetType("Oracle.DataAccess.Client.OracleGlobalization");
438-
if (globalizationType != null)
439-
{
440-
DateTimeOffsetInputFormat = DateTimeOffsetOutputFormat = "yyyy-MM-dd HH:mm:ss.ffffff zzz";
441-
TimestampTzFormat = "YYYY-MM-DD HH24:MI:SS.FF6 TZH:TZM";
442-
443-
ClientInfo = globalizationType.InvokeMember("GetClientInfo", InvokeStaticPublic, null, null, null);
444-
const BindingFlags setProperty = BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.Instance;
445-
globalizationType.InvokeMember("TimeStampTZFormat", setProperty, null, ClientInfo, new object[] { TimestampTzFormat });
446-
SetThreadInfo = globalizationType.GetMethod("SetThreadInfo", BindingFlags.Public | BindingFlags.Static);
447-
448-
var parameterType = OracleAssembly.GetType("Oracle.DataAccess.Client.OracleParameter");
449-
var oracleDbTypeProperty = parameterType.GetProperty("OracleDbType", BindingFlags.Public | BindingFlags.Instance);
450-
SetOracleDbType = oracleDbTypeProperty.GetSetMethod();
451-
452-
var oracleDbType = OracleAssembly.GetType("Oracle.DataAccess.Client.OracleDbType");
453-
TimestampTz = Enum.Parse(oracleDbType, "TimeStampTZ");
454-
455-
var readerType = OracleAssembly.GetType("Oracle.DataAccess.Client.OracleDataReader");
456-
GetOracleValue = readerType.GetMethod("GetOracleValue", BindingFlags.Public | BindingFlags.Instance);
457-
}
458-
else
459-
{
460-
//TODO This is Microsoft provider support and it does not handle the offsets correctly,
461-
// but I don't know how to make it work.
462-
463-
DateTimeOffsetOutputFormat = "dd-MMM-yy hh:mm:ss.fff tt";
464-
DateTimeOffsetInputFormat = "dd-MMM-yy hh:mm:ss tt";
465-
TimestampTzFormat = "DD-MON-RR HH.MI.SSXFF AM";
466-
467-
// var parameterType = OracleAssembly.GetType("System.Data.OracleClient.OracleParameter");
468-
// var oracleTypeProperty = parameterType.GetProperty("OracleType", BindingFlags.Public | BindingFlags.Instance);
469-
// SetOracleDbType = oracleTypeProperty.GetSetMethod();
470-
471-
var oracleDbType = OracleAssembly.GetType("System.Data.OracleClient.OracleType");
472-
TimestampTz = Enum.Parse(oracleDbType, "TimestampWithTZ");
473-
474-
// var readerType = OracleAssembly.GetType("System.Data.OracleClient.OracleDataReader");
475-
// GetOracleValue = readerType.GetMethod("GetOracleValue", BindingFlags.Public | BindingFlags.Instance);
476-
}
477-
}
478-
479-
private void SetOracleTimestampTzFormat()
480-
{
481-
if (ClientInfo == null) return;
482-
483-
var threadId = Thread.CurrentThread.ManagedThreadId;
484-
if (_threadFormatSet.ContainsKey(threadId)) return;
485-
486-
SetThreadInfo.Invoke(null, new[] { ClientInfo });
487-
_threadFormatSet[threadId] = true;
488-
}
489-
490-
private void SetOracleParameterTypeTimestampTz(IDataParameter p)
491-
{
492-
if (SetOracleDbType != null) SetOracleDbType.Invoke(p, new[] { TimestampTz });
493-
}
494-
495-
private DateTimeOffset ConvertTimestampTzToDateTimeOffset(IDataReader dataReader, int colIndex)
496-
{
497-
if (GetOracleValue != null)
498-
{
499-
var value = GetOracleValue.Invoke(dataReader, new object[] { colIndex }).ToString();
500-
return DateTimeOffset.ParseExact(value, DateTimeOffsetInputFormat, CultureInfo.InvariantCulture);
501-
}
502-
else
503-
{
504-
var value = dataReader.GetValue(colIndex);
505-
return new DateTimeOffset((DateTime)value);
506-
}
507-
}
508-
509427
public override string ToInsertRowStatement(IDbCommand dbCommand, object objWithProperties, ICollection<string> insertFields = null)
510428
{
511429
if (insertFields == null)
@@ -797,7 +715,7 @@ public override List<string> ToCreateIndexStatements(Type tableType)
797715
indexName = NamingStrategy.ApplyNameRestrictions(indexName);
798716

799717
sqlIndexes.Add(
800-
ToCreateIndexStatement(fieldDef.IsUnique, indexName, modelDef, fieldDef.FieldName, false));
718+
ToCreateIndexStatement(fieldDef.IsUnique, indexName, modelDef, fieldDef.FieldName));
801719
}
802720

803721
foreach (var compositeIndex in modelDef.CompositeIndexes)
@@ -807,7 +725,7 @@ public override List<string> ToCreateIndexStatements(Type tableType)
807725
var indexNames = string.Join(",", compositeIndex.FieldNames.ToArray());
808726

809727
sqlIndexes.Add(
810-
ToCreateIndexStatement(compositeIndex.Unique, indexName, modelDef, indexNames, false));
728+
ToCreateIndexStatement(compositeIndex.Unique, indexName, modelDef, indexNames));
811729
}
812730

813731
return sqlIndexes;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
using System.Data;
3+
using System.Data.Common;
4+
using System.Globalization;
5+
using System.Reflection;
6+
7+
namespace ServiceStack.OrmLite.Oracle
8+
{
9+
public class OracleTimestampConverter
10+
{
11+
private string DateTimeOffsetOutputFormat { get; set; }
12+
private string DateTimeOffsetInputFormat { get; set; }
13+
private string TimestampTzFormat { get; set; }
14+
15+
private const BindingFlags InvokeStaticPublic = BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod;
16+
private Assembly OracleAssembly { get; set; }
17+
private object[] SetThreadInfoArgs { get; set; }
18+
private MethodInfo SetThreadInfo { get; set; }
19+
private MethodInfo SetOracleDbType { get; set; }
20+
private object[] SetOracleDbTypeArgs { get; set; }
21+
private MethodInfo GetOracleValue { get; set; }
22+
23+
public OracleTimestampConverter(DbProviderFactory factory)
24+
{
25+
OracleAssembly = factory.GetType().Assembly;
26+
var globalizationType = OracleAssembly.GetType("Oracle.DataAccess.Client.OracleGlobalization");
27+
if (globalizationType != null)
28+
{
29+
DateTimeOffsetInputFormat = DateTimeOffsetOutputFormat = "yyyy-MM-dd HH:mm:ss.ffffff zzz";
30+
TimestampTzFormat = "YYYY-MM-DD HH24:MI:SS.FF6 TZH:TZM";
31+
32+
SetThreadInfoArgs = new [] {globalizationType.InvokeMember("GetClientInfo", InvokeStaticPublic, null, null, null)};
33+
const BindingFlags setProperty = BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.Instance;
34+
globalizationType.InvokeMember("TimeStampTZFormat", setProperty, null, SetThreadInfoArgs[0], new object[] { TimestampTzFormat });
35+
SetThreadInfo = globalizationType.GetMethod("SetThreadInfo", BindingFlags.Public | BindingFlags.Static);
36+
37+
var parameterType = OracleAssembly.GetType("Oracle.DataAccess.Client.OracleParameter");
38+
var oracleDbTypeProperty = parameterType.GetProperty("OracleDbType", BindingFlags.Public | BindingFlags.Instance);
39+
SetOracleDbType = oracleDbTypeProperty.GetSetMethod();
40+
41+
var oracleDbType = OracleAssembly.GetType("Oracle.DataAccess.Client.OracleDbType");
42+
SetOracleDbTypeArgs = new [] {Enum.Parse(oracleDbType, "TimeStampTZ")};
43+
44+
var readerType = OracleAssembly.GetType("Oracle.DataAccess.Client.OracleDataReader");
45+
GetOracleValue = readerType.GetMethod("GetOracleValue", BindingFlags.Public | BindingFlags.Instance);
46+
}
47+
else
48+
{
49+
//TODO This is Microsoft provider support and it does not handle the offsets correctly,
50+
// but I don't know how to make it work.
51+
52+
DateTimeOffsetOutputFormat = "dd-MMM-yy hh:mm:ss.fff tt";
53+
DateTimeOffsetInputFormat = "dd-MMM-yy hh:mm:ss tt";
54+
TimestampTzFormat = "DD-MON-RR HH.MI.SSXFF AM";
55+
56+
// var parameterType = OracleAssembly.GetType("System.Data.OracleClient.OracleParameter");
57+
// var oracleTypeProperty = parameterType.GetProperty("OracleType", BindingFlags.Public | BindingFlags.Instance);
58+
// SetOracleDbType = oracleTypeProperty.GetSetMethod();
59+
60+
var oracleDbType = OracleAssembly.GetType("System.Data.OracleClient.OracleType");
61+
SetOracleDbTypeArgs = new [] {Enum.Parse(oracleDbType, "TimestampWithTZ")};
62+
63+
// var readerType = OracleAssembly.GetType("System.Data.OracleClient.OracleDataReader");
64+
// GetOracleValue = readerType.GetMethod("GetOracleValue", BindingFlags.Public | BindingFlags.Instance);
65+
}
66+
}
67+
68+
public void SetOracleTimestampTzFormat()
69+
{
70+
if (SetThreadInfoArgs != null)
71+
SetThreadInfo.Invoke(null, SetThreadInfoArgs);
72+
}
73+
74+
public void SetOracleParameterTypeTimestampTz(IDataParameter p)
75+
{
76+
if (SetOracleDbType != null)
77+
SetOracleDbType.Invoke(p, SetOracleDbTypeArgs);
78+
}
79+
80+
public DateTimeOffset ConvertTimestampTzToDateTimeOffset(IDataReader dataReader, int colIndex)
81+
{
82+
if (GetOracleValue != null)
83+
{
84+
var value = GetOracleValue.Invoke(dataReader, new object[] { colIndex }).ToString();
85+
return DateTimeOffset.ParseExact(value, DateTimeOffsetInputFormat, CultureInfo.InvariantCulture);
86+
}
87+
else
88+
{
89+
var value = dataReader.GetValue(colIndex);
90+
return new DateTimeOffset((DateTime)value);
91+
}
92+
}
93+
94+
public string ConvertDateTimeOffsetToString(DateTimeOffset timestamp)
95+
{
96+
return timestamp.ToString(DateTimeOffsetOutputFormat, CultureInfo.InvariantCulture);
97+
}
98+
}
99+
}

src/ServiceStack.OrmLite.Oracle/ServiceStack.OrmLite.Oracle.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
<Compile Include="OracleDialect.cs" />
6868
<Compile Include="OracleExecFilter.cs" />
6969
<Compile Include="OracleNamingStrategy.cs" />
70+
<Compile Include="OracleTimestampConverter.cs" />
7071
<Compile Include="Properties\AssemblyInfo.cs" />
7172
<Compile Include="OracleOrmLiteDialectProvider.cs" />
7273
<Compile Include="OraSchema\ClassWriter.cs" />
@@ -89,4 +90,4 @@
8990
</ProjectReference>
9091
</ItemGroup>
9192
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" />
92-
</Project>
93+
</Project>

tests/ServiceStack.OrmLite.Tests/LoadReferencesJoinTests.cs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,8 @@ public void Can_do_LeftJoins_using_SqlExpression()
311311
[Test]
312312
public void Can_Join_on_matching_Alias_convention()
313313
{
314-
AddAliasedCustomers();
314+
Country[] countries;
315+
AddAliasedCustomers(out countries);
315316

316317
//Normal Join
317318
var dbCustomers = db.Select<AliasedCustomer>(q => q
@@ -341,7 +342,7 @@ public void Can_Join_on_matching_Alias_convention()
341342
Assert.That(dbAddresses.Count, Is.EqualTo(3));
342343
}
343344

344-
private void AddAliasedCustomers()
345+
private AliasedCustomer[] AddAliasedCustomers(out Country[] countries)
345346
{
346347
db.DropAndCreateTable<AliasedCustomer>();
347348
db.DropAndCreateTable<AliasedCustomerAddress>();
@@ -381,11 +382,16 @@ private void AddAliasedCustomers()
381382
customers.Each(c =>
382383
db.Save(c, references: true));
383384

384-
db.Insert(
385-
new Country { CountryName = "Australia", CountryCode = "AU" },
386-
new Country { CountryName = "USA", CountryCode = "US" },
387-
new Country { CountryName = "Italy", CountryCode = "IT" },
388-
new Country { CountryName = "Spain", CountryCode = "ED" });
385+
countries = new[]
386+
{
387+
new Country {CountryName = "Australia", CountryCode = "AU"},
388+
new Country {CountryName = "USA", CountryCode = "US"},
389+
new Country {CountryName = "Italy", CountryCode = "IT"},
390+
new Country {CountryName = "Spain", CountryCode = "ED"}
391+
};
392+
db.Save(countries);
393+
394+
return customers;
389395
}
390396

391397
[Test]
@@ -439,7 +445,8 @@ public void Does_populate_custom_columns_based_on_property_convention()
439445
[Test]
440446
public void Does_populate_custom_mixed_columns()
441447
{
442-
AddAliasedCustomers();
448+
Country[] countries;
449+
var customers = AddAliasedCustomers(out countries);
443450

444451
//Normal Join
445452
var results = db.Select<MixedCustomerInfo, AliasedCustomer>(q => q
@@ -456,16 +463,16 @@ public void Does_populate_custom_mixed_columns()
456463
Assert.That(customerNames, Is.EquivalentTo(new[] { "Customer 1", "Customer 2" }));
457464

458465
var customerIds = results.Map(x => x.Q_CustomerId);
459-
Assert.That(customerIds, Is.EquivalentTo(new[] { 1, 2 }));
466+
Assert.That(customerIds, Is.EquivalentTo(new[] { customers[0].Id, customers[1].Id }));
460467

461468
customerIds = results.Map(x => x.Q_CustomerAddressQ_CustomerId);
462-
Assert.That(customerIds, Is.EquivalentTo(new[] { 1, 2 }));
469+
Assert.That(customerIds, Is.EquivalentTo(new[] { customers[0].Id, customers[1].Id }));
463470

464471
var countryNames = results.Map(x => x.CountryName);
465472
Assert.That(countryNames, Is.EquivalentTo(new[] { "Australia", "USA" }));
466473

467474
var countryIds = results.Map(x => x.CountryId);
468-
Assert.That(countryIds, Is.EquivalentTo(new[] { 1, 2 }));
475+
Assert.That(countryIds, Is.EquivalentTo(new[] { countries[0].Id, countries[1].Id }));
469476
}
470477
}
471478
}

0 commit comments

Comments
 (0)