diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlHstoreTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlHstoreTranslator.cs
new file mode 100644
index 000000000..65379e459
--- /dev/null
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlHstoreTranslator.cs
@@ -0,0 +1,137 @@
+using System.Collections.Immutable;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
+using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public class NpgsqlHstoreTranslator : IMethodCallTranslator, IMemberTranslator
+{
+ private static readonly Type DictionaryType = typeof(Dictionary);
+ private static readonly Type ImmutableDictionaryType = typeof(ImmutableDictionary);
+
+ private static readonly MethodInfo Dictionary_ContainsKey =
+ DictionaryType.GetMethod(nameof(Dictionary.ContainsKey))!;
+
+ private static readonly MethodInfo ImmutableDictionary_ContainsKey =
+ ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary.ContainsKey))!;
+
+ private static readonly MethodInfo Dictionary_ContainsValue =
+ DictionaryType.GetMethod(nameof(Dictionary.ContainsValue))!;
+
+ private static readonly MethodInfo ImmutableDictionary_ContainsValue =
+ ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary.ContainsValue))!;
+
+ private static readonly MethodInfo Dictionary_Item_Getter =
+ DictionaryType.GetProperty("Item")!.GetMethod!;
+
+ private static readonly MethodInfo ImmutableDictionary_Item_Getter =
+ ImmutableDictionaryType.GetProperty("Item")!.GetMethod!;
+
+ private static readonly PropertyInfo Dictionary_Count = DictionaryType.GetProperty(nameof(Dictionary.Count))!;
+
+ private static readonly PropertyInfo ImmutableDictionary_Count =
+ ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary.Count))!;
+
+ private static readonly PropertyInfo ImmutableDictionary_IsEmpty =
+ ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary.IsEmpty))!;
+
+ private readonly RelationalTypeMapping _stringListTypeMapping;
+ private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public NpgsqlHstoreTranslator(IRelationalTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory)
+ {
+ _sqlExpressionFactory = sqlExpressionFactory;
+ _stringListTypeMapping = typeMappingSource.FindMapping(typeof(List))!;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqlExpression? Translate(
+ SqlExpression? instance,
+ MethodInfo method,
+ IReadOnlyList arguments,
+ IDiagnosticsLogger logger)
+ {
+ if (instance?.TypeMapping is null || instance.TypeMapping.StoreType != NpgsqlHstoreTypeMapping.HstoreType)
+ {
+ return null;
+ }
+
+ if (method == Dictionary_ContainsKey || method == ImmutableDictionary_ContainsKey)
+ {
+ return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreContainsKey, instance, arguments[0]);
+ }
+
+ if (method == Dictionary_ContainsValue || method == ImmutableDictionary_ContainsValue)
+ {
+ return _sqlExpressionFactory.Equal(
+ arguments[0],
+ _sqlExpressionFactory.Function(
+ "ANY", new[]
+ {
+ _sqlExpressionFactory.Function(
+ "avals", new[] { instance }, false, FalseArrays[1], typeof(List), _stringListTypeMapping)
+ }, false, FalseArrays[1], typeof(string)));
+ }
+
+ if (method == Dictionary_Item_Getter || method == ImmutableDictionary_Item_Getter)
+ {
+ return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreValueForKey, instance, arguments[0]);
+ }
+ return null;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public SqlExpression? Translate(
+ SqlExpression? instance,
+ MemberInfo member,
+ Type returnType,
+ IDiagnosticsLogger logger)
+ {
+
+ if (instance?.TypeMapping is null || instance.TypeMapping.StoreType != NpgsqlHstoreTypeMapping.HstoreType)
+ {
+ return null;
+ }
+
+ if (member == Dictionary_Count || member == ImmutableDictionary_Count)
+ {
+ return _sqlExpressionFactory.Function("array_length", new []
+ {
+ _sqlExpressionFactory.Function(
+ "akeys", new[] { instance }, false, FalseArrays[1], typeof(List), _stringListTypeMapping),
+ _sqlExpressionFactory.Constant(1)
+ }, false, FalseArrays[2], typeof(int));
+ }
+
+ if (member == ImmutableDictionary_IsEmpty)
+ {
+ return _sqlExpressionFactory.Equal(
+ Translate(instance, Dictionary_Count, typeof(int), logger)!,
+ _sqlExpressionFactory.Constant(0));
+ }
+ return null;
+ }
+}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
index 28ab9785a..62859b1c4 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs
@@ -43,7 +43,8 @@ public NpgsqlMemberTranslatorProvider(
JsonPocoTranslator,
new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges),
new NpgsqlStringMemberTranslator(sqlExpressionFactory),
- new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory)
+ new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory),
+ new NpgsqlHstoreTranslator(typeMappingSource, sqlExpressionFactory)
]);
}
}
diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
index 63843eab3..785f67a57 100644
--- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
+++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs
@@ -61,7 +61,8 @@ public NpgsqlMethodCallTranslatorProvider(
new NpgsqlRegexIsMatchTranslator(sqlExpressionFactory),
new NpgsqlRowValueTranslator(sqlExpressionFactory),
new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory),
- new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model)
+ new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
+ new NpgsqlHstoreTranslator(typeMappingSource, sqlExpressionFactory)
]);
}
}
diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs
index 03387b988..040bee0fd 100644
--- a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs
+++ b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs
@@ -151,6 +151,9 @@ protected override void Print(ExpressionPrinter expressionPrinter)
PgExpressionType.Distance => "<->",
+ PgExpressionType.HStoreContainsKey => "?",
+ PgExpressionType.HStoreValueForKey => "->",
+
_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}")
})
.Append(" ");
diff --git a/src/EFCore.PG/Query/Expressions/PgExpressionType.cs b/src/EFCore.PG/Query/Expressions/PgExpressionType.cs
index 270a67e01..987a9a2e7 100644
--- a/src/EFCore.PG/Query/Expressions/PgExpressionType.cs
+++ b/src/EFCore.PG/Query/Expressions/PgExpressionType.cs
@@ -159,4 +159,18 @@ public enum PgExpressionType
LTreeFirstMatches, // ?~ or ?@
#endregion LTree
+
+ #region HStore
+
+ ///
+ /// Represents a PostgreSQL operator for checking if a hstore contains the given key
+ ///
+ HStoreContainsKey,
+
+ ///
+ /// Represents a PostgreSQL operator for accessing a hstore value for a given key
+ ///
+ HStoreValueForKey,
+
+ #endregion HStore
}
diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
index 3418d5045..51146b8d2 100644
--- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
+++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
@@ -527,6 +527,9 @@ when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTyp
PgExpressionType.Distance => "<->",
+ PgExpressionType.HStoreValueForKey => "->",
+ PgExpressionType.HStoreContainsKey => "?",
+
_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}")
})
.Append(" ");
diff --git a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
index 84fac6794..0cc8c353c 100644
--- a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
+++ b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
@@ -16,6 +16,7 @@ public class NpgsqlSqlExpressionFactory : SqlExpressionFactory
{
private readonly NpgsqlTypeMappingSource _typeMappingSource;
private readonly RelationalTypeMapping _boolTypeMapping;
+ private readonly RelationalTypeMapping _stringTypeMapping;
private static Type? _nodaTimeDurationType;
private static Type? _nodaTimePeriodType;
@@ -29,6 +30,7 @@ public NpgsqlSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)
{
_typeMappingSource = (NpgsqlTypeMappingSource)dependencies.TypeMappingSource;
_boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool), dependencies.Model)!;
+ _stringTypeMapping = _typeMappingSource.FindMapping(typeof(string), dependencies.Model)!;
}
#region Expression factory methods
@@ -307,12 +309,17 @@ public virtual SqlExpression MakePostgresBinary(
case PgExpressionType.JsonExists:
case PgExpressionType.JsonExistsAny:
case PgExpressionType.JsonExistsAll:
+ case PgExpressionType.HStoreContainsKey:
returnType = typeof(bool);
break;
case PgExpressionType.Distance:
returnType = typeof(double);
break;
+
+ case PgExpressionType.HStoreValueForKey:
+ returnType = typeof(string);
+ break;
}
return (PgBinaryExpression)ApplyTypeMapping(
@@ -773,6 +780,7 @@ private SqlExpression ApplyTypeMappingOnPostgresBinary(
case PgExpressionType.JsonExists:
case PgExpressionType.JsonExistsAny:
case PgExpressionType.JsonExistsAll:
+ case PgExpressionType.HStoreContainsKey:
{
// TODO: For networking, this probably needs to be cleaned up, i.e. we know where the CIDR and INET are
// based on operator type?
@@ -823,6 +831,16 @@ when left.Type.FullName is "NodaTime.Instant" or "NodaTime.LocalDateTime" or "No
break;
}
+ case PgExpressionType.HStoreValueForKey:
+ {
+ return new PgBinaryExpression(
+ operatorType,
+ ApplyDefaultTypeMapping(left),
+ ApplyDefaultTypeMapping(right),
+ typeof(string),
+ _stringTypeMapping);
+ }
+
default:
throw new InvalidOperationException(
$"Incorrect {nameof(operatorType)} for {nameof(pgBinaryExpression)}: {operatorType}");
diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs
index 3d6eef2f0..b6bd178e6 100644
--- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs
+++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs
@@ -14,6 +14,11 @@ public class NpgsqlHstoreTypeMapping : NpgsqlTypeMapping
{
private static readonly HstoreMutableComparer MutableComparerInstance = new();
+ ///
+ /// The database store type of the Hstore type
+ ///
+ public const string HstoreType = "hstore";
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -32,7 +37,7 @@ public NpgsqlHstoreTypeMapping(Type clrType)
: base(
new RelationalTypeMappingParameters(
new CoreTypeMappingParameters(clrType, comparer: GetComparer(clrType)),
- "hstore"),
+ HstoreType),
NpgsqlDbType.Hstore)
{
}
diff --git a/test/EFCore.PG.FunctionalTests/Query/HstoreQueryFixture.cs b/test/EFCore.PG.FunctionalTests/Query/HstoreQueryFixture.cs
new file mode 100644
index 000000000..af0fbd194
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/Query/HstoreQueryFixture.cs
@@ -0,0 +1,73 @@
+using Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Dictionary;
+using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;
+
+public class HstoreQueryFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory
+{
+ protected override string StoreName
+ => "HstoreQueryTest";
+
+ protected override ITestStoreFactory TestStoreFactory
+ => NpgsqlTestStoreFactory.Instance;
+
+ public TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+
+ private DictionaryQueryData _expectedData;
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(wcb => wcb.Ignore(CoreEventId.CollectionWithoutComparer));
+
+ protected override Task SeedAsync(DictionaryQueryContext context)
+ => DictionaryQueryContext.SeedAsync(context);
+
+ public Func GetContextCreator()
+ => CreateContext;
+
+ public ISetSource GetExpectedData()
+ => _expectedData ??= new DictionaryQueryData();
+
+ public IReadOnlyDictionary EntitySorters
+ => new Dictionary>
+ {
+ { typeof(DictionaryEntity), e => ((DictionaryEntity)e)?.Id }, { typeof(DictionaryContainerEntity), e => ((DictionaryContainerEntity)e)?.Id }
+ }.ToDictionary(e => e.Key, e => (object)e.Value);
+
+ public IReadOnlyDictionary EntityAsserters
+ => new Dictionary>
+ {
+ {
+ typeof(DictionaryEntity), (e, a) =>
+ {
+ Assert.Equal(e is null, a is null);
+ if (a is not null)
+ {
+ var ee = (DictionaryEntity)e;
+ var aa = (DictionaryEntity)a;
+
+ Assert.Equal(ee.Id, aa.Id);
+ Assert.Equal(ee.Dictionary, ee.Dictionary);
+ Assert.Equal(ee.ImmutableDictionary, ee.ImmutableDictionary);
+ Assert.Equal(ee.NullableDictionary, ee.NullableDictionary);
+ Assert.Equal(ee.NullableImmutableDictionary, ee.NullableImmutableDictionary);
+
+ }
+ }
+ },
+ {
+ typeof(DictionaryContainerEntity), (e, a) =>
+ {
+ Assert.Equal(e is null, a is null);
+ if (a is not null)
+ {
+ var ee = (DictionaryContainerEntity)e;
+ var aa = (DictionaryContainerEntity)a;
+
+ Assert.Equal(ee.Id, aa.Id);
+ Assert.Equal(ee.DictionaryEntities, ee.DictionaryEntities);
+ }
+ }
+ }
+ }.ToDictionary(e => e.Key, e => (object)e.Value);
+}
diff --git a/test/EFCore.PG.FunctionalTests/Query/HstoreQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/HstoreQueryTest.cs
new file mode 100644
index 000000000..7c75674df
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/Query/HstoreQueryTest.cs
@@ -0,0 +1,255 @@
+using Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Dictionary;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;
+
+public class HstoreQueryTest : QueryTestBase
+{
+ public HstoreQueryTest(HstoreQueryFixture fixture, ITestOutputHelper testOutputHelper)
+ : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Contains_key(bool async)
+ {
+ var keyToTest = "key";
+ await AssertQuery(async, _ => _.Set().Where(s => s.Dictionary.ContainsKey(keyToTest)));
+ AssertSql("""
+@__keyToTest_0='key'
+
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE s."Dictionary" ? @__keyToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task NullableDictionary_Contains_key(bool async)
+ {
+ var keyToTest = "key2";
+ await AssertQuery(async, _ => _.Set().Where(s => s.NullableDictionary != null && s.NullableDictionary.ContainsKey(keyToTest)));
+ AssertSql("""
+@__keyToTest_0='key2'
+
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE s."NullableDictionary" IS NOT NULL AND s."NullableDictionary" ? @__keyToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableNullableDictionary_Contains_key(bool async)
+ {
+ var keyToTest = "key3";
+ await AssertQuery(async, _ => _.Set().Where(s => s.NullableImmutableDictionary != null && s.NullableImmutableDictionary.ContainsKey(keyToTest)));
+ AssertSql("""
+@__keyToTest_0='key3'
+
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE s."NullableImmutableDictionary" IS NOT NULL AND s."NullableImmutableDictionary" ? @__keyToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Contains_key(bool async)
+ {
+ var keyToTest = "key3";
+ await AssertQuery(async, _ => _.Set().Where(s => s.ImmutableDictionary.ContainsKey(keyToTest)));
+ AssertSql(
+ """
+@__keyToTest_0='key3'
+
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE s."ImmutableDictionary" ? @__keyToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Contains_value(bool async)
+ {
+ var valueToTest = "value";
+ await AssertQuery(async, _ => _.Set().Where(s => s.Dictionary.ContainsValue(valueToTest)));
+ AssertSql(
+ """
+@__valueToTest_0='value'
+
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE @__valueToTest_0 = ANY(avals(s."Dictionary"))
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Item_equals_found(bool async)
+ {
+ var keyToTest = "key";
+ var valueToTest = "value";
+ await AssertQuery(async, _ => _.Set().Where(s => s.Dictionary[keyToTest] == valueToTest));
+ AssertSql(
+ """
+@__valueToTest_0='value'
+
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE s."Dictionary" -> 'key' = @__valueToTest_0
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Item_equals_not_found(bool async)
+ {
+ var keyToTest = "key";
+ var valueToTest = "value2";
+ await AssertQuery(async, _ => _.Set().Where(s => s.Dictionary.ContainsKey(keyToTest) && s.Dictionary[keyToTest] == valueToTest), assertEmpty: true);
+ AssertSql(
+ """
+@__keyToTest_0='key'
+@__valueToTest_1='value2'
+
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE s."Dictionary" ? @__keyToTest_0 AND s."Dictionary" -> 'key' = @__valueToTest_1
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task NullableDictionary_Item_equals(bool async)
+ {
+ var keyToTest = "key2";
+ var valueToTest = "value";
+ await AssertQuery(
+ async,
+ _ => _.Set().Where(
+ s => s.NullableDictionary != null
+ && s.NullableDictionary.ContainsKey(keyToTest)
+ && s.NullableDictionary[keyToTest] == valueToTest));
+ AssertSql(
+ """
+@__keyToTest_0='key2'
+@__valueToTest_1='value'
+
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE s."NullableDictionary" IS NOT NULL AND s."NullableDictionary" ? @__keyToTest_0 AND s."NullableDictionary" -> 'key2' = @__valueToTest_1
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableNullableDictionary_Item_equals(bool async)
+ {
+ var keyToTest = "key3";
+ var valueToTest = "value2";
+ await AssertQuery(
+ async,
+ _ => _.Set().Where(
+ s => s.NullableImmutableDictionary != null
+ && s.NullableImmutableDictionary.ContainsKey(keyToTest)
+ && s.NullableImmutableDictionary[keyToTest] == valueToTest));
+ AssertSql(
+ """
+@__keyToTest_0='key3'
+@__valueToTest_1='value2'
+
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE s."NullableImmutableDictionary" IS NOT NULL AND s."NullableImmutableDictionary" ? @__keyToTest_0 AND s."NullableImmutableDictionary" -> @__keyToTest_0 = @__valueToTest_1
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Item_equals(bool async)
+ {
+ var keyToTest = "key2";
+ var valueToTest = "value2";
+ await AssertQuery(async, _ => _.Set().Where(s => s.ImmutableDictionary.ContainsKey(keyToTest) && s.ImmutableDictionary[keyToTest] == valueToTest));
+ AssertSql(
+ """
+@__keyToTest_0='key2'
+@__valueToTest_1='value2'
+
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE s."ImmutableDictionary" ? @__keyToTest_0 AND s."ImmutableDictionary" -> @__keyToTest_0 = @__valueToTest_1
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Where_Count(bool async)
+ {
+ await AssertQuery(async, _ => _.Set().Where(s => s.Dictionary.Count >= 1));
+ AssertSql(
+ """
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE array_length(akeys(s."Dictionary"), 1) >= 1
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task Dictionary_Select_Count(bool async)
+ {
+ await AssertQuery(async, _ => _.Set().Select(s => s.Dictionary.Count));
+ AssertSql(
+ """
+SELECT array_length(akeys(s."Dictionary"), 1)
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Where_Count(bool async)
+ {
+ await AssertQuery(async, _ => _.Set().Where(s => s.ImmutableDictionary.Count >= 1));
+ AssertSql(
+ """
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE array_length(akeys(s."ImmutableDictionary"), 1) >= 1
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Select_Count(bool async)
+ {
+ await AssertQuery(async, _ => _.Set().Select(s => s.ImmutableDictionary.Count));
+ AssertSql(
+ """
+SELECT array_length(akeys(s."ImmutableDictionary"), 1)
+FROM "SomeEntities" AS s
+""");
+ }
+
+ [Theory]
+ [MemberData(nameof(IsAsyncData))]
+ public async Task ImmutableDictionary_Where_IsEmpty(bool async)
+ {
+ await AssertQuery(async, _ => _.Set().Where(s => !s.ImmutableDictionary.IsEmpty));
+ AssertSql(
+ """
+SELECT s."Id", s."Dictionary", s."DictionaryContainerEntityId", s."ImmutableDictionary", s."NullableDictionary", s."NullableImmutableDictionary"
+FROM "SomeEntities" AS s
+WHERE array_length(akeys(s."ImmutableDictionary"), 1) <> 0
+""");
+ }
+
+ protected void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+}
diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryContainerEntity.cs b/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryContainerEntity.cs
new file mode 100644
index 000000000..9db129448
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryContainerEntity.cs
@@ -0,0 +1,7 @@
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Dictionary;
+
+public class DictionaryContainerEntity
+{
+ public int Id { get; set; }
+ public List DictionaryEntities { get; set; } = null!;
+}
diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryEntity.cs b/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryEntity.cs
new file mode 100644
index 000000000..8603ec98c
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryEntity.cs
@@ -0,0 +1,17 @@
+#nullable enable
+using System.Collections.Immutable;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Dictionary;
+
+public class DictionaryEntity
+{
+ public int Id { get; set; }
+
+ public Dictionary Dictionary { get; set; } = null!;
+
+ public ImmutableDictionary ImmutableDictionary { get; set; } = null!;
+
+ public Dictionary? NullableDictionary { get; set; } = null!;
+
+ public ImmutableDictionary? NullableImmutableDictionary { get; set; } = null!;
+}
diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryQueryContext.cs b/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryQueryContext.cs
new file mode 100644
index 000000000..33b98a041
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryQueryContext.cs
@@ -0,0 +1,22 @@
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Dictionary;
+
+public class DictionaryQueryContext(DbContextOptions options) : PoolableDbContext(options)
+{
+ public DbSet SomeEntities { get; set; }
+ public DbSet SomeEntityContainers { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity();
+ modelBuilder.Entity();
+ }
+
+ public static async Task SeedAsync(DictionaryQueryContext context)
+ {
+ var arrayEntities = DictionaryQueryData.CreateDictionaryEntities();
+
+ context.SomeEntities.AddRange(arrayEntities);
+ context.SomeEntityContainers.Add(new DictionaryContainerEntity() { Id = 1, DictionaryEntities = arrayEntities.ToList() });
+ await context.SaveChangesAsync();
+ }
+}
diff --git a/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryQueryData.cs b/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryQueryData.cs
new file mode 100644
index 000000000..5ee965b4a
--- /dev/null
+++ b/test/EFCore.PG.FunctionalTests/TestModels/Dictionary/DictionaryQueryData.cs
@@ -0,0 +1,47 @@
+using System.Collections.Immutable;
+
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Dictionary;
+
+public class DictionaryQueryData : ISetSource
+{
+ public IReadOnlyList DictionaryEntities { get; } = CreateDictionaryEntities();
+ public IReadOnlyList ContainerEntities { get; } = CreateContainerEntities();
+
+ public IQueryable Set()
+ where TEntity : class
+ {
+ if (typeof(TEntity) == typeof(DictionaryEntity))
+ {
+ return (IQueryable)DictionaryEntities.AsQueryable();
+ }
+
+ if (typeof(TEntity) == typeof(DictionaryContainerEntity))
+ {
+ return (IQueryable)ContainerEntities.AsQueryable();
+ }
+
+ throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity));
+ }
+
+ public static IReadOnlyList CreateDictionaryEntities()
+ =>
+ [
+ new()
+ {
+ Id = 1,
+ Dictionary = new() { ["key"] = "value" },
+ ImmutableDictionary = new Dictionary { ["key2"] = "value2" }.ToImmutableDictionary(),
+ },
+ new()
+ {
+ Id = 2,
+ Dictionary = new() { ["key"] = "value" },
+ ImmutableDictionary = new Dictionary { ["key3"] = "value3" }.ToImmutableDictionary(),
+ NullableDictionary = new() { ["key2"] = "value" },
+ NullableImmutableDictionary = new Dictionary { ["key3"] = "value2" }.ToImmutableDictionary(),
+ }
+ ];
+
+ public static IReadOnlyList CreateContainerEntities()
+ => [new DictionaryContainerEntity { Id = 1, DictionaryEntities = CreateDictionaryEntities().ToList() }];
+}