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
<PackageReference Include="MWTech.Dotnet.Dataverse.Helpers" Version="1.0.0.15" />
<PackageVersion Include="MWTech.Dotnet.Dataverse.Helpers" Version="1.0.0.15" />
<PackageReference Include="MWTech.Dotnet.Dataverse.Helpers" />
paket add MWTech.Dotnet.Dataverse.Helpers --version 1.0.0.15
#r "nuget: MWTech.Dotnet.Dataverse.Helpers, 1.0.0.15"
#:package MWTech.Dotnet.Dataverse.Helpers@1.0.0.15
#addin nuget:?package=MWTech.Dotnet.Dataverse.Helpers&version=1.0.0.15
#tool nuget:?package=MWTech.Dotnet.Dataverse.Helpers&version=1.0.0.15
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/Attributeplumbing - encapsulate retry/cancellation/diagnostics
- provide both sync and async‑first APIs (detects underlying
*Asyncmethods 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.Runwhen not found)- generic POCO overloads for CRUD operations
- multi-targeting to
net10.0,net8.0andnetstandard2.1- Roslyn/StyleCop analyzers, XML doc output,
.editorconfigand tests shipped- companion xUnit test project included in repository
- updated to the latest
Microsoft.PowerPlatform.Dataverse.Clientpackage
Table of Contents
- MWTech.Dataverse.Helpers
- Quick start
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 CRMorDataversedelegated 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
InvalidOperationExceptionwith provider-specific error details if the string is malformed or the server is unreachable. - Supports both OAuth and username/password strings.
- Automatically caches the
CrmServiceClientfor 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
*Asyncmethod 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 withTask.Runto avoid blocking calling threads. Consumers writeawait dv.CreateAsync(...)and the helper takes care of choosing the most efficient path.
Supported operations
Create,Update,Delete(cast toGuid/void)- Generic POCO overloads:
Create<T>,Update<T>,Retrieve<T>etc. that convert between CLR objects andEntityinstances usingEntityMapper. Retrieve/RetrieveMultiple(returnsEntity/EntityCollection)Execute<T>generic wrapper for anyOrganizationRequestUpsert– create or update based on presence ofIdorEntityReference- 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; setColumnSetto all columns whenallColumnsis true. Note: some binary/blob attributes such asentityimageare not returned even whenallColumnsis true; you must request them explicitly (seeWithEntityImageorWithBinaryFields)..WithColumns(params string[] cols)– specify explicit columns.WithEntityImage()– convenience helper that adds theentityimagecolumn (required for retrieving the primary image or photo blob).WithBinaryFields(params string[] names)– generic equivalent ofWithEntityImagefor 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 typeT; uses[AttributeLogicalName]if present and lower‑cases property names otherwise, so you don’t have to manually repeat the list of columns. TheIdproperty is ignored (Dataverse exposes it separately asentity.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:
to fetch fields that aren’t defined on the POCO.q.WithColumnsFrom<AlumniResponse>("emailaddress1","customfield");
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.
Joining related entities
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,.Betweenstatic helpers returningConditionExpression.OrderBy(string attr, OrderType)/.OrderByAsc/.OrderByDesc.Page(int number, int count)helper that setsPageInfo, useful for paging loops- Extension methods on
QueryExpressionto 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)– returnsOrganizationResponseExecuteWithLogging<T>(this IOrganizationService, OrganizationRequest, Action<string> log)– generic variant returningTwhereT : 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 ofT, uses[AttributeLogicalName]if present (otherwise property name lower‑cased), and copies values from the entity. The specialIdproperty is set fromentity.Id. Type mismatches are silently ignored.
• OptionSetValue attributes map automatically toint,string,OptionSetValueor any enum type.
• You can now also useKeyValuePair<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 toGuid,stringorEntityReference.
• Multi‑lookup/N:N relationships (EntityReferenceCollection) can populateList<EntityReference>orList<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 anEntityof the given logical name and maps all non-null public properties.Idis not copied; set it manually if needed.
• Primitive ints becomeOptionSetValue; enums are converted to their numeric value.
• Guids/strings that parse as GUIDs becomeEntityReference.
• Lists ofGuidorEntityReferenceare transformed into anEntityReferenceCollectionfor 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
entityimageare returned as a byte array by the SDK.EntityMappernow supports mapping these blobs to astringproperty (base‑64 encoded) or directly to abyte[]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.csregisteringCrmServiceFactoryandDataverseServiceas singleton services.- Controller actions for basic CRUD operations (
GET /accounts,POST /contacts, etc.). - Example of using
QueryHelper,EntityMapper, andLoggingHelperwithin controller methods. appsettings.jsonwith 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:
- Add at least one new test file (same name as the class under test) or augment existing files.
- Use the provided
FakeServicepatterns to simulate common server responses such as multiple pages, aggregate results, or fault conditions. - Run
dotnet testlocally 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 wrappersHelpers/for algorithmic utilitiesExtensions/for entity/tracing extensionsWebApi/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
PagingInfoor the Web API@odata.nextLinkpattern. - 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
Entityinstances and your own POCO classes. It honors[AttributeLogicalName], copies theIdproperty, and ignores null/mismatched values. - SecurityHelper – check user roles, teams, field‐level security, or build impersonation wrappers.
- JsonExtensions – convert
Entity/EntityCollectionto/from JSON, handy for logging or Web API responses. - DateTimeHelper – handle time zone conversions, UTC/local, cruising around
DateTime.SpecifyKindheadaches. - 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.
PluginExecutionContextwrappers).
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 | Versions 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. |
-
.NETStandard 2.1
- Microsoft.PowerPlatform.Dataverse.Client (>= 1.2.10)
- System.Formats.Asn1 (>= 10.0.3)
-
net10.0
- Microsoft.PowerPlatform.Dataverse.Client (>= 1.2.10)
-
net8.0
- Microsoft.PowerPlatform.Dataverse.Client (>= 1.2.10)
- System.Formats.Asn1 (>= 10.0.3)
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 |