提交 dfdc3364 编写于 作者: S Shay Rojansky

Added value comparer for arrays

* Multidimensional arrays not yet supported (#314)
* In some cases boxing will occur when comparing (and snapshotting) the
  elements (https://github.com/aspnet/EntityFrameworkCore/issues/11072)

Closes #305
上级 81b78710
......@@ -23,7 +23,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Text;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
namespace Microsoft.EntityFrameworkCore.Storage.Internal
{
......@@ -35,11 +40,13 @@ public sealed class NpgsqlArrayTypeMapping : RelationalTypeMapping
/// Creates the default array mapping (i.e. for the single-dimensional CLR array type)
/// </summary>
internal NpgsqlArrayTypeMapping(RelationalTypeMapping elementMapping)
: this(elementMapping, elementMapping.ClrType.MakeArrayType())
{}
: this(elementMapping, elementMapping.ClrType.MakeArrayType()) {}
internal NpgsqlArrayTypeMapping(RelationalTypeMapping elementMapping, Type arrayType)
: base(GenerateArrayTypeName(elementMapping.StoreType), arrayType)
: this(elementMapping, arrayType, CreateComparer(elementMapping, arrayType)) {}
NpgsqlArrayTypeMapping(RelationalTypeMapping elementMapping, Type arrayType, ValueComparer comparer)
: base(GenerateArrayTypeName(elementMapping.StoreType), arrayType, null, comparer)
{
ElementMapping = elementMapping;
}
......@@ -74,7 +81,7 @@ static string GenerateArrayTypeName(string elementTypeName)
}
public override RelationalTypeMapping Clone(string storeType, int? size)
=> new NpgsqlArrayTypeMapping(ElementMapping);
=> new NpgsqlArrayTypeMapping(ElementMapping, ClrType, Comparer);
protected override string GenerateNonNullSqlLiteral(object value)
{
......@@ -95,5 +102,145 @@ protected override string GenerateNonNullSqlLiteral(object value)
sb.Append("]");
return sb.ToString();
}
#region Value Comparison
static ValueComparer CreateComparer(RelationalTypeMapping elementMapping, Type arrayType)
{
Debug.Assert(arrayType.IsArray);
var elementType = arrayType.GetElementType();
// In .NET, single-dimensional arrays implement IList<T> and can therefore be accessed generically
// (i.e. efficiently). Multi-dimensional arrays don't, and can only be accessed non-generically via IList.
if (!typeof(IList<>).MakeGenericType(elementType).IsAssignableFrom(arrayType))
return null; // TODO: Implement multi-dimensional array support (#314)
// We usee different comparer implementations based on whether we have a non-null element comparer,
// and if not, whether the element is IEquatable<TElem>
if (elementMapping.Comparer != null)
return (ValueComparer)Activator.CreateInstance(
typeof(SingleDimComparerWithComparer<>).MakeGenericType(elementType), elementMapping);
if (typeof(IEquatable<>).MakeGenericType(elementType).IsAssignableFrom(elementType))
return (ValueComparer)Activator.CreateInstance(typeof(SingleDimComparerWithIEquatable<>).MakeGenericType(elementType));
// There's no custom comparer, and the element type doesn't implement IEquatable<TElem>. We have
// no choice but to use the non-generic Equals method.
return (ValueComparer)Activator.CreateInstance(typeof(SingleDimComparerWithEquals<>).MakeGenericType(elementType));
}
class SingleDimComparerWithComparer<TElem> : ValueComparer<IList<TElem>>
{
public SingleDimComparerWithComparer(RelationalTypeMapping elementMapping) : base(
(a, b) => Compare(a, b, elementMapping.Comparer.CompareFunc),
source => Snapshot(source, elementMapping.Comparer.SnapshotFunc)) {}
static bool Compare(IList<TElem> a, IList<TElem> b, Func<object, object, bool> elementComparer)
{
if (a.Count != b.Count)
return false;
// Note: the following currently boxes every element access because ValueComparer isn't really
// generic (see https://github.com/aspnet/EntityFrameworkCore/issues/11072)
for (var i = 0; i < a.Count; i++)
if (!elementComparer(a[i], b[i]))
return false;
return true;
}
static IList<TElem> Snapshot(IList<TElem> source, Func<object, object> elementSnapshotFunc)
{
var snapshot = new TElem[source.Count];
// Note: the following currently boxes every element access because ValueComparer isn't really
// generic (see https://github.com/aspnet/EntityFrameworkCore/issues/11072)
for (var i = 0; i < source.Count; i++)
snapshot[i] = (TElem)elementSnapshotFunc(source[i]);
return snapshot;
}
}
class SingleDimComparerWithIEquatable<TElem> : ValueComparer<IList<TElem>>
where TElem : IEquatable<TElem>
{
public SingleDimComparerWithIEquatable(): base(
(a, b) => Compare(a, b),
source => Snapshot(source)) {}
static bool Compare(IList<TElem> a, IList<TElem> b)
{
if (a.Count != b.Count)
return false;
for (var i = 0; i < a.Count; i++)
{
var elem1 = a[i];
var elem2 = b[i];
if (elem1 == null)
{
if (elem2 == null)
continue;
return false;
}
if (!elem1.Equals(elem2))
return false;
}
return true;
}
static IList<TElem> Snapshot(IList<TElem> source)
{
var snapshot = new TElem[source.Count];
for (var i = 0; i < source.Count; i++)
snapshot[i] = source[i];
return snapshot;
}
}
class SingleDimComparerWithEquals<TElem> : ValueComparer<IList<TElem>>
{
public SingleDimComparerWithEquals() : base(
(a, b) => Compare(a, b),
source => Snapshot(source)) {}
static bool Compare(IList<TElem> a, IList<TElem> b)
{
if (a.Count != b.Count)
return false;
// Note: the following currently boxes every element access because ValueComparer isn't really
// generic (see https://github.com/aspnet/EntityFrameworkCore/issues/11072)
for (var i = 0; i < a.Count; i++)
{
var elem1 = a[i];
var elem2 = b[i];
if (elem1 == null)
{
if (elem2 == null)
continue;
return false;
}
if (!elem1.Equals(elem2))
return false;
}
return true;
}
static IList<TElem> Snapshot(IList<TElem> source)
{
var snapshot = new TElem[source.Count];
// Note: the following currently boxes every element access because ValueComparer isn't really
// generic (see https://github.com/aspnet/EntityFrameworkCore/issues/11072)
for (var i = 0; i < source.Count; i++)
snapshot[i] = source[i];
return snapshot;
}
}
#endregion Value Comparison
}
}
......@@ -163,6 +163,39 @@ public void GenerateSqlLiteral_returns_bit_literal()
public void GenerateSqlLiteral_returns_array_literal()
=> Assert.Equal("ARRAY[3,4]", GetMapping(typeof(int[])).GenerateSqlLiteral(new[] {3, 4}));
[Fact]
public void ValueComparer_int_array()
{
// This exercises array's comparer when the element doesn't have a comparer, but it implements
// IEquatable<T>
var source = new[] { 2, 3, 4 };
var comparer = GetMapping(typeof(int[])).Comparer;
var snapshot = (int[])comparer.SnapshotFunc(source);
Assert.Equal(source, snapshot);
Assert.True(comparer.CompareFunc(source, snapshot));
snapshot[1] = 8;
Assert.False(comparer.CompareFunc(source, snapshot));
}
[Fact]
public void ValueComparer_hstore_array()
{
// This exercises array's comparer when the element has its own non-null comparer
var source = new[]
{
new Dictionary<string, string> { { "k1", "v1"} },
new Dictionary<string, string> { { "k2", "v2"} },
};
var comparer = GetMapping(typeof(Dictionary<string, string>[])).Comparer;
var snapshot = (Dictionary<string, string>[])comparer.SnapshotFunc(source);
Assert.Equal(source, snapshot);
Assert.True(comparer.CompareFunc(source, snapshot));
snapshot[1]["k2"] = "v8";
Assert.False(comparer.CompareFunc(source, snapshot));
}
[Fact]
public void GenerateSqlLiteral_returns_bytea_literal()
=> Assert.Equal(@"BYTEA E'\\xDEADBEEF'", GetMapping("bytea").GenerateSqlLiteral(new byte[] { 222, 173, 190, 239 }));
......@@ -184,7 +217,7 @@ public void ValueComparer_hstore()
{ "k1", "v1"},
{ "k2", "v2"}
};
var comparer = GetMapping("hstore").Comparer;
var snapshot = (Dictionary<string, string>)comparer.SnapshotFunc(source);
Assert.Equal(source, snapshot);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册