An in-memory fake IOrganizationService and IOrganizationServiceAsync2 for unit testing Dataverse / Dynamics 365 applications — no live connection required.
| Category | Capabilities |
|---|---|
| CRUD | Create, Retrieve, Update, Delete with auto-set fields (timestamps, owner, state, version number) |
| Service interfaces | IOrganizationService and IOrganizationServiceAsync2 (async + cancellation token support) |
| QueryExpression | Filtering (40+ condition operators), ordering, column projection, TopCount, paging with cookies |
| FetchXml | FetchXml parsing and in-memory evaluation for the supported query surface, including aggregation (Count, Sum, Avg, Min, Max, GroupBy) |
| LinkEntity | Inner/outer joins, nested joins, semi/anti joins (exists, in, any, not-any, not-all), link criteria, aliased attributes |
| Pipeline | Pre-validation, pre-operation, and post-operation hooks (plugin-like) |
| Metadata | Entity/attribute metadata store, validation on Create/Update, auto-discovery |
| Security | Security roles, privilege enforcement, record sharing (Grant/Modify/Revoke access) |
| Execute handlers | WhoAmI, SetState, Assign, Upsert, FetchXml/QueryExpression conversion requests, ExecuteMultiple, ExecuteTransaction, and more |
| Concurrency | Optimistic concurrency (ConcurrencyBehavior.IfRowVersionMatches), atomic transactions with undo-log rollback |
| Calculated fields | Calculated and rollup field definitions evaluated on Retrieve |
| Currency | Exchange rates and auto-computed base currency amounts |
| Activity parties | ActivityParty entity support for from/to/cc/bcc fields |
| Binary attributes | Image and file column storage |
| Seeding | Bulk insert, JSON seeding, EntityBuilder fluent API |
| Snapshots | TakeSnapshot() / RestoreSnapshot() / Scope() for test isolation |
| Time control | FakeClock for deterministic date/time testing |
| Operation log | Records all service calls for post-hoc assertions |
| Configuration | FakeOrganizationServiceOptions with Strict/Lenient presets |
| Multi-user | Multiple FakeOrganizationService sessions against the same FakeDataverseEnvironment |
| Multi-target | .NET Framework 4.6.2 and .NET 10 |
Built-in query conversion handlers are registered automatically:
FetchXmlToQueryExpressionRequest converts supported non-aggregate FetchXml, and
QueryExpressionToFetchXmlRequest serializes the supported QueryExpression
surface without silently degrading unsupported operators or join types.
dotnet add package Fake4Dataverse
var env = new FakeDataverseEnvironment();
var service = env.CreateOrganizationService();
var id = service.Create(new Entity("account") { ["name"] = "Contoso" });
var retrieved = service.Retrieve("account", id, new ColumnSet("name"));
Assert.Equal("Contoso", retrieved["name"]);
service.Update(new Entity("account") { Id = id, ["name"] = "Contoso Ltd." });
service.Delete("account", id);using Microsoft.PowerPlatform.Dataverse.Client;
using System.Threading;
var env = new FakeDataverseEnvironment();
var service = env.CreateOrganizationService();
var asyncService = (IOrganizationServiceAsync2)service;
var id = await asyncService.CreateAsync(
new Entity("account") { ["name"] = "Contoso" },
CancellationToken.None);
var account = await asyncService.RetrieveAsync(
"account", id, new ColumnSet("name"),
CancellationToken.None);[Fact]
public void Create_ExecutesRegisteredPlugin()
{
var env = new FakeDataverseEnvironment();
var service = env.CreateOrganizationService();
env.Pipeline.RegisterStep("Create", PipelineStage.PreOperation, "account", new PrefixNamePlugin());
var id = service.Create(new Entity("account") { ["name"] = "Contoso" });
var account = service.Retrieve("account", id, new ColumnSet("name"));
Assert.Equal("PLUGIN: Contoso", account.GetAttributeValue<string>("name"));
}
private sealed class PrefixNamePlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext))!;
var target = (Entity)context.InputParameters["Target"];
target["name"] = $"PLUGIN: {target.GetAttributeValue<string>("name")}";
}
}FakeOrganizationServiceOptions controls auto-set behaviors, validation, security, and pipeline.
Use built-in presets for common setups:
var strict = new FakeDataverseEnvironment(FakeOrganizationServiceOptions.Strict); // metadata validation + security on
var lenient = new FakeDataverseEnvironment(FakeOrganizationServiceOptions.Lenient); // everything off — full manual control
var strictService = strict.CreateOrganizationService();
var lenientService = lenient.CreateOrganizationService();See the Configuration Reference for all options.
| Package | Description |
|---|---|
| Fake4Dataverse.FakeItEasy | Wraps the fake service as a FakeItEasy Fake<IOrganizationService> |
| Fake4Dataverse.Moq | Wraps the fake service as a Moq Mock<IOrganizationService> |
| Fake4Dataverse.AwesomeAssertions | Should().HaveCreated(…) style assertions via AwesomeAssertions |
| Fake4Dataverse.FluentAssertions | Should().HaveCreated(…) style assertions via FluentAssertions |
| Fake4Dataverse.Shouldly | ShouldHaveCreated(…) style assertions via Shouldly |
| Fake4Dataverse.Spkl | Auto-register plugins from SPKL [CrmPluginRegistration] attributes |
Install any adapter alongside the core package:
dotnet add package Fake4Dataverse.AwesomeAssertions
- Getting Started
- Pipeline & Plugin Testing
- Security & Access Control
- Metadata & Validation
- Querying (QueryExpression & FetchXml)
- Calculated & Rollup Fields
- Currency & Exchange Rates
- Binary & File Operations
- Performance & Indexing
- Concurrency & Transactions
- Assertion Adapters
- Cookbook — Common Patterns
- Migration from FakeXrmEasy
- Configuration Reference
- Condition Operators
- Request Handlers
- API Quick Reference
- Feature Comparison
- Performance Benchmarks
The samples/ directory contains runnable examples with matching test projects:
- AccountService — production-style service using
IOrganizationService, with unit tests. - Plugin — real
IPluginimplementation with pipeline-based tests.