From c36f37be4979eb3bde58d28179aef2b6dbc172cf Mon Sep 17 00:00:00 2001 From: Elliot Quogu Prior <1709938+Quogu@users.noreply.github.com> Date: Fri, 3 Jan 2020 17:13:15 +0000 Subject: [PATCH 1/6] Allow hstore columns to be represented as IReadOnlyDictionary. The motivation for this is that most other types on an EF model class supported by Npgsql offer only one type of mutability - replacing the instance of the property. Dictionaries offer an additional type of mutability, as not only can the reference be swapped but the same reference can have its internal state changed, which makes reasoning about the correctness of code harder. Supporting IReadOnlyDictionary ensures there's only one point of mutability (or none, if EF is configured to use private field access and the public contract only offers a getter). It would be nice to offer full support for ImmutableDictionary too but from looking at the code, I think this would require further changes to Npgsql rather than just the EF Core part, which unfortunately I don't have time to look into. Still, I think this is a net win even without that. --- .../Mapping/NpgsqlHstoreTypeMapping.cs | 52 +++------- .../NpgsqlImmutableHstoreTypeMapping.cs | 68 +++++++++++++ .../Mapping/NpgsqlMutableHstoreTypeMapping.cs | 81 ++++++++++++++++ .../Internal/NpgsqlTypeMappingSource.cs | 96 ++++++++++--------- .../Storage/NpgsqlTypeMappingTest.cs | 23 ++++- 5 files changed, 233 insertions(+), 87 deletions(-) create mode 100644 src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs create mode 100644 src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs index 198546aa4..a1bbf3196 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Linq; using System.Text; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Storage; @@ -12,30 +13,15 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping /// /// See: https://www.postgresql.org/docs/current/static/hstore.html /// - public class NpgsqlHstoreTypeMapping : NpgsqlTypeMapping + public abstract class NpgsqlHstoreTypeMapping : NpgsqlTypeMapping { - static readonly HstoreComparer ComparerInstance = new HstoreComparer(); - - /// - /// Constructs an instance of the class. - /// - public NpgsqlHstoreTypeMapping() - : base( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(Dictionary), null, ComparerInstance), - "hstore" - ), NpgsqlDbType.Hstore) {} - protected NpgsqlHstoreTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters, NpgsqlDbType.Hstore) {} - protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlHstoreTypeMapping(parameters); - protected override string GenerateNonNullSqlLiteral(object value) { var sb = new StringBuilder("HSTORE '"); - foreach (var kv in (Dictionary)value) + foreach (var kv in (IReadOnlyDictionary)value) { sb.Append('"'); sb.Append(kv.Key); // TODO: Escape @@ -55,28 +41,18 @@ protected override string GenerateNonNullSqlLiteral(object value) sb.Append('\''); return sb.ToString(); } - - class HstoreComparer : ValueComparer> + protected static bool Compare(IReadOnlyDictionary a, IReadOnlyDictionary b) { - public HstoreComparer() : base( - (a, b) => Compare(a,b), - o => o.GetHashCode(), - o => o == null ? null : new Dictionary(o)) - {} - - static bool Compare(Dictionary a, Dictionary b) - { - if (a == null) - return b == null; - if (b == null) + if (a == null) + return b == null; + if (b == null) + return false; + if (a.Count != b.Count) + return false; + foreach (var kv in a) + if (!b.TryGetValue(kv.Key, out var bValue) || kv.Value != bValue) return false; - if (a.Count != b.Count) - return false; - foreach (var kv in a) - if (!b.TryGetValue(kv.Key, out var bValue) || kv.Value != bValue) - return false; - return true; - } + return true; } } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs new file mode 100644 index 000000000..129160d78 --- /dev/null +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping +{ + /// + /// The type mapping for the PostgreSQL hstore type to immutable .NET Dictionaries. + /// + /// + /// See: https://www.postgresql.org/docs/current/static/hstore.html + /// + public class NpgsqlImmutableHstoreTypeMapping : NpgsqlHstoreTypeMapping + { + static readonly HstoreComparer ComparerInstance = new HstoreComparer(); + + /// + /// Constructs an instance of the class. + /// + public NpgsqlImmutableHstoreTypeMapping() + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(IReadOnlyDictionary), null, ComparerInstance), + "hstore" + )) {} + + protected NpgsqlImmutableHstoreTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) {} + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new NpgsqlImmutableHstoreTypeMapping(parameters); + + protected override string GenerateNonNullSqlLiteral(object value) + { + var sb = new StringBuilder("HSTORE '"); + foreach (var kv in (IReadOnlyDictionary)value) + { + sb.Append('"'); + sb.Append(kv.Key); // TODO: Escape + sb.Append("\"=>"); + if (kv.Value == null) + sb.Append("NULL"); + else + { + sb.Append('"'); + sb.Append(kv.Value); // TODO: Escape + sb.Append("\","); + } + } + + sb.Remove(sb.Length - 1, 1); + + sb.Append('\''); + return sb.ToString(); + } + + class HstoreComparer : ValueComparer> + { + public HstoreComparer() : base( + (a, b) => Compare(a,b), + o => o.GetHashCode(), + o => o) + {} + } + } +} diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs new file mode 100644 index 000000000..27212951b --- /dev/null +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Text; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping +{ + /// + /// The type mapping for the PostgreSQL hstore type to immutable .NET Dictionaries. + /// + /// + /// See: https://www.postgresql.org/docs/current/static/hstore.html + /// + public class NpgsqlMutableHstoreTypeMapping : NpgsqlHstoreTypeMapping + { + static readonly HstoreComparer ComparerInstance = new HstoreComparer(); + + /// + /// Constructs an instance of the class. + /// + public NpgsqlMutableHstoreTypeMapping() + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(Dictionary), null, ComparerInstance), + "hstore" + )) {} + + protected NpgsqlMutableHstoreTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) {} + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new NpgsqlMutableHstoreTypeMapping(parameters); + + protected override string GenerateNonNullSqlLiteral(object value) + { + var sb = new StringBuilder("HSTORE '"); + foreach (var kv in (IReadOnlyDictionary)value) + { + sb.Append('"'); + sb.Append(kv.Key); // TODO: Escape + sb.Append("\"=>"); + if (kv.Value == null) + sb.Append("NULL"); + else + { + sb.Append('"'); + sb.Append(kv.Value); // TODO: Escape + sb.Append("\","); + } + } + + sb.Remove(sb.Length - 1, 1); + + sb.Append('\''); + return sb.ToString(); + } + + class HstoreComparer : ValueComparer> + { + public HstoreComparer() : base( + (a, b) => Compare(a,b), + o => o.GetHashCode(), + o => o == null ? null : new Dictionary(o)) + {} + + static bool Compare(IReadOnlyDictionary a, IReadOnlyDictionary b) + { + if (a == null) + return b == null; + if (b == null) + return false; + if (a.Count != b.Count) + return false; + foreach (var kv in a) + if (!b.TryGetValue(kv.Key, out var bValue) || kv.Value != bValue) + return false; + return true; + } + } + } +} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index 9be22bfca..47264dbc6 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -104,12 +104,13 @@ public class NpgsqlTypeMappingSource : RelationalTypeMappingSource readonly NpgsqlRangeTypeMapping _daterange; // Other types - readonly NpgsqlBoolTypeMapping _bool = new NpgsqlBoolTypeMapping(); - readonly NpgsqlBitTypeMapping _bit = new NpgsqlBitTypeMapping(); - readonly NpgsqlVarbitTypeMapping _varbit = new NpgsqlVarbitTypeMapping(); - readonly NpgsqlByteArrayTypeMapping _bytea = new NpgsqlByteArrayTypeMapping(); - readonly NpgsqlHstoreTypeMapping _hstore = new NpgsqlHstoreTypeMapping(); - readonly NpgsqlTidTypeMapping _tid = new NpgsqlTidTypeMapping(); + readonly NpgsqlBoolTypeMapping _bool = new NpgsqlBoolTypeMapping(); + readonly NpgsqlBitTypeMapping _bit = new NpgsqlBitTypeMapping(); + readonly NpgsqlVarbitTypeMapping _varbit = new NpgsqlVarbitTypeMapping(); + readonly NpgsqlByteArrayTypeMapping _bytea = new NpgsqlByteArrayTypeMapping(); + readonly NpgsqlMutableHstoreTypeMapping _mutableHstore = new NpgsqlMutableHstoreTypeMapping(); + readonly NpgsqlImmutableHstoreTypeMapping _immutableHstore = new NpgsqlImmutableHstoreTypeMapping(); + readonly NpgsqlTidTypeMapping _tid = new NpgsqlTidTypeMapping(); // Special stuff // ReSharper disable once InconsistentNaming @@ -184,7 +185,7 @@ public NpgsqlTypeMappingSource([NotNull] TypeMappingSourceDependencies dependenc { "bit", new[] { _bit } }, { "bit varying", new[] { _varbit } }, { "varbit", new[] { _varbit } }, - { "hstore", new[] { _hstore } }, + { "hstore", new RelationalTypeMapping[] { _mutableHstore, _immutableHstore } }, { "point", new[] { _point } }, { "box", new[] { _box } }, { "line", new[] { _line } }, @@ -213,46 +214,47 @@ public NpgsqlTypeMappingSource([NotNull] TypeMappingSourceDependencies dependenc var clrTypeMappings = new Dictionary { - { typeof(bool), _bool }, - { typeof(byte[]), _bytea }, - { typeof(float), _float4 }, - { typeof(double), _float8 }, - { typeof(decimal), _numeric }, - { typeof(Guid), _uuid }, - { typeof(byte), _int2Byte }, - { typeof(short), _int2 }, - { typeof(int), _int4 }, - { typeof(long), _int8 }, - { typeof(string), _text }, - { typeof(JsonDocument), _jsonbDocument }, - { typeof(JsonElement), _jsonbElement }, - { typeof(char), _singleChar }, - { typeof(DateTime), _timestamp }, - { typeof(TimeSpan), _interval }, - { typeof(DateTimeOffset), _timestamptzDto }, - { typeof(PhysicalAddress), _macaddr }, - { typeof(IPAddress), _inet }, - { typeof((IPAddress, int)), _cidr }, - { typeof(BitArray), _varbit }, - { typeof(Dictionary), _hstore }, - { typeof(NpgsqlTid), _tid }, - - { typeof(NpgsqlPoint), _point }, - { typeof(NpgsqlBox), _box }, - { typeof(NpgsqlLine), _line }, - { typeof(NpgsqlLSeg), _lseg }, - { typeof(NpgsqlPath), _path }, - { typeof(NpgsqlPolygon), _polygon }, - { typeof(NpgsqlCircle), _circle }, - - { typeof(NpgsqlRange), _int4range }, - { typeof(NpgsqlRange), _int8range }, - { typeof(NpgsqlRange), _numrange }, - { typeof(NpgsqlRange), _tsrange }, - - { typeof(NpgsqlTsQuery), _tsquery }, - { typeof(NpgsqlTsVector), _tsvector }, - { typeof(NpgsqlTsRankingNormalization), _rankingNormalization } + { typeof(bool), _bool }, + { typeof(byte[]), _bytea }, + { typeof(float), _float4 }, + { typeof(double), _float8 }, + { typeof(decimal), _numeric }, + { typeof(Guid), _uuid }, + { typeof(byte), _int2Byte }, + { typeof(short), _int2 }, + { typeof(int), _int4 }, + { typeof(long), _int8 }, + { typeof(string), _text }, + { typeof(JsonDocument), _jsonbDocument }, + { typeof(JsonElement), _jsonbElement }, + { typeof(char), _singleChar }, + { typeof(DateTime), _timestamp }, + { typeof(TimeSpan), _interval }, + { typeof(DateTimeOffset), _timestamptzDto }, + { typeof(PhysicalAddress), _macaddr }, + { typeof(IPAddress), _inet }, + { typeof((IPAddress, int)), _cidr }, + { typeof(BitArray), _varbit }, + { typeof(IReadOnlyDictionary), _immutableHstore }, + { typeof(Dictionary), _mutableHstore }, + { typeof(NpgsqlTid), _tid }, + + { typeof(NpgsqlPoint), _point }, + { typeof(NpgsqlBox), _box }, + { typeof(NpgsqlLine), _line }, + { typeof(NpgsqlLSeg), _lseg }, + { typeof(NpgsqlPath), _path }, + { typeof(NpgsqlPolygon), _polygon }, + { typeof(NpgsqlCircle), _circle }, + + { typeof(NpgsqlRange), _int4range }, + { typeof(NpgsqlRange), _int8range }, + { typeof(NpgsqlRange), _numrange }, + { typeof(NpgsqlRange), _tsrange }, + + { typeof(NpgsqlTsQuery), _tsquery }, + { typeof(NpgsqlTsVector), _tsvector }, + { typeof(NpgsqlTsRankingNormalization), _rankingNormalization } }; StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase); diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs index bced06471..7b0f1bdfe 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Net; @@ -308,7 +308,7 @@ public void GenerateSqlLiteral_returns_hstore_literal() })); [Fact] - public void ValueComparer_hstore() + public void ValueComparer_hstore_as_dictionary() { var source = new Dictionary { @@ -325,6 +325,25 @@ public void ValueComparer_hstore() Assert.False(comparer.Equals(source, snapshot)); } + [Fact] + public void ValueComparer_hstore_as_readonlydictionary() + { + var sourceAsDict = new Dictionary + { + { "k1", "v1" }, + { "k2", "v2" } + }; + IReadOnlyDictionary source = sourceAsDict; + + var comparer = GetMapping("hstore").Comparer; + var snapshot = (IReadOnlyDictionary)comparer.Snapshot(source); + Assert.Equal(source, snapshot); + Assert.NotSame(source, snapshot); + Assert.True(comparer.Equals(source, snapshot)); + sourceAsDict.Remove("k1"); + Assert.False(comparer.Equals(source, snapshot)); + } + [Fact] public void GenerateSqlLiteral_returns_enum_literal() { From 93ea3d11bfe099daa58d74bde14df8565d70b790 Mon Sep 17 00:00:00 2001 From: Elliot Quogu Prior <1709938+Quogu@users.noreply.github.com> Date: Fri, 3 Jan 2020 17:16:10 +0000 Subject: [PATCH 2/6] Remove duplicate code that now lives in the base class. --- .../NpgsqlImmutableHstoreTypeMapping.cs | 24 ------------ .../Mapping/NpgsqlMutableHstoreTypeMapping.cs | 39 ------------------- 2 files changed, 63 deletions(-) diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs index 129160d78..0f13ffd50 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs @@ -32,30 +32,6 @@ protected NpgsqlImmutableHstoreTypeMapping(RelationalTypeMappingParameters param protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => new NpgsqlImmutableHstoreTypeMapping(parameters); - protected override string GenerateNonNullSqlLiteral(object value) - { - var sb = new StringBuilder("HSTORE '"); - foreach (var kv in (IReadOnlyDictionary)value) - { - sb.Append('"'); - sb.Append(kv.Key); // TODO: Escape - sb.Append("\"=>"); - if (kv.Value == null) - sb.Append("NULL"); - else - { - sb.Append('"'); - sb.Append(kv.Value); // TODO: Escape - sb.Append("\","); - } - } - - sb.Remove(sb.Length - 1, 1); - - sb.Append('\''); - return sb.ToString(); - } - class HstoreComparer : ValueComparer> { public HstoreComparer() : base( diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs index 27212951b..34d5c0952 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Text; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Storage; @@ -31,30 +30,6 @@ protected NpgsqlMutableHstoreTypeMapping(RelationalTypeMappingParameters paramet protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => new NpgsqlMutableHstoreTypeMapping(parameters); - protected override string GenerateNonNullSqlLiteral(object value) - { - var sb = new StringBuilder("HSTORE '"); - foreach (var kv in (IReadOnlyDictionary)value) - { - sb.Append('"'); - sb.Append(kv.Key); // TODO: Escape - sb.Append("\"=>"); - if (kv.Value == null) - sb.Append("NULL"); - else - { - sb.Append('"'); - sb.Append(kv.Value); // TODO: Escape - sb.Append("\","); - } - } - - sb.Remove(sb.Length - 1, 1); - - sb.Append('\''); - return sb.ToString(); - } - class HstoreComparer : ValueComparer> { public HstoreComparer() : base( @@ -62,20 +37,6 @@ public HstoreComparer() : base( o => o.GetHashCode(), o => o == null ? null : new Dictionary(o)) {} - - static bool Compare(IReadOnlyDictionary a, IReadOnlyDictionary b) - { - if (a == null) - return b == null; - if (b == null) - return false; - if (a.Count != b.Count) - return false; - foreach (var kv in a) - if (!b.TryGetValue(kv.Key, out var bValue) || kv.Value != bValue) - return false; - return true; - } } } } From 5475e700618fba2b5912ec9c33d33159debffba3 Mon Sep 17 00:00:00 2001 From: Elliot Quogu Prior <1709938+Quogu@users.noreply.github.com> Date: Wed, 8 Jan 2020 11:10:53 +0000 Subject: [PATCH 3/6] PR feedback. - Rename Immutable to ReadOnly to better represent what the code is actually doing. - Change comparison type for the ReadOnly hstore comparer to just check for references, with a comment explaining the reasoning for this. --- .../Mapping/NpgsqlHstoreTypeMapping.cs | 13 ------------ .../Mapping/NpgsqlMutableHstoreTypeMapping.cs | 14 +++++++++++++ ....cs => NpgsqlReadOnlyHstoreTypeMapping.cs} | 14 +++++++------ .../Internal/NpgsqlTypeMappingSource.cs | 20 +++++++++---------- .../Storage/NpgsqlTypeMappingTest.cs | 4 ++-- 5 files changed, 34 insertions(+), 31 deletions(-) rename src/EFCore.PG/Storage/Internal/Mapping/{NpgsqlImmutableHstoreTypeMapping.cs => NpgsqlReadOnlyHstoreTypeMapping.cs} (64%) diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs index a1bbf3196..7eef3b4b9 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs @@ -41,18 +41,5 @@ protected override string GenerateNonNullSqlLiteral(object value) sb.Append('\''); return sb.ToString(); } - protected static bool Compare(IReadOnlyDictionary a, IReadOnlyDictionary b) - { - if (a == null) - return b == null; - if (b == null) - return false; - if (a.Count != b.Count) - return false; - foreach (var kv in a) - if (!b.TryGetValue(kv.Key, out var bValue) || kv.Value != bValue) - return false; - return true; - } } } diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs index 34d5c0952..a3b8821d1 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs @@ -30,6 +30,20 @@ protected NpgsqlMutableHstoreTypeMapping(RelationalTypeMappingParameters paramet protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => new NpgsqlMutableHstoreTypeMapping(parameters); + private static bool Compare(IReadOnlyDictionary a, IReadOnlyDictionary b) + { + if (a == null) + return b == null; + if (b == null) + return false; + if (a.Count != b.Count) + return false; + foreach (var kv in a) + if (!b.TryGetValue(kv.Key, out var bValue) || kv.Value != bValue) + return false; + return true; + } + class HstoreComparer : ValueComparer> { public HstoreComparer() : base( diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlReadOnlyHstoreTypeMapping.cs similarity index 64% rename from src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs rename to src/EFCore.PG/Storage/Internal/Mapping/NpgsqlReadOnlyHstoreTypeMapping.cs index 0f13ffd50..31263648a 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlReadOnlyHstoreTypeMapping.cs @@ -12,30 +12,32 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping /// /// See: https://www.postgresql.org/docs/current/static/hstore.html /// - public class NpgsqlImmutableHstoreTypeMapping : NpgsqlHstoreTypeMapping + public class NpgsqlReadOnlyHstoreTypeMapping : NpgsqlHstoreTypeMapping { static readonly HstoreComparer ComparerInstance = new HstoreComparer(); /// - /// Constructs an instance of the class. + /// Constructs an instance of the class. /// - public NpgsqlImmutableHstoreTypeMapping() + public NpgsqlReadOnlyHstoreTypeMapping() : base( new RelationalTypeMappingParameters( new CoreTypeMappingParameters(typeof(IReadOnlyDictionary), null, ComparerInstance), "hstore" )) {} - protected NpgsqlImmutableHstoreTypeMapping(RelationalTypeMappingParameters parameters) + protected NpgsqlReadOnlyHstoreTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters) {} protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new NpgsqlImmutableHstoreTypeMapping(parameters); + => new NpgsqlReadOnlyHstoreTypeMapping(parameters); class HstoreComparer : ValueComparer> { public HstoreComparer() : base( - (a, b) => Compare(a,b), + // We could compare contents here if the references are different, but that would penalize the 99% case + // where a different reference means different contents, which would only save a very rare database update. + (a, b) => ReferenceEquals(a, b), o => o.GetHashCode(), o => o) {} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index 47264dbc6..273165f4b 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -104,13 +104,13 @@ public class NpgsqlTypeMappingSource : RelationalTypeMappingSource readonly NpgsqlRangeTypeMapping _daterange; // Other types - readonly NpgsqlBoolTypeMapping _bool = new NpgsqlBoolTypeMapping(); - readonly NpgsqlBitTypeMapping _bit = new NpgsqlBitTypeMapping(); - readonly NpgsqlVarbitTypeMapping _varbit = new NpgsqlVarbitTypeMapping(); - readonly NpgsqlByteArrayTypeMapping _bytea = new NpgsqlByteArrayTypeMapping(); - readonly NpgsqlMutableHstoreTypeMapping _mutableHstore = new NpgsqlMutableHstoreTypeMapping(); - readonly NpgsqlImmutableHstoreTypeMapping _immutableHstore = new NpgsqlImmutableHstoreTypeMapping(); - readonly NpgsqlTidTypeMapping _tid = new NpgsqlTidTypeMapping(); + readonly NpgsqlBoolTypeMapping _bool = new NpgsqlBoolTypeMapping(); + readonly NpgsqlBitTypeMapping _bit = new NpgsqlBitTypeMapping(); + readonly NpgsqlVarbitTypeMapping _varbit = new NpgsqlVarbitTypeMapping(); + readonly NpgsqlByteArrayTypeMapping _bytea = new NpgsqlByteArrayTypeMapping(); + readonly NpgsqlMutableHstoreTypeMapping _hstore = new NpgsqlMutableHstoreTypeMapping(); + readonly NpgsqlReadOnlyHstoreTypeMapping _readOnlyHstore = new NpgsqlReadOnlyHstoreTypeMapping(); + readonly NpgsqlTidTypeMapping _tid = new NpgsqlTidTypeMapping(); // Special stuff // ReSharper disable once InconsistentNaming @@ -185,7 +185,7 @@ public NpgsqlTypeMappingSource([NotNull] TypeMappingSourceDependencies dependenc { "bit", new[] { _bit } }, { "bit varying", new[] { _varbit } }, { "varbit", new[] { _varbit } }, - { "hstore", new RelationalTypeMapping[] { _mutableHstore, _immutableHstore } }, + { "hstore", new RelationalTypeMapping[] { _hstore, _readOnlyHstore } }, { "point", new[] { _point } }, { "box", new[] { _box } }, { "line", new[] { _line } }, @@ -235,8 +235,8 @@ public NpgsqlTypeMappingSource([NotNull] TypeMappingSourceDependencies dependenc { typeof(IPAddress), _inet }, { typeof((IPAddress, int)), _cidr }, { typeof(BitArray), _varbit }, - { typeof(IReadOnlyDictionary), _immutableHstore }, - { typeof(Dictionary), _mutableHstore }, + { typeof(IReadOnlyDictionary), _readOnlyHstore }, + { typeof(Dictionary), _hstore }, { typeof(NpgsqlTid), _tid }, { typeof(NpgsqlPoint), _point }, diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs index 7b0f1bdfe..86b64b7b5 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs @@ -308,7 +308,7 @@ public void GenerateSqlLiteral_returns_hstore_literal() })); [Fact] - public void ValueComparer_hstore_as_dictionary() + public void ValueComparer_hstore_as_Dictionary() { var source = new Dictionary { @@ -326,7 +326,7 @@ public void ValueComparer_hstore_as_dictionary() } [Fact] - public void ValueComparer_hstore_as_readonlydictionary() + public void ValueComparer_hstore_as_IReadOnlyDictionary() { var sourceAsDict = new Dictionary { From 19e08b09d317ef7d1a5242262d59077767423d05 Mon Sep 17 00:00:00 2001 From: Elliot Quogu Prior <1709938+Quogu@users.noreply.github.com> Date: Wed, 8 Jan 2020 14:41:08 +0000 Subject: [PATCH 4/6] Add support for ImmutableDictionary now that npgsql/npgsql#2776 has added support for them to the base library. --- .../NpgsqlImmutableHstoreTypeMapping.cs | 44 +++++++++ .../Internal/NpgsqlTypeMappingSource.cs | 19 ++-- .../BuiltInDataTypesNpgsqlTest.cs | 98 ++++++++++++------- .../Storage/NpgsqlTypeMappingTest.cs | 16 +++ 4 files changed, 132 insertions(+), 45 deletions(-) create mode 100644 src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs new file mode 100644 index 000000000..ec9378864 --- /dev/null +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs @@ -0,0 +1,44 @@ +using System.Collections.Immutable; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping +{ + /// + /// The type mapping for the PostgreSQL hstore type to immutable .NET Dictionaries. + /// + /// + /// See: https://www.postgresql.org/docs/current/static/hstore.html + /// + public class NpgsqlImmutableHstoreTypeMapping : NpgsqlHstoreTypeMapping + { + static readonly HstoreComparer ComparerInstance = new HstoreComparer(); + + /// + /// Constructs an instance of the class. + /// + public NpgsqlImmutableHstoreTypeMapping() + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(ImmutableDictionary), null, ComparerInstance), + "hstore" + )) {} + + protected NpgsqlImmutableHstoreTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) {} + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new NpgsqlImmutableHstoreTypeMapping(parameters); + + class HstoreComparer : ValueComparer> + { + public HstoreComparer() : base( + // We could compare contents here if the references are different, but that would penalize the 99% case + // where a different reference means different contents, which would only save a very rare database update. + (a, b) => ReferenceEquals(a, b), + o => o.GetHashCode(), + o => o) + {} + } + } +} diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index 273165f4b..8df4de5e4 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Data; using System.Diagnostics; using System.Linq; @@ -104,13 +105,14 @@ public class NpgsqlTypeMappingSource : RelationalTypeMappingSource readonly NpgsqlRangeTypeMapping _daterange; // Other types - readonly NpgsqlBoolTypeMapping _bool = new NpgsqlBoolTypeMapping(); - readonly NpgsqlBitTypeMapping _bit = new NpgsqlBitTypeMapping(); - readonly NpgsqlVarbitTypeMapping _varbit = new NpgsqlVarbitTypeMapping(); - readonly NpgsqlByteArrayTypeMapping _bytea = new NpgsqlByteArrayTypeMapping(); - readonly NpgsqlMutableHstoreTypeMapping _hstore = new NpgsqlMutableHstoreTypeMapping(); - readonly NpgsqlReadOnlyHstoreTypeMapping _readOnlyHstore = new NpgsqlReadOnlyHstoreTypeMapping(); - readonly NpgsqlTidTypeMapping _tid = new NpgsqlTidTypeMapping(); + readonly NpgsqlBoolTypeMapping _bool = new NpgsqlBoolTypeMapping(); + readonly NpgsqlBitTypeMapping _bit = new NpgsqlBitTypeMapping(); + readonly NpgsqlVarbitTypeMapping _varbit = new NpgsqlVarbitTypeMapping(); + readonly NpgsqlByteArrayTypeMapping _bytea = new NpgsqlByteArrayTypeMapping(); + readonly NpgsqlMutableHstoreTypeMapping _hstore = new NpgsqlMutableHstoreTypeMapping(); + readonly NpgsqlReadOnlyHstoreTypeMapping _readOnlyHstore = new NpgsqlReadOnlyHstoreTypeMapping(); + readonly NpgsqlImmutableHstoreTypeMapping _immutableHstore = new NpgsqlImmutableHstoreTypeMapping(); + readonly NpgsqlTidTypeMapping _tid = new NpgsqlTidTypeMapping(); // Special stuff // ReSharper disable once InconsistentNaming @@ -185,7 +187,7 @@ public NpgsqlTypeMappingSource([NotNull] TypeMappingSourceDependencies dependenc { "bit", new[] { _bit } }, { "bit varying", new[] { _varbit } }, { "varbit", new[] { _varbit } }, - { "hstore", new RelationalTypeMapping[] { _hstore, _readOnlyHstore } }, + { "hstore", new RelationalTypeMapping[] { _hstore, _readOnlyHstore, _immutableHstore } }, { "point", new[] { _point } }, { "box", new[] { _box } }, { "line", new[] { _line } }, @@ -236,6 +238,7 @@ public NpgsqlTypeMappingSource([NotNull] TypeMappingSourceDependencies dependenc { typeof((IPAddress, int)), _cidr }, { typeof(BitArray), _varbit }, { typeof(IReadOnlyDictionary), _readOnlyHstore }, + { typeof(ImmutableDictionary), _immutableHstore }, { typeof(Dictionary), _hstore }, { typeof(NpgsqlTid), _tid }, diff --git a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs index 345e16bf9..de40b6ce8 100644 --- a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Net.NetworkInformation; @@ -115,6 +116,8 @@ public virtual void Can_query_using_any_mapped_data_type() StringAsJsonb = @"{""a"": ""b""}", StringAsJson = @"{""a"": ""b""}", DictionaryAsHstore = new Dictionary { { "a", "b" } }, + IReadOnlyDictionaryAsHstore = new Dictionary { { "c", "d" } }, + ImmutableDictionaryAsHstore = ImmutableDictionary.Empty.Add("e", "f"), NpgsqlRangeAsRange = new NpgsqlRange(4, true, 8, false), IntArrayAsIntArray= new[] { 2, 3 }, @@ -248,36 +251,42 @@ public virtual void Can_query_using_any_mapped_data_type() var param30 = new Dictionary { { "a", "b" } }; Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.DictionaryAsHstore == param30)); - var param31 = new NpgsqlRange(4, true, 8, false); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.NpgsqlRangeAsRange == param31)); + var param31 = (IReadOnlyDictionary)new Dictionary { { "c", "d" } }; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.IReadOnlyDictionaryAsHstore == param31)); - var param32 = new[] { 2, 3 }; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.IntArrayAsIntArray == param32)); + var param32 = ImmutableDictionary.Empty.Add("e", "f"); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.ImmutableDictionaryAsHstore == param32)); - var param33 = new[] { PhysicalAddress.Parse("08-00-2B-01-02-03"), PhysicalAddress.Parse("08-00-2B-01-02-04") }; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.PhysicalAddressArrayAsMacaddrArray == param33)); + var param33 = new NpgsqlRange(4, true, 8, false); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.NpgsqlRangeAsRange == param33)); + + var param34 = new[] { 2, 3 }; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.IntArrayAsIntArray == param34)); + + var param35 = new[] { PhysicalAddress.Parse("08-00-2B-01-02-03"), PhysicalAddress.Parse("08-00-2B-01-02-04") }; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.PhysicalAddressArrayAsMacaddrArray == param35)); // ReSharper disable once ConvertToConstant.Local - var param34 = (uint)int.MaxValue + 1; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UintAsXid == param34)); + var param36 = (uint)int.MaxValue + 1; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.UintAsXid == param36)); - var param35 = NpgsqlTsQuery.Parse("a & b"); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.SearchQuery == param35)); + var param37 = NpgsqlTsQuery.Parse("a & b"); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.SearchQuery == param37)); - var param36 = NpgsqlTsVector.Parse("a b"); - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.SearchVector == param36)); + var param38 = NpgsqlTsVector.Parse("a b"); + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.SearchVector == param38)); // ReSharper disable once ConvertToConstant.Local - var param37 = NpgsqlTsRankingNormalization.DivideByLength; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.RankingNormalization == param37)); + var param39 = NpgsqlTsRankingNormalization.DivideByLength; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.RankingNormalization == param39)); // ReSharper disable once ConvertToConstant.Local - var param38 = 12724u; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Regconfig == param38)); + var param40 = 12724u; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Regconfig == param40)); // ReSharper disable once ConvertToConstant.Local - var param39 = Mood.Sad; - Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Mood == param39)); + var param41 = Mood.Sad; + Assert.Same(entity, context.Set().Single(e => e.Int == 999 && e.Mood == param41)); } } @@ -408,32 +417,38 @@ public virtual void Can_query_using_any_mapped_data_types_with_nulls() Dictionary param30 = null; Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.DictionaryAsHstore == param30)); - NpgsqlRange? param31 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.NpgsqlRangeAsRange == param31)); + IReadOnlyDictionary param31 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.IReadOnlyDictionaryAsHstore == param31)); + + ImmutableDictionary param32 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.ImmutableDictionaryAsHstore == param32)); + + NpgsqlRange? param33 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.NpgsqlRangeAsRange == param33)); - int[] param32 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.IntArrayAsIntArray == param32)); + int[] param34 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.IntArrayAsIntArray == param34)); - PhysicalAddress[] param33 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.PhysicalAddressArrayAsMacaddrArray== param33)); + PhysicalAddress[] param35 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.PhysicalAddressArrayAsMacaddrArray== param35)); - uint? param34 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UintAsXid == param34)); + uint? param36 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.UintAsXid == param36)); - NpgsqlTsQuery param35 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.SearchQuery == param35)); + NpgsqlTsQuery param37 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.SearchQuery == param37)); - NpgsqlTsVector param36 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.SearchVector == param36)); + NpgsqlTsVector param38 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.SearchVector == param38)); - NpgsqlTsRankingNormalization? param37 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.RankingNormalization == param37)); + NpgsqlTsRankingNormalization? param39 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.RankingNormalization == param39)); - uint? param38 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.Regconfig == param38)); + uint? param40 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.Regconfig == param40)); - Mood? param39 = null; - Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.Mood == param39)); + Mood? param41 = null; + Assert.Same(entity, context.Set().Single(e => e.Int == 911 && e.Mood == param41)); } } @@ -682,6 +697,8 @@ static void AssertNullMappedNullableDataTypes(MappedNullableDataTypes entity, in Assert.Null(entity.StringAsJsonb); Assert.Null(entity.StringAsJson); Assert.Null(entity.DictionaryAsHstore); + Assert.Null(entity.IReadOnlyDictionaryAsHstore); + Assert.Null(entity.ImmutableDictionaryAsHstore); Assert.Null(entity.NpgsqlRangeAsRange); Assert.Null(entity.IntArrayAsIntArray); @@ -1310,6 +1327,13 @@ protected class MappedNullableDataTypes [Column(TypeName = "hstore")] public Dictionary DictionaryAsHstore { get; set; } + [Column(TypeName = "hstore")] + // ReSharper disable once InconsistentNaming + public IReadOnlyDictionary IReadOnlyDictionaryAsHstore { get; set; } + + [Column(TypeName = "hstore")] + public ImmutableDictionary ImmutableDictionaryAsHstore { get; set; } + [Column(TypeName = "int4range")] public NpgsqlRange? NpgsqlRangeAsRange { get; set; } diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs index 86b64b7b5..704f239b0 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Net; using System.Net.NetworkInformation; using System.Text.Json; @@ -344,6 +345,21 @@ public void ValueComparer_hstore_as_IReadOnlyDictionary() Assert.False(comparer.Equals(source, snapshot)); } + [Fact] + public void ValueComparer_hstore_as_ImmutableDictionary() + { + var source = ImmutableDictionary.Empty + .Add("k1", "v1") + .Add("k2", "v2"); + + var comparer = Mapper.FindMapping(typeof(ImmutableDictionary), "hstore").Comparer; + var snapshot = comparer.Snapshot(source); + Assert.Equal(source, snapshot); + Assert.True(comparer.Equals(source, snapshot)); + source = source.Remove("k1"); + Assert.False(comparer.Equals(source, snapshot)); + } + [Fact] public void GenerateSqlLiteral_returns_enum_literal() { From 4a8750095b636bf7730febf35a6e5e5cffa0e767 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 8 Sep 2020 15:40:32 +0300 Subject: [PATCH 5/6] Update src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs --- .../Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs index ec9378864..5a328cec7 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlImmutableHstoreTypeMapping.cs @@ -5,7 +5,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping { /// - /// The type mapping for the PostgreSQL hstore type to immutable .NET Dictionaries. + /// The type mapping for the PostgreSQL hstore type to . /// /// /// See: https://www.postgresql.org/docs/current/static/hstore.html From 7af807cc910c7899ca66613c72169439c126dc82 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 8 Sep 2020 15:41:13 +0300 Subject: [PATCH 6/6] Update src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs --- .../Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs index a3b8821d1..026fa7b08 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlMutableHstoreTypeMapping.cs @@ -5,7 +5,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping { /// - /// The type mapping for the PostgreSQL hstore type to immutable .NET Dictionaries. + /// The type mapping for the PostgreSQL hstore type to . /// /// /// See: https://www.postgresql.org/docs/current/static/hstore.html