diff --git a/src/QueryBuilder/ExpandBuilder.cs b/src/QueryBuilder/ExpandBuilder.cs index 4b37e15..6b5d98e 100644 --- a/src/QueryBuilder/ExpandBuilder.cs +++ b/src/QueryBuilder/ExpandBuilder.cs @@ -1,4 +1,4 @@ -namespace DataverseQuery +namespace DataverseQuery.QueryBuilder { public sealed class ExpandBuilder { @@ -18,4 +18,4 @@ public ExpandBuilder(string relationshipName, Type targetType, IQueryBuilder bui IsCollection = isCollection; } } -} +} \ No newline at end of file diff --git a/src/QueryBuilder/Extensions/WhereGroupBuilderExtensions.cs b/src/QueryBuilder/Extensions/WhereGroupBuilderExtensions.cs new file mode 100644 index 0000000..8559717 --- /dev/null +++ b/src/QueryBuilder/Extensions/WhereGroupBuilderExtensions.cs @@ -0,0 +1,167 @@ +using System.Linq.Expressions; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace DataverseQuery.QueryBuilder.Extensions +{ + /// + /// Extension methods for WhereGroupBuilder to provide common filter patterns. + /// + public static class WhereGroupBuilderExtensions + { + /// + /// Adds an equality condition. + /// + /// The entity type. + /// The value type of the property. + /// The WhereGroupBuilder instance. + /// The lambda expression selecting the field. + /// The value to compare against. + /// This WhereGroupBuilder instance for method chaining. + /// Thrown when builder or fieldSelector is null. + public static WhereGroupBuilder WhereEqual( + this WhereGroupBuilder builder, + Expression> fieldSelector, + TValue value) + where TEntity : Entity + { + ArgumentNullException.ThrowIfNull(builder); + return builder.Where(fieldSelector, ConditionOperator.Equal, value); + } + + /// + /// Adds a 'like' condition. + /// + /// The entity type. + /// The WhereGroupBuilder instance. + /// The lambda expression selecting the field. + /// The pattern to match (supports % wildcards). + /// This WhereGroupBuilder instance for method chaining. + /// Thrown when builder or fieldSelector is null. + public static WhereGroupBuilder WhereLike( + this WhereGroupBuilder builder, + Expression> fieldSelector, + string pattern) + where TEntity : Entity + { + ArgumentNullException.ThrowIfNull(builder); + return builder.Where(fieldSelector, ConditionOperator.Like, pattern); + } + + /// + /// Adds an 'in' condition. + /// + /// The entity type. + /// The value type of the property. + /// The WhereGroupBuilder instance. + /// The lambda expression selecting the field. + /// The values to check against. + /// This WhereGroupBuilder instance for method chaining. + /// Thrown when builder or fieldSelector is null. + public static WhereGroupBuilder WhereIn( + this WhereGroupBuilder builder, + Expression> fieldSelector, + params TValue[] values) + where TEntity : Entity + { + ArgumentNullException.ThrowIfNull(builder); + return builder.Where(fieldSelector, ConditionOperator.In, values); + } + + /// + /// Adds a 'not equal' condition. + /// + /// The entity type. + /// The value type of the property. + /// The WhereGroupBuilder instance. + /// The lambda expression selecting the field. + /// The value to compare against. + /// This WhereGroupBuilder instance for method chaining. + /// Thrown when builder or fieldSelector is null. + public static WhereGroupBuilder WhereNotEqual( + this WhereGroupBuilder builder, + Expression> fieldSelector, + TValue value) + where TEntity : Entity + { + ArgumentNullException.ThrowIfNull(builder); + return builder.Where(fieldSelector, ConditionOperator.NotEqual, value); + } + + /// + /// Adds a 'greater than' condition. + /// + /// The entity type. + /// The value type of the property. + /// The WhereGroupBuilder instance. + /// The lambda expression selecting the field. + /// The value to compare against. + /// This WhereGroupBuilder instance for method chaining. + /// Thrown when builder or fieldSelector is null. + public static WhereGroupBuilder WhereGreaterThan( + this WhereGroupBuilder builder, + Expression> fieldSelector, + TValue value) + where TEntity : Entity + { + ArgumentNullException.ThrowIfNull(builder); + return builder.Where(fieldSelector, ConditionOperator.GreaterThan, value); + } + + /// + /// Adds a 'less than' condition. + /// + /// The entity type. + /// The value type of the property. + /// The WhereGroupBuilder instance. + /// The lambda expression selecting the field. + /// The value to compare against. + /// This WhereGroupBuilder instance for method chaining. + /// Thrown when builder or fieldSelector is null. + public static WhereGroupBuilder WhereLessThan( + this WhereGroupBuilder builder, + Expression> fieldSelector, + TValue value) + where TEntity : Entity + { + ArgumentNullException.ThrowIfNull(builder); + return builder.Where(fieldSelector, ConditionOperator.LessThan, value); + } + + /// + /// Adds a 'null' condition. + /// + /// The entity type. + /// The value type of the property. + /// The WhereGroupBuilder instance. + /// The lambda expression selecting the field. + /// This WhereGroupBuilder instance for method chaining. + /// Thrown when builder or fieldSelector is null. + public static WhereGroupBuilder WhereNull( + this WhereGroupBuilder builder, + Expression> fieldSelector) + where TEntity : Entity + { + ArgumentNullException.ThrowIfNull(builder); + return builder.Where(fieldSelector, ConditionOperator.Null); + } + + /// + /// Adds a 'not null' condition. + /// + /// The entity type. + /// The value type of the property. + /// The WhereGroupBuilder instance. + /// The lambda expression selecting the field. + /// This WhereGroupBuilder instance for method chaining. + /// Thrown when builder or fieldSelector is null. + public static WhereGroupBuilder WhereNotNull( + this WhereGroupBuilder builder, + Expression> fieldSelector) + where TEntity : Entity + { + ArgumentNullException.ThrowIfNull(builder); + return builder.Where(fieldSelector, ConditionOperator.NotNull); + } + } +} \ No newline at end of file diff --git a/src/QueryBuilder/IQueryBuilder.cs b/src/QueryBuilder/IQueryBuilder.cs index 4f9a80f..55c2107 100644 --- a/src/QueryBuilder/IQueryBuilder.cs +++ b/src/QueryBuilder/IQueryBuilder.cs @@ -1,6 +1,6 @@ using Microsoft.Xrm.Sdk.Query; -namespace DataverseQuery +namespace DataverseQuery.QueryBuilder { public interface IQueryBuilder { diff --git a/src/QueryBuilder/Interfaces/IAttributeNameResolver.cs b/src/QueryBuilder/Interfaces/IAttributeNameResolver.cs new file mode 100644 index 0000000..5fe0a05 --- /dev/null +++ b/src/QueryBuilder/Interfaces/IAttributeNameResolver.cs @@ -0,0 +1,21 @@ +using System.Linq.Expressions; +using Microsoft.Xrm.Sdk; + +namespace DataverseQuery.QueryBuilder.Interfaces +{ + /// + /// Resolves attribute names from lambda expressions. + /// + public interface IAttributeNameResolver + { + /// + /// Gets the attribute name from a property selector expression. + /// + /// The entity type. + /// The value type of the property. + /// The lambda expression selecting the property. + /// The attribute name, or null if it cannot be resolved. + string? GetAttributeName(Expression> fieldSelector) + where TEntity : Entity; + } +} \ No newline at end of file diff --git a/src/QueryBuilder/Interfaces/IValueConverter.cs b/src/QueryBuilder/Interfaces/IValueConverter.cs new file mode 100644 index 0000000..78e30f5 --- /dev/null +++ b/src/QueryBuilder/Interfaces/IValueConverter.cs @@ -0,0 +1,16 @@ +namespace DataverseQuery.QueryBuilder.Interfaces +{ + /// + /// Converts values for use in Dataverse queries. + /// + public interface IValueConverter + { + /// + /// Converts an array of values to their primitive representations for use in Dataverse queries. + /// + /// The type of values to convert. + /// The values to convert. + /// An array of converted values suitable for Dataverse queries. + object[] ConvertValues(TValue[] values); + } +} \ No newline at end of file diff --git a/src/QueryBuilder/QueryExpressionBuilder.cs b/src/QueryBuilder/QueryExpressionBuilder.cs index 807cbb0..0c1bf87 100644 --- a/src/QueryBuilder/QueryExpressionBuilder.cs +++ b/src/QueryBuilder/QueryExpressionBuilder.cs @@ -1,8 +1,10 @@ using System.Linq.Expressions; +using DataverseQuery.QueryBuilder.Interfaces; +using DataverseQuery.QueryBuilder.Services; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; -namespace DataverseQuery +namespace DataverseQuery.QueryBuilder { public sealed class QueryExpressionBuilder : IQueryBuilder where TEntity : Entity @@ -11,13 +13,39 @@ public sealed class QueryExpressionBuilder : IQueryBuilder private readonly List columns = new(); private readonly List filters = new(); private readonly List expands = new(); + private readonly IAttributeNameResolver attributeNameResolver; + private readonly IValueConverter valueConverter; private int? topCount; + /// + /// Initializes a new instance of the class. + /// public QueryExpressionBuilder() { var logicalNameProp = typeof(TEntity).GetField("EntityLogicalName"); entityLogicalName = logicalNameProp?.GetValue(null) as string ?? typeof(TEntity).Name.ToLowerInvariant(); + + attributeNameResolver = new AttributeNameResolver(); + valueConverter = new ValueConverter(); + } + + /// + /// Initializes a new instance of the class with custom services. + /// + /// The service for resolving attribute names from expressions. + /// The service for converting values for Dataverse queries. + /// Thrown when any parameter is null. + internal QueryExpressionBuilder( + IAttributeNameResolver attributeNameResolver, + IValueConverter valueConverter) + { + var logicalNameProp = typeof(TEntity).GetField("EntityLogicalName"); + entityLogicalName = logicalNameProp?.GetValue(null) as string + ?? typeof(TEntity).Name.ToLowerInvariant(); + + this.attributeNameResolver = attributeNameResolver ?? throw new ArgumentNullException(nameof(attributeNameResolver)); + this.valueConverter = valueConverter ?? throw new ArgumentNullException(nameof(valueConverter)); } // Implement interface methods @@ -30,7 +58,8 @@ public ColumnSet GetColumns() public FilterExpression GetCombinedFilter() { - if (filters.Count == 0) return new FilterExpression(); + if (filters.Count == 0) + return new FilterExpression(); var filter = new FilterExpression(LogicalOperator.And); foreach (var f in filters) @@ -52,10 +81,10 @@ public QueryExpressionBuilder Select(params Expression(selector); + var name = attributeNameResolver.GetAttributeName(selector); if (!string.IsNullOrEmpty(name)) { - columns.Add(name.ToLowerInvariant()); + columns.Add(name); } } @@ -70,22 +99,12 @@ public QueryExpressionBuilder Where( ArgumentNullException.ThrowIfNull(fieldSelector); ArgumentNullException.ThrowIfNull(values); - var name = GetAttributeName(fieldSelector); + var name = attributeNameResolver.GetAttributeName(fieldSelector); if (!string.IsNullOrEmpty(name)) { - var filter = new FilterExpression(); - var primitiveValues = values - .Cast() - .Select(v => - v switch - { - Enum => Convert.ChangeType(v, Enum.GetUnderlyingType(v.GetType()), System.Globalization.CultureInfo.InvariantCulture), - EntityReference er => er.Id, - _ => v, - }) - .ToArray(); - filter.AddCondition(name.ToLowerInvariant(), op, primitiveValues); - filters.Add(filter); + var andFilter = GetOrCreateAndFilter(); + var primitiveValues = valueConverter.ConvertValues(values); + andFilter.AddCondition(name, op, primitiveValues); } return this; @@ -229,21 +248,6 @@ private static string GetEntityLogicalName(Type t) } // --- Helpers --- - private static string? GetAttributeName(Expression> expr) - { - if (expr.Body is MemberExpression member) - { - return member.Member.Name; - } - - if (expr.Body is UnaryExpression unary && unary.Operand is MemberExpression m) - { - return m.Member.Name; - } - - return null; - } - private static string? GetRelationshipSchemaName(Expression> navigation) { string? propName = null; @@ -353,5 +357,85 @@ private static (string FromAttr, string ToAttr) GetLinkAttributesForReference(st var toAttr = GetEntityLogicalName(targetType) + "id"; return (fromAttr, toAttr); } + + /// + /// Adds a grouped set of filter conditions with the specified logical operator. + /// + /// The logical operator to use within the group (And/Or). + /// Action to configure the filter group. + /// This QueryExpressionBuilder instance for method chaining. + /// Thrown when configure is null. + public QueryExpressionBuilder WhereGroup( + LogicalOperator logicalOperator, + Action> configure) + { + ArgumentNullException.ThrowIfNull(configure); + + var groupBuilder = new WhereGroupBuilder(attributeNameResolver, valueConverter); + configure(groupBuilder); + + if (groupBuilder.HasConditions) + { + var groupFilter = new FilterExpression(logicalOperator); + foreach (var condition in groupBuilder.GetConditions()) + { + groupFilter.AddCondition(condition); + } + + filters.Add(groupFilter); + } + + return this; + } + + /// + /// Adds an OR group of filter conditions. + /// + /// Action to configure the filter group. + /// This QueryExpressionBuilder instance for method chaining. + /// Thrown when configure is null. + public QueryExpressionBuilder OrWhereGroup(Action> configure) + => WhereGroup(LogicalOperator.Or, configure); + + /// + /// Adds an AND group of filter conditions. + /// + /// Action to configure the filter group. + /// This QueryExpressionBuilder instance for method chaining. + /// Thrown when configure is null. + public QueryExpressionBuilder AndWhereGroup(Action> configure) + => WhereGroup(LogicalOperator.And, configure); + + /// + /// Adds multiple OR conditions to a single filter group. + /// Convenience method for creating OR groups. + /// + /// Action to configure the filter group. + /// This QueryExpressionBuilder instance for method chaining. + /// Thrown when configure is null. + public QueryExpressionBuilder WhereAny(Action> configure) + => OrWhereGroup(configure); + + /// + /// Adds multiple AND conditions to a single filter group. + /// Convenience method for creating AND groups. + /// + /// Action to configure the filter group. + /// This QueryExpressionBuilder instance for method chaining. + /// Thrown when configure is null. + public QueryExpressionBuilder WhereAll(Action> configure) + => AndWhereGroup(configure); + + private FilterExpression GetOrCreateAndFilter() + { + var andFilter = filters.LastOrDefault(f => f.FilterOperator == LogicalOperator.And); + if (andFilter == null) + { + andFilter = new FilterExpression(LogicalOperator.And); + filters.Add(andFilter); + } + + return andFilter; + } } -} +} \ No newline at end of file diff --git a/src/QueryBuilder/QueryExpressionBuilderExtensions.cs b/src/QueryBuilder/QueryExpressionBuilderExtensions.cs index 297fee7..f3a70a2 100644 --- a/src/QueryBuilder/QueryExpressionBuilderExtensions.cs +++ b/src/QueryBuilder/QueryExpressionBuilderExtensions.cs @@ -1,6 +1,6 @@ using Microsoft.Xrm.Sdk; -namespace DataverseQuery +namespace DataverseQuery.QueryBuilder { public static class QueryExpressionBuilderExtensions { diff --git a/src/QueryBuilder/Services/AttributeNameResolver.cs b/src/QueryBuilder/Services/AttributeNameResolver.cs new file mode 100644 index 0000000..f672732 --- /dev/null +++ b/src/QueryBuilder/Services/AttributeNameResolver.cs @@ -0,0 +1,32 @@ +using System.Linq.Expressions; +using DataverseQuery.QueryBuilder.Interfaces; +using Microsoft.Xrm.Sdk; + +namespace DataverseQuery.QueryBuilder.Services +{ + /// + /// Default implementation of attribute name resolution. + /// + public sealed class AttributeNameResolver : IAttributeNameResolver + { + /// + /// Gets the attribute name from a property selector expression. + /// + /// The entity type. + /// The value type of the property. + /// The lambda expression selecting the property. + /// The attribute name in lowercase, or null if it cannot be resolved. + public string? GetAttributeName(Expression> fieldSelector) + where TEntity : Entity + { + ArgumentNullException.ThrowIfNull(fieldSelector); + + return fieldSelector.Body switch + { + MemberExpression member => member.Member.Name.ToLowerInvariant(), + UnaryExpression { Operand: MemberExpression unaryMember } => unaryMember.Member.Name.ToLowerInvariant(), + _ => null, + }; + } + } +} \ No newline at end of file diff --git a/src/QueryBuilder/Services/ValueConverter.cs b/src/QueryBuilder/Services/ValueConverter.cs new file mode 100644 index 0000000..d8186e6 --- /dev/null +++ b/src/QueryBuilder/Services/ValueConverter.cs @@ -0,0 +1,41 @@ +using System.Globalization; +using DataverseQuery.QueryBuilder.Interfaces; +using Microsoft.Xrm.Sdk; + +namespace DataverseQuery.QueryBuilder.Services +{ + /// + /// Default implementation of value conversion for Dataverse queries. + /// + public sealed class ValueConverter : IValueConverter + { + /// + /// Converts an array of values to their primitive representations for use in Dataverse queries. + /// + /// The type of values to convert. + /// The values to convert. + /// An array of converted values suitable for Dataverse queries. + /// Thrown when values is null. + /// Thrown when any value in the array is null. + public object[] ConvertValues(TValue[] values) + { + ArgumentNullException.ThrowIfNull(values); + + return values + .Cast() + .Select(ConvertValue) + .ToArray(); + } + + private static object ConvertValue(object value) + { + return value switch + { + null => throw new ArgumentException("Null values are not supported in filter conditions.", nameof(value)), + Enum enumValue => Convert.ChangeType(enumValue, Enum.GetUnderlyingType(enumValue.GetType()), CultureInfo.InvariantCulture), + EntityReference entityRef => entityRef.Id, + _ => value, + }; + } + } +} \ No newline at end of file diff --git a/src/QueryBuilder/WhereGroupBuilder.cs b/src/QueryBuilder/WhereGroupBuilder.cs new file mode 100644 index 0000000..c6ec268 --- /dev/null +++ b/src/QueryBuilder/WhereGroupBuilder.cs @@ -0,0 +1,72 @@ +using System.Linq.Expressions; +using DataverseQuery.QueryBuilder.Interfaces; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace DataverseQuery.QueryBuilder +{ + /// + /// Builder for creating grouped filter conditions with a specific logical operator. + /// + /// The entity type being queried. + public sealed class WhereGroupBuilder + where TEntity : Entity + { + private readonly List conditions = new(); + private readonly IAttributeNameResolver attributeNameResolver; + private readonly IValueConverter valueConverter; + + /// + /// Initializes a new instance of the class. + /// + /// The service for resolving attribute names from expressions. + /// The service for converting values for Dataverse queries. + /// Thrown when any parameter is null. + internal WhereGroupBuilder( + IAttributeNameResolver attributeNameResolver, + IValueConverter valueConverter) + { + this.attributeNameResolver = attributeNameResolver ?? throw new ArgumentNullException(nameof(attributeNameResolver)); + this.valueConverter = valueConverter ?? throw new ArgumentNullException(nameof(valueConverter)); + } + + /// + /// Adds a condition to this filter group. + /// + /// The type of the field value. + /// The lambda expression selecting the field. + /// The condition operator to apply. + /// The values to compare against. + /// This WhereGroupBuilder instance for method chaining. + /// Thrown when fieldSelector or values is null. + public WhereGroupBuilder Where( + Expression> fieldSelector, + ConditionOperator op, + params TValue[] values) + { + ArgumentNullException.ThrowIfNull(fieldSelector); + ArgumentNullException.ThrowIfNull(values); + + var attributeName = attributeNameResolver.GetAttributeName(fieldSelector); + if (!string.IsNullOrEmpty(attributeName)) + { + var convertedValues = valueConverter.ConvertValues(values); + conditions.Add(new ConditionExpression(attributeName, op, convertedValues)); + } + + return this; + } + + /// + /// Gets all conditions in this group. + /// + /// A read-only list of conditions in this group. + internal IReadOnlyList GetConditions() => conditions.AsReadOnly(); + + /// + /// Gets whether this group has any conditions. + /// + /// True if this group has conditions, false otherwise. + internal bool HasConditions => conditions.Count > 0; + } +} \ No newline at end of file diff --git a/test/QueryBuilder.Tests/QueryExpressionBuilderTests.cs b/test/QueryBuilder.Tests/QueryExpressionBuilderTests.cs index 42aa852..a00ecb5 100644 --- a/test/QueryBuilder.Tests/QueryExpressionBuilderTests.cs +++ b/test/QueryBuilder.Tests/QueryExpressionBuilderTests.cs @@ -1,3 +1,4 @@ +using DataverseQuery.QueryBuilder; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; @@ -116,5 +117,125 @@ public void Where_EntityReferenceFilter_UsesGuidValue() Assert.IsType(condition.Values[0]); Assert.Equal(parentId, (Guid)condition.Values[0]); } + + [Fact] + public void WhereGroup_WithOrConditions_CreatesCorrectFilterStructure() + { + // Arrange & Act + var query = new QueryExpressionBuilder() + .Select(a => a.Name, a => a.StateCode) + .Where(a => a.StateCode, ConditionOperator.Equal, SharedContext.AccountState.Aktiv) + .OrWhereGroup(group => group + .Where(a => a.Name, ConditionOperator.Like, "%Test%") + .Where(a => a.AccountNumber, ConditionOperator.Like, "%123%")) + .Build(); + + // Assert + Assert.Equal(2, query.Criteria.Filters.Count); + + var andFilter = query.Criteria.Filters.First(f => f.FilterOperator == LogicalOperator.And); + Assert.Single(andFilter.Conditions); + Assert.Equal("statecode", andFilter.Conditions[0].AttributeName); + + var orFilter = query.Criteria.Filters.First(f => f.FilterOperator == LogicalOperator.Or); + Assert.Equal(2, orFilter.Conditions.Count); + Assert.Equal("name", orFilter.Conditions[0].AttributeName); + Assert.Equal("accountnumber", orFilter.Conditions[1].AttributeName); + } + + [Fact] + public void AndWhereGroup_CreatesAndFilter() + { + // Arrange & Act + var query = new QueryExpressionBuilder() + .Select(a => a.Name) + .AndWhereGroup(group => group + .Where(a => a.Name, ConditionOperator.Like, "%Test%") + .Where(a => a.StateCode, ConditionOperator.Equal, SharedContext.AccountState.Aktiv)) + .Build(); + + // Assert + Assert.Single(query.Criteria.Filters); + var andFilter = query.Criteria.Filters[0]; + Assert.Equal(LogicalOperator.And, andFilter.FilterOperator); + Assert.Equal(2, andFilter.Conditions.Count); + } + + [Fact] + public void WhereAny_CreatesOrFilter() + { + // Arrange & Act + var query = new QueryExpressionBuilder() + .Select(a => a.Name) + .WhereAny(group => group + .Where(a => a.Name, ConditionOperator.Like, "%Test%") + .Where(a => a.Name, ConditionOperator.Like, "%Demo%")) + .Build(); + + // Assert + Assert.Single(query.Criteria.Filters); + var orFilter = query.Criteria.Filters[0]; + Assert.Equal(LogicalOperator.Or, orFilter.FilterOperator); + Assert.Equal(2, orFilter.Conditions.Count); + } + + [Fact] + public void WhereAll_CreatesAndFilter() + { + // Arrange & Act + var query = new QueryExpressionBuilder() + .Select(a => a.Name) + .WhereAll(group => group + .Where(a => a.StateCode, ConditionOperator.Equal, SharedContext.AccountState.Aktiv) + .Where(a => a.StatusCode, ConditionOperator.Equal, SharedContext.Account_StatusCode.Aktiv)) + .Build(); + + // Assert + Assert.Single(query.Criteria.Filters); + var andFilter = query.Criteria.Filters[0]; + Assert.Equal(LogicalOperator.And, andFilter.FilterOperator); + Assert.Equal(2, andFilter.Conditions.Count); + } + + [Fact] + public void ComplexGrouping_WithMultipleFilters_CreatesCorrectStructure() + { + // Arrange & Act - (status = active OR status = inactive) AND (type = customer OR type = partner) + var query = new QueryExpressionBuilder() + .Select(a => a.Name) + .OrWhereGroup(group => group + .Where(a => a.StateCode, ConditionOperator.Equal, SharedContext.AccountState.Aktiv) + .Where(a => a.StateCode, ConditionOperator.Equal, SharedContext.AccountState.Inaktiv)) + .OrWhereGroup(group => group + .Where(a => a.CustomerTypeCode, ConditionOperator.Equal, SharedContext.Account_CustomerTypeCode.Kunde) + .Where(a => a.CustomerTypeCode, ConditionOperator.Equal, SharedContext.Account_CustomerTypeCode.Partner)) + .Build(); + + // Assert + Assert.Equal(2, query.Criteria.Filters.Count); + + foreach (var filter in query.Criteria.Filters) + { + Assert.Equal(LogicalOperator.Or, filter.FilterOperator); + Assert.Equal(2, filter.Conditions.Count); + } + } + + [Fact] + public void WhereGroup_WithEmptyGroup_DoesNotAddFilter() + { + // Arrange & Act + var query = new QueryExpressionBuilder() + .Select(a => a.Name) + .Where(a => a.StateCode, ConditionOperator.Equal, SharedContext.AccountState.Aktiv) + .OrWhereGroup(group => { /* Empty group */ }) + .Build(); + + // Assert - Only the regular Where condition should be present + Assert.Single(query.Criteria.Filters); + var filter = query.Criteria.Filters[0]; + Assert.Equal(LogicalOperator.And, filter.FilterOperator); + Assert.Single(filter.Conditions); + } } -} +} \ No newline at end of file