Can't use Ormlite Rowversion and Dapper custom SQL in same project

Hi again.

I would like to revisit an issue that I discovered earlier with Dapper/RowVersion (WHERE IN params for custom / raw sql fail?), as it’s looking more serious.

Specifically, the use of ulong for Ormlite RowVersion functionality means I am subsequently unable to use any of the embedded Dapper functionality to perform complex queries (I am particular interested in the QueryMulti logic, as per the attached demo), because of a resulting error:

System.InvalidOperationException: 'Error parsing column 2 (RowVersion=System.Byte[] - Object)'
Inner Exception: InvalidCastException: Object must implement IConvertible.

You suggested that perhaps including the RowVersion column explicitly in my query params rather than generically might get around the issue
(select t.RowVersion vs select t.*) but unfortunately it seems that that isn’t the case.

I looked at the SqlServer2012OrmLiteDialectProvider and considered trying to implement my own SqlRowVersion converter using a rowversion of type byte[] rather than ulong, but ulong is baked in quite heavily to the logic, so I doubt that’s a good/possible approach?

So basically, it seems that we am stuck either:

a) able to use Ormlite with RowVersion / optimistic concurrency checking functionality but lose the ability/flexibility of Dapper-based custom queries (which is likely to be a big problem going forward), or

b) regain Dapper/custom SQL by ditching the RowVersion capability (which is also going to be technically/politically awkward for this project).

Does anyone have any suggestions / workarounds? As it is, we’re kind of stopped without a clear way forward.

Thanks in advance

Sample Demo:

using System;
using System.Collections.Generic;
using System.Linq;
using ServiceStack.DataAnnotations;
using ServiceStack.OrmLite;
using ServiceStack.OrmLite.Dapper;
using ServiceStack.OrmLite.SqlServer;

namespace ConsoleApp1
{
	internal class Program
	{
		private static void Main(string[] args)
		{
			var connectionString = "Server=localhost;Database=debug.NUKEABLE;User Id=sa; Password=mypass;";
			var fac = new OrmLiteConnectionFactory(connectionString, SqlServer2012OrmLiteDialectProvider.Instance);
			using (var db = fac.OpenDbConnection())
			{
				db.DropTable<ChildTbl>(); db.DropTable<ParentTbl>();
				db.CreateTable<ParentTbl>(); db.CreateTable<ChildTbl>();
				var parentTbl = new ParentTbl { ParentId = Guid.NewGuid(), Name = "a",
					Children = new List<ChildTbl>
					{
						new ChildTbl { ChildId = Guid.NewGuid(), Name = "a", StartDate = DateTime.Today.AddDays(3) },
						new ChildTbl { ChildId = Guid.NewGuid(), Name = "b", StartDate = DateTime.Today.AddDays(2) },
						new ChildTbl { ChildId = Guid.NewGuid(), Name = "c", StartDate = DateTime.Today.AddDays(1) }
					} };
				db.Save(parentTbl, true);

				var complexMultiQueryThatIWantToUseWithDapperQueryMulti = $@"
select P.ParentId, P.Name, P.RowVersion, C.ChildId, C.ParentId, C.Name, C.StartDate, C.RowVersion
From ChildTbl C
join ParentTbl P on P.ParentId = C.ParentId
INNER JOIN 
(
	select C.ParentId, Min(C.StartDate) as MinStartDate
	from ChildTbl C
	Group BY C.ParentId
) as GRP
ON GRP.MinStartDate = C.StartDate and GRP.ParentId = C.ParentId";


				using (var multi = db.QueryMultiple(complexMultiQueryThatIWantToUseWithDapperQueryMulti))
				{
					// This line will crash because of inability to parse RowVersion
					var results = multi.Read<ParentTbl, ChildTbl,
						Tuple<ParentTbl, ChildTbl>>(Tuple.Create,
						splitOn: $"{nameof(ParentTbl.ParentId)}, {nameof(ChildTbl.ChildId)}"
						).ToList();

					foreach (var res in results)
					{
						// do stuff but we don't get this far
					}
				}


				// As a further example, using Dapper but without rowversion column WILL work
				var thisDapperQryWorks = db.Query<ParentTbl>("select ParentId, Name from [ParentTbl]");

				// But any time RowVersion as ulong is include... no joy
				var thisDapperQueryFails = db.Query<ParentTbl>("select ParentId, Name, RowVersion from [ParentTbl]");

				Console.WriteLine("hit a key to end test");
				Console.ReadLine();
			}
		}
	}

	public class ParentTbl
	{
		[PrimaryKey]
		public Guid ParentId { get; set; }
		public string Name { get; set; }
		// Use of ulong makes embedded Dapper functionality unavailable
		[RowVersion]
		public ulong RowVersion { get; set; }
		[Reference]
		public List<ChildTbl> Children { get; set; } = new List<ChildTbl>();
	}

	public class ChildTbl
	{
		[PrimaryKey]
		public Guid ChildId { get; set; }
		[References(typeof(ParentTbl))]
		public Guid ParentId { get; set; }
		public string Name { get; set; }
		// Use of ulong makes embedded Dapper functionality unavailable
		[RowVersion]
		public ulong RowVersion { get; set; }
		public DateTime StartDate { get; set; }
	}
}

If your queries don’t return RowVersion column than it shouldn’t be able to impact Dapper. Otherwise you’d likely need to ditch using RowVersion unless the Dapper team knows of another workaround.

Rightoh. I’ll see what I can find and let you know if I uncover a solution .
Thanks!

I haven’t come across anything from the Dapper team or elsewhere on the internet, but as an experiement last night I took a copy of the Ormlite source code and I made the switch over to byte[] rather than ulong in all the SqlServer and IOrmliteDialect interfaces etc , and then both Ormlite library methods and custom dapper queries appear to work nicely. Which made me think that I should be able to tweak the RowVersion implementation to be a generic format that handles both, with the user able to select / configure which they prefer.

I wondered if you’d entertain the idea of this as a modification, and if so, whats the best strategy? Make a fork and do the refactoring and just submit a pull request? Or, before I spend the time and attempt to do so, if you are deadset against the idea?

I’ve provided a broad example of the modifications I would attempt, basically adding a new interface to define IRowVersionConverter and the current IOrmLiteDialectProvider interface (and implementations) would change from containing a RowVersionConverter property, and instead use that new interface all the way up the chain. The SqlServer2012OrmLiteDialectProvider would then be configuratble to use either the ulong or byte[]-specific concrete rowversionconverter implementations.

Take a look and let me know if you are happy for me to take a crack at implementing this - Hopefully you get the idea of what I mean. I’m a big fan of the SS project and would love to be able to offer something back to the community.

// new base interface that the OrmLiteDialectProviderBase and IOrmLiteDialectProvider 
// would implement instead of current RowVersionConverter
public interface IRowVersionConverter<TRowVersionType>
{
	TRowVersionType FromDbRowVersion(object value);
}

// equivalent to original ormlite rowversionconverter but implementing new interface, should keep existing users using ulong happy
public class RowVersionConverter : OrmLiteConverter, IRowVersionConverter<ulong>
{
	public override string ColumnDefinition => "BIGINT";

	public virtual ulong FromDbRowVersion(object value)
	{
		return (ulong)this.ConvertNumber(typeof(ulong), value);
	}

	public override object FromDbValue(Type fieldType, object value)
	{
		return value != null
			? this.ConvertNumber(typeof(ulong), value)
			: null;
	}
}

// Original sql row converter effectively unchanged - will use the ulong mode
public class SqlServerRowVersionConverter : RowVersionConverter
{
	public override string ColumnDefinition
	{
		get { return "rowversion"; }
	}

	public override object FromDbValue(Type fieldType, object value)
	{
		var bytes = value as byte[];
		if (bytes != null)
		{
			var ulongValue = OrmLiteUtils.ConvertToULong(bytes);
			return ulongValue;
		}
		return null;
	}

	public override ulong FromDbRowVersion(object value)
	{
		var bytes = value as byte[];
		var ulongValue = OrmLiteUtils.ConvertToULong(bytes);
		return ulongValue;
	}
}

// proposed NEW sql server converter, to cater for .NET using byte[] array for rowversion column (particularly to play nice with Dapper)
public class DalesByteArraySqlServerRowVersionConverter : OrmLiteConverter, IRowVersionConverter<byte[]>
{
	public override string ColumnDefinition
	{
		get { return "rowversion"; }
	}

	public override object FromDbValue(Type fieldType, object value)
	{
		var bytes = value as byte[];
		if (bytes != null)
		{
			//var ulongValue = OrmLiteUtils.ConvertToULong(bytes);
			//return ulongValue;
			return bytes;
		}
		return null;
	}

	public byte[] FromDbRowVersion(object value)
	{
		var bytes = value as byte[];
		//var ulongValue = OrmLiteUtils.ConvertToULong(bytes);
		return bytes;
	}
}

Thoughts? Suggestion/guidance for implementation?

The default behavior needs to be backwards compatible and support existing ulong RowVersions, but if you can also add support for classes with byte[] RowVersion we’d definitely be interested. Focus for the implementation should try to be minimally disruptive to the existing code-base, it doesn’t need to introduce new interfaces or be overridable/customizable as it wont need to support any other Type, so the preference should be on the simplest impl that would work. Ideally it would just be detecting which impl to use based on the Type of the RowVersion property so it works transparently and no additional configuration is needed.

I’ve submitted a pull request (https://github.com/ServiceStack/ServiceStack.OrmLite/pull/603) specifically on a feature branch named “rowversion”, you were right, the changed ended up being VASTLY less complicated than i originally anticipated, but as per the pull request, I am unable to confrim test cases against any db that is not MSSQL 2012 as I dont have instances of those.

I should mention that on a side note, running the full suite of MS SQL Server unit tests , I have lots of errors generally speaking - geo tests, json expressions and sql expression visitors for example - but i believe (expect/hope) that they are nothing to do with my changes.

Awesome thx for the sweet PR + tests!

This support is now available in the latest ServiceStack v5 that’s now available on MyGet.