MWTech.Dotnet.Dataverse.Helpers 1.0.0.15

dotnet add package MWTech.Dotnet.Dataverse.Helpers --version 1.0.0.15
                    
NuGet\Install-Package MWTech.Dotnet.Dataverse.Helpers -Version 1.0.0.15
                    
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="MWTech.Dotnet.Dataverse.Helpers" Version="1.0.0.15" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MWTech.Dotnet.Dataverse.Helpers" Version="1.0.0.15" />
                    
Directory.Packages.props
<PackageReference Include="MWTech.Dotnet.Dataverse.Helpers" />
                    
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 MWTech.Dotnet.Dataverse.Helpers --version 1.0.0.15
                    
#r "nuget: MWTech.Dotnet.Dataverse.Helpers, 1.0.0.15"
                    
#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 MWTech.Dotnet.Dataverse.Helpers@1.0.0.15
                    
#: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=MWTech.Dotnet.Dataverse.Helpers&version=1.0.0.15
                    
Install as a Cake Addin
#tool nuget:?package=MWTech.Dotnet.Dataverse.Helpers&version=1.0.0.15
                    
Install as a Cake Tool

MWTech.Dataverse.Helpers

MWTech.Dataverse.Helpers is a lightweight, stand‑alone helper library that wraps the Microsoft.PowerPlatform.Dataverse.Client SDK and provides a clean, opinionated overlay for common Dataverse scenarios. It is intended for use in Web APIs, console apps, background services or any .NET workload that needs to talk to Dynamics 365 / Dataverse without the usual SDK boilerplate.

Key goals:

  • minimize Entity/Attribute plumbing
  • encapsulate retry/cancellation/diagnostics
  • provide both sync and async‑first APIs (detects underlying *Async methods when available)
  • offer generic POCO converters, query builders and utility extensions
  • work on modern .NET 10/8, older .NET Framework via netstandard2.1

Highlights added in 2026

  • async‑first methods with reflection-based detection of real SDK async implementations (falls back to Task.Run when not found)
  • generic POCO overloads for CRUD operations
  • multi-targeting to net10.0, net8.0 and netstandard2.1
  • Roslyn/StyleCop analyzers, XML doc output, .editorconfig and tests shipped
  • companion xUnit test project included in repository
  • updated to the latest Microsoft.PowerPlatform.Dataverse.Client package

Table of Contents


Installation

You can obtain the helpers either by referencing the NuGet package (when available) or by adding a project reference to MWTech.Dataverse.Helpers in your solution. It targets modern SDKs and older frameworks via netstandard2.1.

<PackageReference Include="MWTech.Dataverse.Helpers" Version="1.0.0" />

or

dotnet add <YourProject> reference ../MWTech.Dataverse.Helpers/MWTech.Dataverse.Helpers.csproj

Quick start

The following minimal example shows how to configure the service, perform basic CRUD operations, and use both generic POCO mapping and the async‑aware API.

// configure (typically in Program.cs or Startup.cs)
var conn = configuration["Dataverse:ConnectionString"];
var orgService = CrmServiceFactory.CreateService(conn);
var dv = new DataverseService(orgService);

// sync create
var ent = new Entity("account") { ["name"] = "QuickStart" };
var id = dv.Create(ent);

// retrieve into a POCO using the generic overload
public class AccountDto { public Guid Id { get; set; } public string? Name { get; set; } }
var dto = dv.Retrieve<AccountDto>("account", id, new ColumnSet("name"));

// update via POCO
dto.Name = "Updated";
dv.Update(dto, "account");

// delete asynchronously (async detection will prefer real async when available)
await dv.DeleteAsync("account", id);

This snippet covers the most common cases; see Usage Examples for more elaborate patterns including querying, paging and logging.

Configuration

The only configuration required is a Dataverse connection string. Typical formats:

AuthType=OAuth;Username=...;Password=...;Url=https://yourorg.crm.dynamics.com;AppId=...;RedirectUri=...;TokenCacheStorePath=c:\tokencache;

Obtaining a valid connection string

  • Power Platform Admin Center – sign in to https://make.powerapps.com, select your environment, then go to Settings > Customizations > Developer Resources. Scroll to the "Instance Web API" section and click Download SDK; the sample connection strings are listed there.
  • Plugin Registration Tool – when you connect to an environment using the tool, the connection dialog displays the full connection string. You can copy it directly for use in your application.
  • Azure AD App Registration – if you use OAuth with a client id/secret, register an application in Azure Active Directory, grant it the Dynamics CRM or Dataverse delegated permissions, then use the generated AppId/ClientSecret in your string. The URL is your org URL (e.g. https://yourorg.crm.dynamics.com).
  • User/Password Authentication – for quick testing you can use AuthType=OAuth;Username=you@contoso.onmicrosoft.com;Password=yourpassword;Url=... but beware this is not recommended for production.

You can also build the string programmatically using the CrmServiceClient constructor overloads or helper methods in the SDK if you prefer not to store it in plain text.

Once you have the string, store it in appsettings.json (or any other configuration provider) and retrieve it via IConfiguration.

{
  "Dataverse": {
    "ConnectionString": "AuthType=OAuth;..."
  }
}

Core Components

CrmServiceFactory

A simple factory that turns a Dataverse connection string into a live IOrganizationService instance. Internally it uses CrmServiceClient from the Microsoft.PowerPlatform.Dataverse.Client package, and it validates the connection before returning the service.

Key features:

  • Throws InvalidOperationException with provider-specific error details if the string is malformed or the server is unreachable.
  • Supports both OAuth and username/password strings.
  • Automatically caches the CrmServiceClient for the lifetime of the application (see source for optional singleton patterns).
// basic usage
var service = CrmServiceFactory.CreateService(connString);

// with error handling
try
{
    var svc = CrmServiceFactory.CreateService(conn);
    // use svc...
}
catch (InvalidOperationException ex)
{
    Console.WriteLine("connection failed: " + ex.Message);
}

If you prefer to construct the CrmServiceClient manually (for unusual settings such as certificate authentication), you can still pass the resulting CrmServiceClient.OrganizationServiceProxy instance to new DataverseService(...) directly.

DataverseService

Encapsulates an IOrganizationService and adds built‑in retry policies, diagnostics hooks and a rich set of convenience overloads. The class itself temporarily implements IOrganizationService so it can be supplied anywhere the SDK interface is expected; the original service remains available via the InnerService property.

🚀 Async‑first behaviour: every async method attempts to call a matching *Async method on the underlying service via reflection. If the real async implementation is present (as it is in recent versions of the Dataverse client), that method is invoked directly; otherwise the call is wrapped with Task.Run to avoid blocking calling threads. Consumers write await dv.CreateAsync(...) and the helper takes care of choosing the most efficient path.

Supported operations
  • Create, Update, Delete (cast to Guid/void)
  • Generic POCO overloads: Create<T>, Update<T>, Retrieve<T> etc. that convert between CLR objects and Entity instances using EntityMapper.
  • Retrieve / RetrieveMultiple (returns Entity / EntityCollection)
  • Execute<T> generic wrapper for any OrganizationRequest
  • Upsert – create or update based on presence of Id or EntityReference
  • Full async counterparts (CreateAsync, UpdateAsync, RetrieveAsync, DeleteAsync, RetrieveMultipleAsync, ExecuteAsync<T>, …) which favour true async SDK methods when available

Retry logic is handled by RetryHelper with default parameters: 3 tries, exponential backoff starting at 1 second, ignoring OrganizationServiceFaults where the error code indicates a lock or timeout. You can also pass your own RetryPolicy object if you need custom behaviour.

var dv = new DataverseService(service);

// create new record
var newAccount = new Entity("account") { ["name"] = "Contoso" };
var createdRef = dv.Create(newAccount);

// upsert existing record (Id present)
existingAccount["name"] = "Updated";
dv.Upsert(existingAccount);

// retrieve with retry
var q = QueryHelper.New("contact").WithColumns("emailaddress1");
var contacts = dv.RetrieveMultiple(q);

// execute arbitrary request
var whoami = dv.Execute<WhoAmIResponse>(new WhoAmIRequest());
Console.WriteLine(whoami.UserId);

// async
await dv.DeleteAsync("account", someGuid);

All responses are returned directly; any communication faults bubble up as OrganizationServiceFault or InvalidOperationException after retry exhaustion.

QueryHelper

A small fluent DSL for constructing QueryExpression objects. It was designed to remove repetitive boilerplate such as new ConditionExpression(...) and to provide chainable methods that read like English.

Notable methods
  • New(string entityName, bool allColumns = false) – start a query; set ColumnSet to all columns when allColumns is true. Note: some binary/blob attributes such as entityimage are not returned even when allColumns is true; you must request them explicitly (see WithEntityImage or WithBinaryFields).
  • .WithColumns(params string[] cols) – specify explicit columns
  • .WithEntityImage() – convenience helper that adds the entityimage column (required for retrieving the primary image or photo blob)
  • .WithBinaryFields(params string[] names) – generic equivalent of WithEntityImage for any blob/large field (e.g. entityimage, documentbody, custom file attributes, etc.)
  • .WithColumnsFrom<T>() – automatically add columns corresponding to the public properties of the POCO type T; uses [AttributeLogicalName] if present and lower‑cases property names otherwise, so you don’t have to manually repeat the list of columns. The Id property is ignored (Dataverse exposes it separately as entity.Id), and any collection/List properties (e.g. your child‑entity lists) are skipped so you don’t accidentally request them. You can also call the overload that accepts additional column names:
    q.WithColumnsFrom<AlumniResponse>("emailaddress1","customfield");
    
    to fetch fields that aren’t defined on the POCO.

Convenience query helpers

DataverseService now offers RetrieveSingle<T> / RetrieveSingleAsync<T> helpers that run a QueryExpression, grab the first matching record and map it to your POCO using EntityMapper. These methods throw an InvalidOperationException if the query returns no results, making them handy for "get by key" or single‑result lookups without boilerplate.

var q = QueryHelper.New("contact")
            .WithColumnsFrom<AlumniResponse>()
            .WithEntityImage()              // include binary field
            .AddFilter(/* same filter as before */);

// synchronous
var alumni = dv.RetrieveSingle<AlumniResponse>(q, (ent, attr) =>
{
    var lookup = ent.Get<EntityReference>(attr);
    if (lookup != null && !string.IsNullOrEmpty(lookup.Name))
        return lookup.Name;
    return ent.GetOptionSetLabel(attr);
});

// async
var alumni2 = await dv.RetrieveSingleAsync<AlumniResponse>(q, resolver);

This removes the need for .RetrieveMultiple(...) followed by .Select(...).First() in simple scenarios.

If you need to retrieve related records in the same query you can add one or more LinkEntity objects. The QueryHelper.AddLink helper makes this easier by creating the link and returning it so you can add filters or columns on the linked side.

var q = QueryHelper.New("contact")
            .WithColumnsFrom<AlumniResponse>()
            .AddFilter(QueryHelper.Eq("statuscode", 1));

// link to account via the contact lookup field
var link = q.AddLink("account", "contactid", "primarycontactid",
                     JoinOperator.LeftOuter);
link.EntityAlias = "acct";
link.Columns = new ColumnSet("name","accountnumber");
link.LinkCriteria.AddCondition(QueryHelper.Eq("statecode", 0));

var results = dv.RetrieveMultiple(q);
// access account name with an alias:
var accountName = (string)
    results.Entities[0].GetAttributeValue<AliasedValue>("acct.name").Value;

You can chain links arbitrarily to traverse many‑to‑one, one‑to‑many or N‑to‑N relationships (for N‑to‑N simply link through the intersect entity).

Auto‑selecting child columns

Just as you can use WithColumnsFrom<T>() on the parent query, the same concept is available on links: call WithColumnsFrom<TChild>() on a LinkEntity to automatically pick the child attributes based on the properties of the child POCO. This saves you from having to list every qualification field manually.

var link = q.AddLink("mannai_qualification", "contactid", "mannai_alumni");
link.EntityAlias = "qualification";
link.WithColumnsFrom<QualificationResponse>();

An overload accepting extra column names is also available if you need a few additional fields that don’t exist on the POCO.

  • .AddFilter(FilterExpression filter) – append an existing filter (e.g. for reuse)
  • .Eq, .Neq, .Gt, .Lt, .In, .Between static helpers returning ConditionExpression
  • .OrderBy(string attr, OrderType) / .OrderByAsc / .OrderByDesc
  • .Page(int number, int count) helper that sets PageInfo, useful for paging loops
  • Extension methods on QueryExpression to merge filters or copy criteria from another query
// basic select
var q = QueryHelper.New("account")
            .WithColumns("name","accountnumber")
            .AddFilter(new FilterExpression(LogicalOperator.And)
            {
                Conditions =
                {
                    QueryHelper.Eq("statuscode", 1),
                    QueryHelper.In("industrycode", 1, 3, 5)
                }
            })
            .OrderByDesc("createdon");

var accounts = service.RetrieveMultiple(q);

// paging using loop
q.PageInfo = new PagingInfo { Count = 500, PageNumber = 1 };
while (true)
{
    var page = service.RetrieveMultiple(q);
    foreach (var a in page.Entities) { /* process */ }
    if (!page.MoreRecords) break;
    q.PageInfo.PageNumber++;
    q.PageInfo.PagingCookie = page.PagingCookie;
}

The helper can also produce FetchExpression objects via the ToFetchXml() extension, but for complex queries you may want to use FetchXmlBuilder instead.

// create a service (from config or elsewhere)
var service = CrmServiceFactory.CreateService(connString);

// 1. simple retrieve all accounts ordered by name
var q = QueryHelper.New("account", allColumns: true)
              .OrderBy("name");
var results = service.RetrieveMultiple(q);

// 2. add a filter with helpers (equivalent to hand‑coded query)
q = QueryHelper.New("contact")
            .WithColumns("firstname", "lastname", "emailaddress1")
            .AddFilter(new FilterExpression(LogicalOperator.And)
            {
                Conditions =
                {
                    QueryHelper.Eq("statecode", 0),          // active
                    QueryHelper.In("gendercode", 1, 2)
                }
            })
            .OrderBy("lastname", OrderType.Ascending);
var contacts = service.RetrieveMultiple(q);

// 3. build same query manually without helpers (for comparison)
var manual = new QueryExpression("opportunity")
{
    ColumnSet = new ColumnSet("name", "estimatedvalue"),
    Criteria =
    {
        Conditions =
        {
            new ConditionExpression("statuscode", ConditionOperator.Equal, 1)
        }
    },
    Orders =
    {
        new OrderExpression("createdon", OrderType.Descending)
    }
};
var opps = service.RetrieveMultiple(manual);

Each RetrieveMultiple call returns an EntityCollection that you can iterate or convert to your own type. To use the DataverseService wrapper with retry and async support, do:

var dv = new DataverseService(service);

// synchronous usage
var coll = dv.RetrieveMultiple(q);

// asynchronous call — the helper will automatically prefer a real
// `RetrieveMultipleAsync` implementation if the underlying service exposes one
var collAsync = await dv.RetrieveMultipleAsync(q);

Generic & async examples

The library provides generic POCO converters and async helpers to keep your code clean:

public class AccountDto { public Guid Id { get; set; } public string? Name { get; set; } }

// generic retrieve
var account = await dv.RetrieveAsync<AccountDto>("account", id, new ColumnSet("name"));
Console.WriteLine(account.Name);

// update via POCO and async
account.Name = "New Name";
await dv.UpdateAsync(account, "account");

EntityExtensions

This class is your one‑stop shop for safely reading and writing attributes on a Microsoft.Xrm.Sdk.Entity. It abstracts the dirty work of checking Contains, casing the value, handling nulls and formatted values, and even converting OptionSetValue/EntityReference/EntityReferenceCollection for you.

Each helper is a tiny extension, so you can write concise, readable code instead of repeating the same boilerplate everywhere.

Basic getters
string?   name   = entity.GetString("name");          // null if missing
int?      qty    = entity.GetInt("quantity");
decimal?  rev    = entity.GetDecimal("revenue");
bool?     active = entity.GetBool("statecode");
Guid?     id     = entity.GetGuid("primarycontactid");

// generic form (useful in loops or generic code)
var x = entity.Get<T>("someattribute");
Lookups
Guid pkid   = entity.GetLookupId("primarycontactid");          // Guid.Empty if missing
string? ln  = entity.GetLookupLogicalName("primarycontactid"); // e.g. "contact"
string? nm  = entity.GetLookupName("primarycontactid");        // the referenced record's name

// raw EntityReference if you need it
var er = entity.Get<EntityReference>("primarycontactid");
Option sets / status codes
// raw OptionSetValue object or null
var osv = entity.GetOptionSet("industrycode");

// numeric value, throws if not present
int code = entity.GetOptionSetValue("industrycode");

// formatted (human) label, falls back to number
string? label = entity.GetOptionSetLabel("industrycode");
Misc helpers
// check existence without throwing
if (entity.Has("statuscode")) { }

// track changes between images
if (entity.HasChanged("name")) { /* build audit entry */ }
if (entity.HasChanged(newImage, "name")) { /* compare two images */ }

// deal with joined/aliased fields
decimal? credit = entity.GetAliasedValue<decimal>("c", "creditlimit");

// helper methods ported from toolkit
entity.SetAttributeValue("description", "text"); // skip nulls
var defaultVal = entity.GetAttributeValueOrDefault<bool>("merged");

// merge attributes from another entity (ignores nulls)
entity.Merge(otherEntity);

Tip: take a quick look at the source (Extensions/EntityExtensions.cs) – it lists every method along with comments. The implementations are intentionally lightweight, making them safe to use in plugins, workflows, or Web API code.

Using these helpers keeps your Dataverse business logic clean, expressive, and free from repetitive if (entity.Contains(...) && entity["x"] is T t) blocks.

RetryHelper

A generic retry engine you can use wherever transient faults might occur. It is intentionally dependency‑free (no logging) and returns whatever the delegate returns, retrying according to a basic exponential backoff policy.

// simple use
RetryHelper.Run(() => service.Create(entity));

// with return value
int count = RetryHelper.Run(() => repository.GetCount());

// customise number of attempts / delay
var result = RetryHelper.Run(() => api.Call(), attempts:5, initialDelayMs:200);

// async
await RetryHelper.RunAsync(() => httpClient.GetStringAsync(url));

By default the helper swallows Exception and retries; to target specific exception types use the overload accepting Func<Exception,bool> shouldRetry.

Logging/Diagnostics Helper

A very lightweight utility that wraps the SDK's Execute method to emit pre‑ and post‑call messages. It was added to make it easy to trace single requests without needing a full logging framework.

Methods:

  • ExecuteWithLogging(this IOrganizationService, OrganizationRequest, Action<string> log) – returns OrganizationResponse
  • ExecuteWithLogging<T>(this IOrganizationService, OrganizationRequest, Action<string> log) – generic variant returning T where T : OrganizationResponse

Logging callback is invoked twice: once immediately before the request with the request name and parameter list, and once after completion with the response type. Example use cases:

// simple console tracing
svc.ExecuteWithLogging(new WhoAmIRequest(), Console.WriteLine);

// capture in a list for assertions in unit tests
var msgs = new List<string>();
svc.ExecuteWithLogging(new RetrieveEntityRequest { /*...*/ }, msgs.Add);
Assert.Contains("Request: RetrieveEntity", msgs[0]);

// integrate with Microsoft.Extensions.Logging
svc.ExecuteWithLogging(req, msg => logger.LogInformation(msg));

You can easily extend the helper to track performance, dump parameters to JSON, or filter out sensitive values.

Extending this further is trivial – replace the delegate with an ILogger from Microsoft.Extensions.Logging or whatever your app uses.

EntityMapper (POCO converters)

Converts between CRM Entity objects and your own plain-CLR classes. It comprises two static methods:

  • ToPoco<T>(Entity) – inspects properties of T, uses [AttributeLogicalName] if present (otherwise property name lower‑cased), and copies values from the entity. The special Id property is set from entity.Id. Type mismatches are silently ignored.
    • OptionSetValue attributes map automatically to int, string, OptionSetValue or any enum type.
    • You can now also use KeyValuePair<int,string> or a (int Value,string Label) tuple – the mapper will set the key/item1 from the numeric option value and the label/item2 to the same value as a string (see below for a resolver that can return the real label).
    • Lookup fields map to Guid, string or EntityReference.
    • Multi‑lookup/N:N relationships (EntityReferenceCollection) can populate List<EntityReference> or List<Guid>.
    • To receive data you must decorate properties with the correct logical name when it differs from the CLR name (e.g. [AttributeLogicalName("statuscode")]).

  • ToEntity(object poco, string logicalName) – creates an Entity of the given logical name and maps all non-null public properties. Id is not copied; set it manually if needed.
    • Primitive ints become OptionSetValue; enums are converted to their numeric value.
    • Guids/strings that parse as GUIDs become EntityReference.
    • Lists of Guid or EntityReference are transformed into an EntityReferenceCollection for multi‑lookup fields.

These helpers are extremely useful when building Web API endpoints or background jobs: you can define DTOs/POCOs with typed properties and automatically translate to/from CRM entities.

// simple example
public class AccountDto { public Guid Id { get; set; } public string? Name { get; set; } }
var dto = EntityMapper.ToPoco<AccountDto>(entity);
var update = EntityMapper.ToEntity(dto, "account");
service.Update(update);
// using KeyValuePair/tuple for option set fields
public class FooDto
{
    public Guid Id { get; set; }
    [AttributeLogicalName("familystatuscode")]
    public KeyValuePair<int,string> FamilyStatus { get; set; }
    [AttributeLogicalName("statuscode")]
    public (int Value,string Label) CaseStatus { get; set; }
}

// lookups can be handled the same way (ID + name)
public class BarDto
{
    public Guid Id { get; set; }
    [AttributeLogicalName("parentcustomerid")]
    public KeyValuePair<Guid,string> Parent { get; set; }
}

// basic mapping – label defaults to the numeric text
var dto2 = EntityMapper.ToPoco<FooDto>(entity);
// dto2.FamilyStatus.Key -> 1 (option value)
// dto2.FamilyStatus.Value -> "1" (stringified value)
// dto2.CaseStatus.Value -> 1
// dto2.CaseStatus.Label -> "1"

// supply a resolver to obtain the real display name from metadata
var dto3 = EntityMapper.ToPoco<FooDto>(entity, (ent, attr) => ent.GetOptionSetLabel(attr));
// now dto3.FamilyStatus.Value == "Married" (if the metadata says so)

// converting back ignores the label and uses the numeric portion
var ent2 = EntityMapper.ToEntity(dto3, "foo");
// ent2["familystatuscode"] is OptionSetValue(1)

Option‑sets are handled transparently.

Image/binary fields – attributes such as entityimage are returned as a byte array by the SDK. EntityMapper now supports mapping these blobs to a string property (base‑64 encoded) or directly to a byte[] property. The feature was added to make it easy to work with profile pictures, web resources, etc.; simply declare the POCO property with [AttributeLogicalName("entityimage")] and the mapper does the rest.

// imagine 'gendercode' is an optionset field in contact
public enum Gender { Male = 1, Female = 2, Unknown = 3 }
public class ContactDto
{
    public Guid Id { get; set; }
    [AttributeLogicalName("gendercode")]
    public Gender Gender { get; set; }      // enum property
}

var contact = service.Retrieve("contact", id, new ColumnSet("gendercode"));
var dto2 = EntityMapper.ToPoco<ContactDto>(contact);
// dto2.Gender will be the equivalent enum value (or default)

dto2.Gender = Gender.Female;
```var upd = EntityMapper.ToEntity(dto2, "contact");
// upd["gendercode"] is now an OptionSetValue(2)

You can also map into an int or string property instead of an enum if you prefer – the mapper simply converts the underlying OptionSetValue.Value.

### DataverseWebApiClient

A small HTTP client abstraction that wraps the Dataverse Web API. It is useful when you want to avoid the heavy SDK or when running in environments where the SDK cannot be used (e.g. Linux containers).

Features:

- Builds request URLs from the base organisation URL and entity set names
- Serialises/deserialises JSON using `Newtonsoft.Json`
- Supports `GET`, `POST`, `PATCH`, `DELETE` methods and can send `WebApiQueryOptions` (e.g. `$filter`, `$select`)
- Handles authentication via `CrmServiceClient.AuthenticationType` tokens when provided, or accepts a bearer token string

Example:

```csharp
var client = new DataverseWebApiClient(orgUrl, authToken);

// retrieve accounts with filter
var json = await client.GetAsync("accounts?$select=name,accountnumber&$filter=statecode eq 0");
var jArr = JArray.Parse(json);

// create record
var acct = new JObject { ["name"] = "WebAPI Test" };
await client.PostAsync("accounts", acct.ToString());

// update using PATCH
await client.PatchAsync("accounts(00000000-0000-0000-0000-000000000000)",
    "{ \"telephone1\": \"555-1234\" }");

Because it deals only with raw JSON strings, it pairs nicely with JsonExtensions for converting to/from Entity/EntityCollection when needed.

Helper Library Reference

Below is a catalogue of the helper/utility classes included in the library. Each entry includes a short description and example usage where appropriate. Read through this section to understand what the library offers; everything is already implemented and covered by unit tests.

FetchXmlBuilder

Fluent builder for FetchXML queries. Supports selecting attributes, adding filters, ordering, paging, aggregates, grouping and link-entity joins with nested filters.

var xml = new FetchXmlBuilder("account")
    .Select("name")
    .Where("statecode","eq",0)
    .OrderBy("name")
    .Page(1,100)
    .Aggregate("revenue","sum","totalRev")
    .GroupBy("businessunitid")
    .LinkEntity("contact","accountid","parentcustomerid","c")
        .Where("emailaddress1","not-null",null)
        .Select("contactid")
        .Aggregate("contactid","count","cnt")
        .EndLink()
    .ToFetchXml();

The produced string can be passed directly to new FetchExpression(xml).

PagingHelper

Iterates over large result sets by advancing the PageInfo automatically. You supply an IOrganizationService and a QueryExpression; the method yields every Entity returned by the server.

foreach(var ent in PagingHelper.RetrieveAll(service, query))
{
    // process
}

Also provides RetrieveAllAsync for asynchronous enumeration.

OptionSetHelper

This helper simplifies working with picklists/option sets. You can create OptionSetValue instances, extract values/labels from entities, and – new in the latest version – read the entire option set definition from metadata.

var opt = OptionSetHelper.Create(3);
int? val = OptionSetHelper.GetValue(opt);    // 3
string? lbl = OptionSetHelper.GetLabel(entity, "statuscode");

To obtain the list of allowable values (for example when populating a drop‑down on a UI), call GetOptions with a service reference:

var options = OptionSetHelper.GetOptions(service, "contact", "gendercode");
// result is IList<KeyValuePair<int,string>>: 1=>"Male", 2=>"Female", …

// global option set (no entity required):
var globals = OptionSetHelper.GetOptions(service, "new_colors");
// e.g. 10=>"Red", 20=>"Blue"

The method handles both local and global option sets by querying the attribute metadata via RetrieveAttributeRequest and returning the numeric values along with their user‑localized labels.

Convenience for working with option set values and labels without manually hitting metadata.

var opt = OptionSetHelper.Create(100000000);
var value = OptionSetHelper.GetValue(opt);
var label = OptionSetHelper.GetLabel(opt);

BatchHelper

Simplifies ExecuteMultiple operations with automatic chunking and error reporting.

var resp = BatchHelper.Execute(service, new CreateRequest{Target=new Entity("contact")});
if(resp.IsFaulted) { /* iterate resp.Responses for exceptions */ }

JsonExtensions

Extension methods to convert between Entity/EntityCollection and JSON strings. Useful for logging or serializing results to clients.

string json = entity.ToJson();
Entity e2 = json.FromJson<Entity>();

CacheHelper

In-memory cache for cross-request reuse. Stores arbitrary objects with optional expiration.

CacheHelper.Set("key", obj, 5);
var obj2 = CacheHelper.Get("key");

MetadataHelper

Wraps common metadata retrievals and caches results internally.

string label = service.GetAttributeDisplayName("contact","firstname");
var entityMeta = MetadataHelper.GetEntityMetadata(service, "account");

AnnotationHelper

Helpers for attaching and retrieving note (annotation) records.

AnnotationHelper.AttachAnnotation(service, someRef, null, "note text");
var notes = AnnotationHelper.GetNotes(service, someRef);

DateTimeHelper

Utility methods for converting between UTC and local times and for working with CRM's date/time kinds.

var utc = DateTimeHelper.ToUtc(localTime, timeZoneCode);

EntityMapper

Convert between POCOs and Entity instances. Detailed earlier in Core Components section.

SecurityHelper (placeholder)

A lightweight class with methods such as UserHasRole, ImpersonateUser etc. (stubbed for now; extend as needed).

LoggingHelper

Described previously under Core Components; wraps OrganizationRequest execution with logging callbacks.

DataverseWebApiClient

HTTP client wrapper for the Dataverse Web API described earlier.


Usage Examples

(See the previous section for quick snippets; the extended document above includes them as well.)

Sample Application

The Samples/DataverseWebApiSample project demonstrates how to wire the helpers into an ASP.NET Core Web API with dependency injection and configuration. It includes:

  • Startup.cs registering CrmServiceFactory and DataverseService as singleton services.
  • Controller actions for basic CRUD operations (GET /accounts, POST /contacts, etc.).
  • Example of using QueryHelper, EntityMapper, and LoggingHelper within controller methods.
  • appsettings.json with a placeholder for the Dataverse connection string.

To run the sample:

cd Samples/DataverseWebApiSample
# set your connection string in appsettings.json or via environment variable
dotnet run

Use a tool such as Postman or curl to hit the API endpoints and observe how the helpers reduce the amount of code you need to write.

The sample is intentionally minimal; feel free to copy it into your own solution as a starting point.

Additional helpers

// FetchXmlBuilder (now with paging, aggregates, group-by and link filters)
var fetch = new FetchXmlBuilder("account")
                .Select("name", "accountid")
                .Where("statecode", "eq", 0)
                .OrderBy("name")
                .Page(1, 100)                          // fetch first 100
                .Aggregate("revenue", "sum", "totalRev")
                .GroupBy("businessunitid")
                .LinkEntity("contact","accountid","parentcustomerid", alias: "c", linkType: "inner")
                    .Where("emailaddress1", "not-null", null) // filter on linked entity
                    .Select("contactid")
                    .EndLink();
var coll = service.RetrieveMultiple(fetch.ToFetchExpression());
// Logging
var logMsgs = new List<string>();
service.ExecuteWithLogging(new OrganizationRequest("WhoAmI"), logMsgs.Add);
// logMsgs now contains two messages: request and response

// Upsert via DataverseService var dv = new DataverseService(service); var acct = new Entity("account") { ["name"] = "New" }; var upsertRef = dv.Upsert(acct); // returns EntityReference to created/updated record

// OptionSetHelper var option = OptionSetHelper.Create(3); var val = OptionSetHelper.GetValue(option); // 3 var lbl = OptionSetHelper.GetLabel(option); // null until retrieved via metadata

// BatchHelper var create = new CreateRequest { Target = new Entity("contact") { ["firstname"] = "Alice" } }; var batchResp = BatchHelper.Execute(service, create); if (batchResp.IsFaulted) { // inspect batchResp.Responses for errors }

// JsonExtensions var json = entity.ToJson(); var listJson = coll.ToJson();

// CacheHelper CacheHelper.Set("xyz", someObj, expirationMinutes:5); var cached = CacheHelper.Get("xyz");

// MetadataHelper var label = service.GetAttributeDisplayName("contact","firstname");

// AnnotationHelper AnnotationHelper.AttachAnnotation(service, new EntityReference("account", acct.Id), noteText: "hello");

// EntityMapper (POCO conversion) public class AccountDto { public Guid Id { get; set; } public string? Name { get; set; } } var dto = EntityMapper.ToPoco<AccountDto>(entity); var newEntity = EntityMapper.ToEntity(dto, "account");

cd MWTech.Dataverse.Helpers.Tests
dotnet test

Testing and Quality

All helper classes are covered by unit tests in the MWTech.Dataverse.Helpers.Tests project. These tests use lightweight fake IOrganizationService implementations so they execute quickly without requiring a live Dataverse environment. When extending the library you should:

  1. Add at least one new test file (same name as the class under test) or augment existing files.
  2. Use the provided FakeService patterns to simulate common server responses such as multiple pages, aggregate results, or fault conditions.
  3. Run dotnet test locally and on your CI pipeline; the build definition already runs the tests and fails the build on any test failure.

Tests also serve as additional documentation: you can open them to see exactly how each helper behaves and how edge cases are handled.

If you need code coverage metrics, tools like coverlet or Visual Studio's built-in coverage can be used. Coverage reports are not checked into source control but can be generated as part of automated builds.

Packaging & Distribution

A NuGet package is produced during build (see earlier section). Use the provided PowerShell snippet or integrate into your CI pipeline for automatic versioning/publishing.

Extending the Library

This codebase is intentionally small and modular. Add new helpers under appropriate folders:

  • Core/ for service wrappers
  • Helpers/ for algorithmic utilities
  • Extensions/ for entity/tracing extensions
  • WebApi/ for REST clients

Follow the existing naming and documentation style, add tests, and update this README accordingly.

Other helpers & ideas

Over time you may find it useful to add additional building blocks. Some common helpers we (or you) might add:

  • FetchXmlBuilder – fluent helpers for constructing FetchXML queries with filters, joins, aggregate, paging and ordering. Many developers still prefer FetchXML for advanced queries.
  • PagingHelper – convenience methods for looping through large result sets using PagingInfo or the Web API @odata.nextLink pattern.
  • MetadataHelper – cache and simplify calls to RetrieveEntityRequest, RetrieveAttributeRequest, option set lookups, relationship metadata, etc.
  • OptionSetHelper – read/write optionset label/value pairs, translate between text and int.
  • BatchHelper / ExecuteMultiple – group create/update/delete operations with automatic chunking and error summarization.
  • EntityMapper – convert between CRM Entity instances and your own POCO classes. It honors [AttributeLogicalName], copies the Id property, and ignores null/mismatched values.
  • SecurityHelper – check user roles, teams, field‐level security, or build impersonation wrappers.
  • JsonExtensions – convert Entity/EntityCollection to/from JSON, handy for logging or Web API responses.
  • DateTimeHelper – handle time zone conversions, UTC/local, cruising around DateTime.SpecifyKind headaches.
  • ServiceDiagnostics – trace request/response, performance timers or log to ITracingService.
  • Plugin/Workflow helper – although this lives in another toolkit, you could borrow patterns for external apps (e.g. PluginExecutionContext wrappers).

Feel free to contribute any of these ideas (or your own) – the goal is to keep the core lean while providing reusable patterns that make working with Dataverse simpler.

Target Frameworks

The library is built for:

  • net10.0 – for modern .NET applications
  • netstandard2.0 – so older projects (including .NET Framework plugins) can reference it

When the time comes, you can add additional targets (net8.0, etc.) by editing the project file.


MWTech.Dataverse.Helpers aims to be the easiest way to connect .NET code to Dataverse. We hope this documentation makes adoption simple for any developer on the team. Contributions and suggestions are welcome!


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 is compatible.  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 is compatible.  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 netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen 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.0.0.15 107 2/28/2026
1.0.0.14 100 2/28/2026
1.0.0.13 101 2/28/2026
1.0.0.12 101 2/28/2026
1.0.0.11 102 2/28/2026
1.0.0.10 103 2/28/2026
1.0.0.9 104 2/28/2026
1.0.0.8 104 2/28/2026
1.0.0.7 100 2/28/2026
1.0.0.6 100 2/28/2026
1.0.0.5 108 2/28/2026
1.0.0.4 110 2/26/2026
1.0.0.3 107 2/25/2026
1.0.0.2 106 2/25/2026
1.0.0.1 106 2/25/2026
1.0.0 111 2/25/2026