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

Commit a5264e3

Browse files
daleholborowmythz
authored andcommitted
Feature/rowversion: make RowVersion logic cater for ulong and byte[] properties (#603)
* rowversion: allow rowversion to be byte array as well as ulong so that dapper support is catered for * rowversion: I suspect that we dont even need the sql rowversion converter any longer... * rowversion: Removed unused FromDbVersion method because the RowVersionConverter now handles all conversions to rowversion tracking fields
1 parent 8cee8a2 commit a5264e3

File tree

8 files changed

+327
-41
lines changed

8 files changed

+327
-41
lines changed
Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System;
2-
using System.Data;
3-
using ServiceStack.OrmLite.Converters;
1+
using ServiceStack.OrmLite.Converters;
42

53
namespace ServiceStack.OrmLite.SqlServer.Converters
64
{
@@ -10,23 +8,5 @@ public override string ColumnDefinition
108
{
119
get { return "rowversion"; }
1210
}
13-
14-
public override object FromDbValue(Type fieldType, object value)
15-
{
16-
var bytes = value as byte[];
17-
if (bytes != null)
18-
{
19-
var ulongValue = OrmLiteUtils.ConvertToULong(bytes);
20-
return ulongValue;
21-
}
22-
return null;
23-
}
24-
25-
public override ulong FromDbRowVersion(object value)
26-
{
27-
var bytes = value as byte[];
28-
var ulongValue = OrmLiteUtils.ConvertToULong(bytes);
29-
return ulongValue;
30-
}
3111
}
3212
}
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using NUnit.Framework;
5+
using ServiceStack.Data;
6+
using ServiceStack.DataAnnotations;
7+
using ServiceStack.OrmLite.Dapper;
8+
9+
namespace ServiceStack.OrmLite.SqlServerTests
10+
{
11+
[TestFixture]
12+
public class RowVersionTests : OrmLiteTestBase
13+
{
14+
public class ByteChildTbl
15+
{
16+
[PrimaryKey]
17+
public Guid Id { get; set; }
18+
19+
[References(typeof(ByteTbl))]
20+
public Guid ParentId { get; set; }
21+
22+
[RowVersion]
23+
public byte[] RowVersion { get; set; }
24+
25+
26+
}
27+
28+
public class ByteTbl
29+
{
30+
[PrimaryKey]
31+
public Guid Id { get; set; }
32+
33+
public string Text { get; set; }
34+
35+
[RowVersion]
36+
public byte[] RowVersion { get; set; }
37+
38+
[Reference]
39+
public List<ByteChildTbl> Children { get; set; } = new List<ByteChildTbl>();
40+
}
41+
42+
public class UlongChildTbl
43+
{
44+
[PrimaryKey]
45+
public Guid Id { get; set; }
46+
47+
[References(typeof(UlongTbl))]
48+
public Guid ParentId { get; set; }
49+
50+
[RowVersion]
51+
public ulong RowVersion { get; set; }
52+
}
53+
54+
public class UlongTbl
55+
{
56+
[PrimaryKey]
57+
public Guid Id { get; set; }
58+
59+
public string Text { get; set; }
60+
61+
// Use of ulong makes embedded Dapper functionality unavailable
62+
[RowVersion]
63+
public ulong RowVersion { get; set; }
64+
65+
[Reference]
66+
public List<UlongChildTbl> Children { get; set; } = new List<UlongChildTbl>();
67+
}
68+
69+
[Test]
70+
public void Read_and_write_to_tables_with_rowversions()
71+
{
72+
using (var db = OpenDbConnection())
73+
{
74+
// Show that we can drop and create tables with rowversions of both .NET types and both get created as ROWVERSION in MSSQL
75+
db.DropTable<ByteChildTbl>();
76+
db.DropTable<ByteTbl>();
77+
db.DropTable<UlongChildTbl>();
78+
db.DropTable<UlongTbl>();
79+
db.CreateTable<ByteTbl>();
80+
db.CreateTable<ByteChildTbl>();
81+
db.CreateTable<UlongTbl>();
82+
db.CreateTable<UlongChildTbl>();
83+
84+
85+
86+
87+
{
88+
// Confirm off new Ormlite CRUD functionality with byte[] rowversion
89+
90+
var byteId = Guid.NewGuid();
91+
db.Insert(new ByteTbl() { Id = byteId });
92+
93+
var getByteRecord = db.SingleById<ByteTbl>(byteId);
94+
getByteRecord.Text += " Updated";
95+
db.Update(getByteRecord); //success!
96+
97+
getByteRecord.Text += "Attempting to update stale record";
98+
99+
//Can't update stale record
100+
Assert.Throws<OptimisticConcurrencyException>(() => db.Update(getByteRecord));
101+
102+
//Can update latest version
103+
var updatedRow = db.SingleById<ByteTbl>(byteId); // fresh version
104+
updatedRow.Text += "Update Success!";
105+
db.Update(updatedRow);
106+
107+
updatedRow = db.SingleById<ByteTbl>(byteId);
108+
db.Delete(updatedRow); // can delete fresh version
109+
}
110+
111+
112+
{
113+
// confirm original Ormlite CRUD functionality based on ulong rowversion
114+
115+
var ulongId = Guid.NewGuid();
116+
db.Insert(new UlongTbl { Id = ulongId });
117+
118+
var getUlongRecord = db.SingleById<UlongTbl>(ulongId);
119+
getUlongRecord.Text += " Updated";
120+
db.Update(getUlongRecord); //success!
121+
122+
getUlongRecord.Text += "Attempting to update stale record";
123+
124+
//Can't update stale record
125+
Assert.Throws<OptimisticConcurrencyException>(() => db.Update(getUlongRecord));
126+
127+
// Can update latest version
128+
var updatedUlongRow = db.SingleById<UlongTbl>(ulongId); // fresh version
129+
updatedUlongRow.Text += "Update Success!";
130+
db.Update(updatedUlongRow);
131+
132+
updatedUlongRow = db.SingleById<UlongTbl>(ulongId);
133+
db.Delete(updatedUlongRow); // can delete fresh version
134+
}
135+
136+
137+
{
138+
// Confirm that original ulong rowversion unfortunately fails with Dapper custom sql queries
139+
140+
var ulongId = Guid.NewGuid();
141+
db.Insert(new UlongTbl { Id = ulongId });
142+
143+
// As a further example, using Dapper but without rowversion column WILL work (but rowversion will NOT be set of course)
144+
var thisDapperQryWorks = db.Query<UlongTbl>("select Id, Text from [UlongTbl]").ToList();
145+
Assert.True(thisDapperQryWorks.Count == 1);
146+
147+
// But any time RowVersion as ulong is include... no joy.. the map from db rowversion to ulong in dapper fails
148+
Assert.Throws<System.Data.DataException>(() => db.Query<UlongTbl>("select Id, Text, Rowversion from [UlongTbl]"));
149+
}
150+
151+
152+
{
153+
// Now test out use of new byte[] rowversion with Dapper custom sql queries
154+
155+
var byteId = Guid.NewGuid();
156+
db.Insert(new ByteTbl { Id = byteId });
157+
158+
// As a further example, using Dapper but without rowversion column WILL work, BUT the rowversion WONT BE SET (of course)
159+
var thisDapperQryWorks = db.Query<ByteTbl>("select Id, Text from [ByteTbl]").ToList();
160+
Assert.True(thisDapperQryWorks.Count == 1);
161+
Assert.True(thisDapperQryWorks.First().RowVersion == default(byte[]));
162+
163+
// But any time RowVersion as ulong is include... no joy.. the map from db rowversion to ulong in dapper fails
164+
var thisDapperQryWithRowVersionAlsoWorks = db.Query<ByteTbl>("select Id, Text, Rowversion from [ByteTbl]").ToList();
165+
Assert.True(thisDapperQryWithRowVersionAlsoWorks.Count == 1);
166+
Assert.True(thisDapperQryWithRowVersionAlsoWorks.First().RowVersion != default(byte[]));
167+
}
168+
169+
170+
{
171+
// test original ulong version with child operations
172+
var ulongId1 = Guid.NewGuid();
173+
db.Save(new UlongTbl
174+
{
175+
Id = ulongId1,
176+
Children = new List<UlongChildTbl>()
177+
{
178+
new UlongChildTbl { Id = Guid.NewGuid() },
179+
new UlongChildTbl { Id = Guid.NewGuid() }
180+
}
181+
}, references: true);
182+
var ulongObj1 = db.LoadSelect<UlongTbl>(x => x.Id == ulongId1, x => x.Children).First();
183+
Assert.AreNotEqual(default(byte[]), ulongObj1.RowVersion);
184+
Assert.AreNotEqual(default(byte[]), ulongObj1.Children[0].RowVersion);
185+
Assert.AreNotEqual(default(byte[]), ulongObj1.Children[1].RowVersion);
186+
187+
var ulongId2 = Guid.NewGuid();
188+
db.Save(new UlongTbl
189+
{
190+
Id = ulongId2,
191+
Children = new List<UlongChildTbl>()
192+
{
193+
new UlongChildTbl {Id = Guid.NewGuid()},
194+
new UlongChildTbl {Id = Guid.NewGuid()}
195+
}
196+
}, references: true);
197+
var ulongObj2 = db.LoadSelect<UlongTbl>(x => x.Id == ulongId2, x => x.Children).First();
198+
Assert.AreNotEqual(default(byte[]), ulongObj2.RowVersion);
199+
Assert.AreNotEqual(default(byte[]), ulongObj2.Children[0].RowVersion);
200+
Assert.AreNotEqual(default(byte[]), ulongObj2.Children[1].RowVersion);
201+
202+
// COnfirm multip select logic works
203+
var q = db.From<UlongTbl>()
204+
.Join<UlongTbl, UlongChildTbl>()
205+
;
206+
var results = db.SelectMulti<UlongTbl, UlongChildTbl>(q);
207+
foreach (var tuple in results)
208+
{
209+
UlongTbl parent = tuple.Item1;
210+
UlongChildTbl child = tuple.Item2;
211+
Assert.AreNotEqual(default(byte[]), parent.RowVersion);
212+
Assert.AreNotEqual(default(byte[]), child.RowVersion);
213+
}
214+
}
215+
216+
{
217+
// test byte[] version with child operations
218+
var byteid1 = Guid.NewGuid();
219+
db.Save(new ByteTbl
220+
{
221+
Id = byteid1,
222+
Children = new List<ByteChildTbl>()
223+
{
224+
new ByteChildTbl() { Id = Guid.NewGuid(), ParentId = byteid1},
225+
new ByteChildTbl { Id = Guid.NewGuid(), ParentId = byteid1 }
226+
}
227+
}, references: true);
228+
var byteObj1 = db.LoadSelect<ByteTbl>(x => x.Id == byteid1, x => x.Children).First();
229+
Assert.AreNotEqual(default(byte[]), byteObj1.RowVersion);
230+
Assert.AreNotEqual(default(byte[]), byteObj1.Children[0].RowVersion);
231+
Assert.AreNotEqual(default(byte[]), byteObj1.Children[1].RowVersion);
232+
233+
var byteid2 = Guid.NewGuid();
234+
db.Save(new ByteTbl
235+
{
236+
Id = byteid2,
237+
Children = new List<ByteChildTbl>()
238+
{
239+
new ByteChildTbl {Id = Guid.NewGuid(), ParentId = byteid2},
240+
new ByteChildTbl {Id = Guid.NewGuid(), ParentId = byteid2}
241+
}
242+
}, references: true);
243+
var byteObj2 = db.LoadSelect<ByteTbl>(x => x.Id == byteid2, x => x.Children).First();
244+
Assert.AreNotEqual(default(byte[]), byteObj2.RowVersion);
245+
Assert.AreNotEqual(default(byte[]), byteObj2.Children[0].RowVersion);
246+
Assert.AreNotEqual(default(byte[]), byteObj2.Children[1].RowVersion);
247+
248+
// COnfirm multip select logic works
249+
var q = db.From<ByteTbl>()
250+
.Join<ByteTbl, ByteChildTbl>()
251+
;
252+
var results = db.SelectMulti<ByteTbl, ByteChildTbl>(q);
253+
foreach (var tuple in results)
254+
{
255+
ByteTbl parent = tuple.Item1;
256+
ByteChildTbl child = tuple.Item2;
257+
Assert.AreNotEqual(default(byte[]), parent.RowVersion);
258+
Assert.AreNotEqual(default(byte[]), child.RowVersion);
259+
}
260+
}
261+
262+
263+
{
264+
// test the multi select using dapper
265+
266+
var byteid2 = Guid.NewGuid();
267+
db.Save(new ByteTbl
268+
{
269+
Id = byteid2,
270+
Children = new List<ByteChildTbl>()
271+
{
272+
new ByteChildTbl {Id = Guid.NewGuid(), ParentId = byteid2},
273+
new ByteChildTbl {Id = Guid.NewGuid(), ParentId = byteid2}
274+
}
275+
}, references: true);
276+
277+
var q = db.From<ByteTbl>()
278+
.Join<ByteTbl, ByteChildTbl>()
279+
.Select("*");
280+
281+
using (var multi = db.QueryMultiple(q.ToSelectStatement()))
282+
{
283+
var results = multi.Read<ByteTbl, ByteChildTbl,
284+
Tuple<ByteTbl, ByteChildTbl>>(Tuple.Create).ToList();
285+
286+
foreach (var tuple in results)
287+
{
288+
ByteTbl parent = tuple.Item1;
289+
ByteChildTbl child = tuple.Item2;
290+
Assert.AreNotEqual(default(byte[]), parent.RowVersion);
291+
Assert.AreNotEqual(default(byte[]), child.RowVersion);
292+
}
293+
Assert.True(results.Count > 0);
294+
}
295+
}
296+
//Console.WriteLine("hit a key to end test");
297+
//Console.ReadLine();
298+
299+
300+
}
301+
}
302+
}
303+
}

src/ServiceStack.OrmLite/Async/OrmLiteWriteCommandExtensionsAsync.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -523,11 +523,11 @@ internal static Task ExecuteProcedureAsync<T>(this IDbCommand dbCommand, T obj,
523523
return dbCommand.ExecuteSqlAsync(sql, token);
524524
}
525525

526-
internal static Task<ulong> GetRowVersionAsync(this IDbCommand dbCmd, ModelDefinition modelDef, object id, CancellationToken token)
526+
internal static Task<object> GetRowVersionAsync(this IDbCommand dbCmd, ModelDefinition modelDef, object id, CancellationToken token)
527527
{
528528
var sql = dbCmd.RowVersionSql(modelDef, id);
529-
return dbCmd.ScalarAsync<ulong>(sql, token)
530-
.Success(x => dbCmd.GetDialectProvider().FromDbRowVersion(x));
529+
return dbCmd.ScalarAsync<object>(sql, token)
530+
.Success(x => dbCmd.GetDialectProvider().FromDbRowVersion(modelDef.RowVersion.FieldType, x));
531531
}
532532
}
533533
}

src/ServiceStack.OrmLite/Converters/SpecialConverters.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,18 @@ public class RowVersionConverter : OrmLiteConverter
6767
{
6868
public override string ColumnDefinition => "BIGINT";
6969

70-
public virtual ulong FromDbRowVersion(object value)
71-
{
72-
return (ulong)this.ConvertNumber(typeof(ulong), value);
73-
}
74-
7570
public override object FromDbValue(Type fieldType, object value)
7671
{
77-
return value != null
78-
? this.ConvertNumber(typeof(ulong), value)
79-
: null;
72+
var bytes = value as byte[];
73+
if (bytes != null)
74+
{
75+
if (fieldType == typeof(byte[])) return bytes;
76+
if (fieldType == typeof(ulong)) return OrmLiteUtils.ConvertToULong(bytes);
77+
78+
// an SQL row version has to be declared as either byte[] OR ulong...
79+
throw new Exception("Rowversion property must be declared as either byte[] or ulong");
80+
}
81+
return null;
8082
}
8183
}
8284

src/ServiceStack.OrmLite/IOrmLiteDialectProvider.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ string ToSelectFromProcedureStatement(object fromObjWithProperties,
161161

162162
void DropColumn(IDbConnection db, Type modelType, string columnName);
163163

164-
ulong FromDbRowVersion(object value);
164+
object FromDbRowVersion(Type fieldType, object value);
165+
165166
SelectItem GetRowVersionColumnName(FieldDefinition field, string tablePrefix = null);
166167

167168
string GetColumnNames(ModelDefinition modelDef);

src/ServiceStack.OrmLite/OrmLiteConfigExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ internal static ModelDefinition GetModelDefinition(this Type modelType)
9595
|| propertyInfo.HasAttributeNamed(typeof(PrimaryKeyAttribute).Name);
9696

9797
var isRowVersion = propertyInfo.Name == ModelDefinition.RowVersionName
98-
&& propertyInfo.PropertyType == typeof(ulong);
98+
&& (propertyInfo.PropertyType == typeof(ulong) || propertyInfo.PropertyType == typeof(byte[]));
9999

100100
var isNullableType = propertyInfo.PropertyType.IsNullableType();
101101

0 commit comments

Comments
 (0)