From f17116c8132dd9149fd99fcc519dc36ee53c5503 Mon Sep 17 00:00:00 2001
From: Maxwell Weru <mburumaxwell@gmail.com>
Date: Sat, 17 Aug 2024 17:13:31 +0300
Subject: [PATCH] Allow conversion of `Etag` in EFCore to `byte[]`, `int`,
 `uint`, `long`, or `ulong` (#306)

Depending on the database type, the row version may have different types.
SQL Server uses `byte[]` for row version, Postgres uses `uint` for `xid`/`xmin`, while the new Mongo driver for EFCore can use `int`, `uint`, `long`, or `ulong` since it is developer driven.

This also allows a concurrency setups that are not not entirely database driven.
---
 .../Converters/EtagConverter.cs               | 42 +++++++++++-
 .../ModelConfigurationBuilderExtensions.cs    | 50 ++++++++++++++-
 .../Extensions/PropertyBuilderExtensions.cs   | 64 ++++++++++++++++++-
 3 files changed, 148 insertions(+), 8 deletions(-)

diff --git a/src/Tingle.Extensions.EntityFrameworkCore/Converters/EtagConverter.cs b/src/Tingle.Extensions.EntityFrameworkCore/Converters/EtagConverter.cs
index a61e9caa..c43d7580 100644
--- a/src/Tingle.Extensions.EntityFrameworkCore/Converters/EtagConverter.cs
+++ b/src/Tingle.Extensions.EntityFrameworkCore/Converters/EtagConverter.cs
@@ -5,11 +5,47 @@
 namespace Tingle.Extensions.EntityFrameworkCore.Converters;
 
 ///
-public class EtagConverter : ValueConverter<Etag, byte[]>
+public class EtagToBytesConverter : ValueConverter<Etag, byte[]>
 {
     ///
-    public EtagConverter() : base(convertToProviderExpression: v => v.ToByteArray(),
-                                  convertFromProviderExpression: v => v == null ? default : new Etag(v))
+    public EtagToBytesConverter() : base(convertToProviderExpression: v => v.ToByteArray(),
+                                         convertFromProviderExpression: v => v == null ? default : new Etag(v))
+    { }
+}
+
+///
+public class EtagToInt32Converter : ValueConverter<Etag, int>
+{
+    ///
+    public EtagToInt32Converter() : base(convertToProviderExpression: v => Convert.ToInt32((ulong)v),
+                                         convertFromProviderExpression: v => new Etag(Convert.ToUInt64(v)))
+    { }
+}
+
+///
+public class EtagToUInt32Converter : ValueConverter<Etag, uint>
+{
+    ///
+    public EtagToUInt32Converter() : base(convertToProviderExpression: v => Convert.ToUInt32((ulong)v),
+                                          convertFromProviderExpression: v => new Etag(v))
+    { }
+}
+
+///
+public class EtagToInt64Converter : ValueConverter<Etag, long>
+{
+    ///
+    public EtagToInt64Converter() : base(convertToProviderExpression: v => Convert.ToInt64((ulong)v),
+                                         convertFromProviderExpression: v => new Etag(Convert.ToUInt64(v)))
+    { }
+}
+
+///
+public class EtagToUInt64Converter : ValueConverter<Etag, ulong>
+{
+    ///
+    public EtagToUInt64Converter() : base(convertToProviderExpression: v => (ulong)v,
+                                          convertFromProviderExpression: v => new Etag(v))
     { }
 }
 
diff --git a/src/Tingle.Extensions.EntityFrameworkCore/Extensions/ModelConfigurationBuilderExtensions.cs b/src/Tingle.Extensions.EntityFrameworkCore/Extensions/ModelConfigurationBuilderExtensions.cs
index 959cf009..22bd9b3c 100644
--- a/src/Tingle.Extensions.EntityFrameworkCore/Extensions/ModelConfigurationBuilderExtensions.cs
+++ b/src/Tingle.Extensions.EntityFrameworkCore/Extensions/ModelConfigurationBuilderExtensions.cs
@@ -17,14 +17,58 @@ namespace Microsoft.EntityFrameworkCore;
 public static class ModelConfigurationBuilderExtensions
 {
     /// <summary>
-    /// Add fields of type <see cref="Etag"/> to be converted using <see cref="EtagConverter"/>.
+    /// Add fields of type <see cref="Etag"/> to be converted to a <see cref="T:byte[]"/>.
     /// </summary>
     /// <param name="configurationBuilder">The <see cref="ModelConfigurationBuilder"/> to use.</param>
-    public static void AddEtagConventions(this ModelConfigurationBuilder configurationBuilder)
+    public static void AddEtagToBytesConventions(this ModelConfigurationBuilder configurationBuilder)
     {
         ArgumentNullException.ThrowIfNull(configurationBuilder);
 
-        configurationBuilder.Properties<Etag>().HaveConversion<EtagConverter, EtagComparer>();
+        configurationBuilder.Properties<Etag>().HaveConversion<EtagToBytesConverter, EtagComparer>();
+    }
+
+    /// <summary>
+    /// Add fields of type <see cref="Etag"/> to be converted to a <see cref="uint"/>.
+    /// </summary>
+    /// <param name="configurationBuilder">The <see cref="ModelConfigurationBuilder"/> to use.</param>
+    public static void AddEtagToInt32Conventions(this ModelConfigurationBuilder configurationBuilder)
+    {
+        ArgumentNullException.ThrowIfNull(configurationBuilder);
+
+        configurationBuilder.Properties<Etag>().HaveConversion<EtagToInt32Converter, EtagComparer>();
+    }
+
+    /// <summary>
+    /// Add fields of type <see cref="Etag"/> to be converted to a <see cref="int"/>.
+    /// </summary>
+    /// <param name="configurationBuilder">The <see cref="ModelConfigurationBuilder"/> to use.</param>
+    public static void AddEtagToUInt32Conventions(this ModelConfigurationBuilder configurationBuilder)
+    {
+        ArgumentNullException.ThrowIfNull(configurationBuilder);
+
+        configurationBuilder.Properties<Etag>().HaveConversion<EtagToUInt32Converter, EtagComparer>();
+    }
+
+    /// <summary>
+    /// Add fields of type <see cref="Etag"/> to be converted to a <see cref="long"/>.
+    /// </summary>
+    /// <param name="configurationBuilder">The <see cref="ModelConfigurationBuilder"/> to use.</param>
+    public static void AddEtagToInt64Conventions(this ModelConfigurationBuilder configurationBuilder)
+    {
+        ArgumentNullException.ThrowIfNull(configurationBuilder);
+
+        configurationBuilder.Properties<Etag>().HaveConversion<EtagToInt64Converter, EtagComparer>();
+    }
+
+    /// <summary>
+    /// Add fields of type <see cref="Etag"/> to be converted to a <see cref="ulong"/>.
+    /// </summary>
+    /// <param name="configurationBuilder">The <see cref="ModelConfigurationBuilder"/> to use.</param>
+    public static void AddEtagToUInt64Conventions(this ModelConfigurationBuilder configurationBuilder)
+    {
+        ArgumentNullException.ThrowIfNull(configurationBuilder);
+
+        configurationBuilder.Properties<Etag>().HaveConversion<EtagToUInt64Converter, EtagComparer>();
     }
 
     /// <summary>
diff --git a/src/Tingle.Extensions.EntityFrameworkCore/Extensions/PropertyBuilderExtensions.cs b/src/Tingle.Extensions.EntityFrameworkCore/Extensions/PropertyBuilderExtensions.cs
index 3763572a..4a850dbf 100644
--- a/src/Tingle.Extensions.EntityFrameworkCore/Extensions/PropertyBuilderExtensions.cs
+++ b/src/Tingle.Extensions.EntityFrameworkCore/Extensions/PropertyBuilderExtensions.cs
@@ -22,11 +22,71 @@ public static class PropertyBuilderExtensions
     /// </summary>
     /// <param name="propertyBuilder">The <see cref="PropertyBuilder{TProperty}"/> to extend.</param>
     /// <returns></returns>
-    public static PropertyBuilder<Etag> HasEtagConversion(this PropertyBuilder<Etag> propertyBuilder)
+    public static PropertyBuilder<Etag> HasEtagToBytesConversion(this PropertyBuilder<Etag> propertyBuilder)
     {
         ArgumentNullException.ThrowIfNull(propertyBuilder);
 
-        propertyBuilder.HasConversion(new EtagConverter());
+        propertyBuilder.HasConversion(new EtagToBytesConverter());
+        propertyBuilder.Metadata.SetValueComparer(new EtagComparer());
+
+        return propertyBuilder;
+    }
+
+    /// <summary>
+    /// Attach conversion of property to/from <see cref="Etag"/> stored in the database as a <see cref="int"/>.
+    /// </summary>
+    /// <param name="propertyBuilder">The <see cref="PropertyBuilder{TProperty}"/> to extend.</param>
+    /// <returns></returns>
+    public static PropertyBuilder<Etag> HasEtagToInt32Conversion(this PropertyBuilder<Etag> propertyBuilder)
+    {
+        ArgumentNullException.ThrowIfNull(propertyBuilder);
+
+        propertyBuilder.HasConversion(new EtagToInt32Converter());
+        propertyBuilder.Metadata.SetValueComparer(new EtagComparer());
+
+        return propertyBuilder;
+    }
+
+    /// <summary>
+    /// Attach conversion of property to/from <see cref="Etag"/> stored in the database as a <see cref="uint"/>.
+    /// </summary>
+    /// <param name="propertyBuilder">The <see cref="PropertyBuilder{TProperty}"/> to extend.</param>
+    /// <returns></returns>
+    public static PropertyBuilder<Etag> HasEtagToUInt32Conversion(this PropertyBuilder<Etag> propertyBuilder)
+    {
+        ArgumentNullException.ThrowIfNull(propertyBuilder);
+
+        propertyBuilder.HasConversion(new EtagToUInt32Converter());
+        propertyBuilder.Metadata.SetValueComparer(new EtagComparer());
+
+        return propertyBuilder;
+    }
+
+    /// <summary>
+    /// Attach conversion of property to/from <see cref="Etag"/> stored in the database as a <see cref="long"/>.
+    /// </summary>
+    /// <param name="propertyBuilder">The <see cref="PropertyBuilder{TProperty}"/> to extend.</param>
+    /// <returns></returns>
+    public static PropertyBuilder<Etag> HasEtagToInt64Conversion(this PropertyBuilder<Etag> propertyBuilder)
+    {
+        ArgumentNullException.ThrowIfNull(propertyBuilder);
+
+        propertyBuilder.HasConversion(new EtagToInt64Converter());
+        propertyBuilder.Metadata.SetValueComparer(new EtagComparer());
+
+        return propertyBuilder;
+    }
+
+    /// <summary>
+    /// Attach conversion of property to/from <see cref="Etag"/> stored in the database as a <see cref="ulong"/>.
+    /// </summary>
+    /// <param name="propertyBuilder">The <see cref="PropertyBuilder{TProperty}"/> to extend.</param>
+    /// <returns></returns>
+    public static PropertyBuilder<Etag> HasEtagToUInt64Conversion(this PropertyBuilder<Etag> propertyBuilder)
+    {
+        ArgumentNullException.ThrowIfNull(propertyBuilder);
+
+        propertyBuilder.HasConversion(new EtagToUInt64Converter());
         propertyBuilder.Metadata.SetValueComparer(new EtagComparer());
 
         return propertyBuilder;