FluentDynamics.QueryBuilder 1.1.2

dotnet add package FluentDynamics.QueryBuilder --version 1.1.2
                    
NuGet\Install-Package FluentDynamics.QueryBuilder -Version 1.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FluentDynamics.QueryBuilder" Version="1.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FluentDynamics.QueryBuilder" Version="1.1.2" />
                    
Directory.Packages.props
<PackageReference Include="FluentDynamics.QueryBuilder" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FluentDynamics.QueryBuilder --version 1.1.2
                    
#r "nuget: FluentDynamics.QueryBuilder, 1.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FluentDynamics.QueryBuilder@1.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FluentDynamics.QueryBuilder&version=1.1.2
                    
Install as a Cake Addin
#tool nuget:?package=FluentDynamics.QueryBuilder&version=1.1.2
                    
Install as a Cake Tool

FluentDynamics QueryBuilder

FluentDynamics QueryBuilder is a fluent, chainable API for building and executing Dynamics 365/Dataverse queries. It simplifies the process of creating complex QueryExpressions with a more intuitive and readable syntax.

NuGet License Line Coverage Branch Coverage Method Coverage Tests

Features

  • ๐Ÿ”„ Fluent API - Chainable, intuitive query building
  • ๐Ÿ” Type-safe - Strong typing for Dynamics 365 operations
  • ๐Ÿš€ Async Support - Full support for async/await patterns
  • ๐Ÿ“Š LINQ-like Operations - Familiar extension methods for query results
  • ๐Ÿ“‘ Pagination - Built-in support for handling paged results
  • ๐Ÿ”— Complex & Advanced Joins - Rich set of join helpers (Inner, LeftOuter, Natural, CrossApply, In, Exists, Any/NotAny, All/NotAll)
  • ๐Ÿงฉ Extensible - Clean architecture for extending functionality
  • ๐Ÿ›  FetchXML Conversion - Convert queries to FetchXML easily
  • ๐Ÿงฎ Distinct, NoLock, QueryHint, ForceSeek - Advanced query options
  • โšก FilterBuilder Extensions - Syntactic sugar methods for common conditions (Equal, In, Like, LastXDays, IsNull, etc.)
  • ๐Ÿ” Query Debugging - Human-readable query inspection with DebugView
  • ๐Ÿ”„ Optimized Cloning - Efficient query cloning operations for different use cases

Installation

Install via NuGet Package Manager:

Install-Package FluentDynamics.QueryBuilder

Or via .NET CLI:

dotnet add package FluentDynamics.QueryBuilder

Basic Usage

using FluentDynamics.QueryBuilder;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

// Create a simple query
var query = Query.For("account")
    .Select("name", "accountnumber", "telephone1")
    .Where(f => f
        .Condition("statecode", ConditionOperator.Equal, 0)
    )
    .OrderBy("name");

// Execute the query
EntityCollection results = query.RetrieveMultiple(organizationService);

// Use extension methods on results
var accounts = results.ToList();

Advanced Examples

Complex Filtering (Nested AND/OR)

var query = Query.For("contact")
    .Select("firstname", "lastname", "emailaddress1")
    .Where(f => f
        .Condition("statecode", ConditionOperator.Equal, 0)
        .And(fa => fa
            .Condition("createdon", ConditionOperator.LastXDays, 30)
            .Condition("emailaddress1", ConditionOperator.NotNull)
        )
        .Or(fo => fo
            .Condition("parentcustomerid", ConditionOperator.Equal, accountId)
            .Condition("address1_city", ConditionOperator.Equal, "Seattle")
        )
    )
    .OrderBy("lastname")
    .OrderBy("firstname");

Complex Filtering Using FilterBuilder Extensions

var query = Query.For("contact")
    .Select("firstname", "lastname", "emailaddress1")
    .Where(f => f
        .Equal("statecode", 0)
        .And(fa => fa
            .LastXDays("createdon", 30)
            .IsNotNull("emailaddress1")
        )
        .Or(fo => fo
            .Equal("parentcustomerid", accountId)
            .Equal("address1_city", "Seattle")
        )
    )
    .OrderBy("lastname")
    .OrderBy("firstname");
var query = Query.For("opportunity")
    .Select("name", "estimatedvalue", "closeprobability")
    .Where(f => f
        .Condition("statecode", ConditionOperator.Equal, 0)
    )
    .Link("account", "customerid", "accountid", JoinOperator.Inner, link => {
        link.Select("name", "accountnumber")
            .As("account")
            .Where(f => f
                .Condition("statecode", ConditionOperator.Equal, 0)
            );
    })
    .Link("contact", "customerid", "contactid", JoinOperator.LeftOuter, link => {
        link.Select("fullname", "emailaddress1")
            .As("contact");
    });

Advanced Join Extensions

The library exposes helper extension methods (in QueryBuilderExtensions) for additional Dataverse JoinOperator values. These allow more expressive and often more performant queries in certain scenarios.

Extension Method Underlying JoinOperator Purpose / Behavior
InnerJoin Inner Standard inner join (matching related rows only)
LeftOuterJoin LeftOuter Include parent even if no related rows exist
NaturalJoin Natural Natural join (rarely used; relies on platform semantics)
CrossApplyJoin MatchFirstRowUsingCrossApply Optimizes when you only need the first matching child row
InJoin In Translates to IN semantics; can improve performance on large link sets
ExistsJoin Exists Uses EXISTS logic to restrict parents to those having matches
AnyJoin Any Parent rows where any related rows (after link filters) exist
NotAnyJoin NotAny Parent rows where no related rows (after link filters) exist
AllJoin All Parents with related rows where none of those rows satisfy additional filters (logical โ€œALL NOTโ€)
NotAllJoin NotAll Alias of Any semantics (Dataverse quirk)

Note: Some of these operators can significantly change the result semantics and/or execution plan. Always validate with real data and inspect the generated FetchXML (via ToFetchExpression) or use DebugView() for clarity.

When to Use Which Advanced Join

  • Use CrossApplyJoin to fetch a single "primary" child row (e.g., latest activity) without bringing an entire child set.
  • Use ExistsJoin / AnyJoin when you want to check for the presence of related rows without needing their data.
  • Use NotAnyJoin to enforce absence of related data (e.g. accounts without open opportunities).
  • Use InJoin when a sub-select style inclusion might be faster than a traditional join (test vs inner join).
  • Use AllJoin when you need parent rows where all related rows fail a condition expressed in the filters (Dataverse implements this as โ€œall related but none meet filter Xโ€).

Code Examples

1. Any vs NotAny (Presence / Absence)
// Accounts that HAVE at least one active contact
var accountsWithActiveContact = Query.For("account")
    .AnyJoin("contact", "accountid", "parentcustomerid", link => link
        .Where(f => f.Equal("statecode", 0))
    );

// Accounts that have NO active contact
var accountsWithoutActiveContact = Query.For("account")
    .NotAnyJoin("contact", "accountid", "parentcustomerid", link => link
        .Where(f => f.Equal("statecode", 0))
    );
2. Exists vs In
// Accounts that have at least one open opportunity (using EXISTS)
var accountsWithOpportunitiesExists = Query.For("account")
    .ExistsJoin("opportunity", "accountid", "parentaccountid", link => link
        .Where(f => f.Equal("statecode", 0))
    );

// Accounts using IN-based restriction (platform may choose different plan)
var accountsWithOpportunitiesIn = Query.For("account")
    .InJoin("opportunity", "accountid", "parentaccountid", link => link
        .Where(f => f.Equal("statecode", 0))
    );
3. CrossApply (First Matching Child)
// Get account with only ONE (first) recent task (improves performance vs full child set)
var accountsWithOneTask = Query.For("account")
    .CrossApplyJoin("task", "accountid", "regardingobjectid", link => link
        .Select("subject", "scheduledend")
        .OrderByDesc("scheduledend")  // Ensure desired ordering for "first"
        .Top(1) // (Optional) If you later add Top to the link (manual pattern)
    );

Dataverseโ€™s cross-apply logic tries to retrieve just the first qualifying child row; ensure your sorting reflects the intended โ€œfirstโ€.

4. All / NotAll Semantics
// Accounts where related opportunities exist, but NONE of those are still open
var accountsWhereAllOpportunitiesClosed = Query.For("account")
    .AllJoin("opportunity", "accountid", "parentaccountid", link => link
        .Where(f => f.Equal("statecode", 0)) // Filter defines "open" โ€“ ALL join returns parents where no linked rows satisfy this
    );

// NotAll is effectively Any (platform quirk)
var accountsWithAnyOpenOpportunity = Query.For("account")
    .NotAllJoin("opportunity", "accountid", "parentaccountid", link => link
        .Where(f => f.Equal("statecode", 0))
    );
5. Combining Multiple Advanced Joins
var complex = Query.For("account")
    .Select("name")
    .AnyJoin("contact", "accountid", "parentcustomerid", c => c
        .Where(f => f.Equal("statecode", 0))
        .Select("fullname")
    )
    .NotAnyJoin("opportunity", "accountid", "parentaccountid", o => o
        .Where(f => f.Equal("statecode", 0)) // Has no active opportunities
    )
    .CrossApplyJoin("task", "accountid", "regardingobjectid", t => t
        .Select("subject")
        .OrderByDesc("createdon")
    );

Performance Tips

Scenario Recommended Join Helper Rationale
Need only first child row CrossApplyJoin Avoids pulling all children
Presence check only ExistsJoin or AnyJoin Clear semantic & often cheaper
Absence check NotAnyJoin Efficient anti-semi logic
None of children meet condition AllJoin Expresses universal negative
Explore optimizer difference Compare InnerJoin vs InJoin Sometimes IN variant changes plan
Filtering parent by complex related predicate ExistsJoin Moves predicate to EXISTS scope

Use DebugView() during development:

var debug = complex.DebugView();
Console.WriteLine(debug);

Pagination & Async

// Get a specific page
var page2 = query.RetrieveMultiple(service, pageNumber: 2, pageSize: 50);

// Retrieve all pages automatically
var allResults = query.RetrieveMultipleAllPages(service);

// Using async version
var results = await query.RetrieveMultipleAsync(service);

// Async with pagination
var pageResults = await query.RetrieveMultipleAsync(service, pageNumber: 2, pageSize: 50);

// Async all pages
var allAsyncResults = await query.RetrieveMultipleAllPagesAsync(service);

FetchXML Conversion

// Convert QueryExpression to FetchXML
var fetchXml = query.ToFetchExpression(service);

Working with Results

// Convert to list
var entities = results.ToList();

// Filter results
var filteredEntities = results.Where(e => e.Contains("emailaddress1"));

// Project to new form
var names = results.Select(e => e.GetAttributeValue<string>("name"));

// Get first matching entity
var matchingContact = results.FirstOrDefault(e => 
    e.GetAttributeValue<string>("emailaddress1")?.Contains("example.com") == true);

// Safe attribute access
string name = entity.TryGet<string>("name", "Default Name");

API Reference

Query

Entry point for building queries:

  • Query.For(entityName) - Creates a new query for the specified entity

QueryExpressionBuilder

Methods for configuring the main query:

  • Select(params string[] attributes) - Specifies columns to include
  • SelectAll() - Includes all columns
  • Where(Action<FilterBuilder> filterConfig) - Adds a filter group using fluent configuration
  • OrderBy(attribute) - Adds ascending a sort order
  • OrderByDesc(attribute) - Adds descending a sort order
  • Link(toEntity, fromAttribute, toAttribute, joinType, Action<LinkEntityBuilder> linkBuilder) - Adds a join
  • Top(count) - Limits the number of records
  • Distinct() - Returns only distinct records
  • NoLock() - Uses NOLOCK hint
  • QueryHint(hint) - Adds a query hint
  • ForceSeek(indexName) - Forces using a specific index

Execution Methods

  • RetrieveMultiple(service)
  • RetrieveMultiple(service, pageNumber, pageSize)
  • RetrieveMultipleAllPages(service)
  • Exists(service)
  • RetrieveMultipleAsync(service, CancellationToken cancellationToken = default)
  • RetrieveMultipleAsync(service, pageNumber, pageSize, CancellationToken cancellationToken = default)
  • RetrieveMultipleAllPagesAsync(service, CancellationToken cancellationToken = default)
  • ExistsAsync(service, CancellationToken cancellationToken = default)
  • ToQueryExpression()
  • ToFetchExpression(service)

FilterBuilder

Builds complex filter logic:

  • Condition(attribute, operator, value)
  • And(Action<FilterBuilder> nested)
  • Or(Action<FilterBuilder> nested)
  • ToExpression()

FilterBuilder Extensions (Syntactic Sugar)

Convenience methods mapping to common ConditionOperator values. They improve readability and reduce verbosity.

Extension Purpose Equivalent
Equal(attr, value) Equality Condition(attr, Equal, value)
NotEqual(attr, value) Inequality Condition(attr, NotEqual, value)
GreaterThan(attr, value) Greater than Condition(attr, GreaterThan, value)
GreaterEqual(attr, value) Greater or equal Condition(attr, GreaterEqual, value)
LessThan(attr, value) Less than Condition(attr, LessThan, value)
LessEqual(attr, value) Less or equal Condition(attr, LessEqual, value)
Like(attr, pattern) SQL-like pattern Condition(attr, Like, pattern)
NotLike(attr, pattern) Negated like Condition(attr, NotLike, pattern)
BeginsWith(attr, value) Prefix Condition(attr, BeginsWith, value)
EndsWith(attr, value) Suffix Condition(attr, EndsWith, value)
Contains(attr, value) Contains text Condition(attr, Contains, value)
In(attr, params values) In list Condition(attr, In, valuesArray)
NotIn(attr, params values) Not in list Condition(attr, NotIn, valuesArray)
Between(attr, from, to) Between range Condition(attr, Between, new[]{from,to})
NotBetween(attr, from, to) Not between Condition(attr, NotBetween, new[]{from,to})
IsNull(attr) Null check Condition(attr, Null, null)
IsNotNull(attr) Not null Condition(attr, NotNull, null)
LastXDays(attr, days) Relative date Condition(attr, LastXDays, days)
NextXDays(attr, days) Relative date Condition(attr, NextXDays, days)
LastXMonths(attr, months) Relative date Condition(attr, LastXMonths, months)
NextXMonths(attr, months) Relative date Condition(attr, NextXMonths, months)
LastXYears(attr, years) Relative date Condition(attr, LastXYears, years)
NextXYears(attr, years) Relative date Condition(attr, NextXYears, years)
On(attr, date) Specific date Condition(attr, On, date)
OnOrBefore(attr, date) On or before Condition(attr, OnOrBefore, date)
OnOrAfter(attr, date) On or after Condition(attr, OnOrAfter, date)

Example:

var q = Query.For("contact")
    .Select("firstname", "lastname", "emailaddress1", "createdon")
    .Where(f => f
        .Equal("statecode", 0)
        .IsNotNull("emailaddress1")
        .LastXDays("createdon", 30)
        .Or(o => o
            .Like("emailaddress1", "%@example.com")
            .In("address1_city", "Seattle", "London", "Berlin")
        )
    );

LinkEntityBuilder

Configures join/link entities:

  • Select(params string[] attributes)
  • SelectAll()
  • As(alias)
  • OrderBy(attribute)
  • OrderByDesc(attribute)
  • Where(Action<FilterBuilder> filterConfig)
  • Link(toEntity, fromAttribute, toAttribute, joinType, Action<LinkEntityBuilder> linkBuilder)

Advanced Join Helpers (QueryBuilderExtensions)

  • InnerJoin(...)
  • LeftOuterJoin(...)
  • NaturalJoin(...)
  • CrossApplyJoin(...)
  • InJoin(...)
  • ExistsJoin(...)
  • AnyJoin(...)
  • NotAnyJoin(...)
  • AllJoin(...)
  • NotAllJoin(...)

Extension Methods (LINQ-like)

  • ToList() / ToArray()
  • FirstOrDefault(predicate)
  • SingleOrDefault(predicate)
  • Where(predicate)
  • Select(selector)
  • TryGet<T>(attributeName, defaultValue)
  • DeepClone()
  • ShallowClone()
  • CloneForPagination()
  • DebugView()

Module Coverage

Module Line Branch Method
FluentDynamics.QueryBuilder 85.35% 72.72% 94.55%

Overall Coverage

Metric Line Branch Method
Total 85.35% 72.72% 94.55%
Average 85.35% 72.72% 94.55%

Test Summary

  • Total Tests: 180
  • Failed: 0
  • Succeeded: 180
  • Skipped: 0
  • Duration: 2.5s

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.


Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.2 0 8/22/2025
1.1.1 0 8/22/2025
1.1.0 11 8/20/2025
1.0.5 12 8/20/2025
1.0.4 10 8/19/2025
1.0.3 12 8/19/2025