Guides

Coming from Dynamics XRM

ARX.SDK's model is inspired by the Dynamics 365 / Dynamics CRM SDK, so most of what you write there has a direct counterpart here. ARX is a data access library over your own database, not the CRM platform: this page maps the concepts and lists where the same name behaves differently.

The concepts, side by side

Dynamics SDK ARX.SDK Same, or different
Entity ArxEntity("account"): a table name and its attributes Same idea; the name is your table's. entity.Id is entity.RecordId, an ArxRecordId (set by Create and by reads). Unlike XRM, Create takes the id from the id attribute: to give a new record its id, set entity["accountid"] (the declared id column), not RecordId. A RecordId your code sets before a Create is ARX1001 unless the attribute holds the same id (from 0.1.0-preview.5; it was ignored and the database made a new id)
entity["name"], Contains, GetAttributeValue<T> The same three The indexer returns null for a missing attribute instead of throwing; GetAttributeValue<T> converts numbers when nothing is lost (a Count, always a long, read as int) and reads a lookup as its id; a number that doesn't fit is ARX4001 (1.5 read as int), any other mismatch is ARX4004, an ArxValidationException, not an InvalidCastException. Of a NULL column it is default(T) (0 for int), as in XRM: ask for int? to tell NULL from 0. Unlike XRM, a column that was read as NULL is present: Contains is true and its value is null (XRM leaves it out). entity.Attributes["x"] is the dictionary and throws KeyNotFoundException for a missing attribute, as XRM's indexer does
EntityReference ArxEntityReference(table, id, name) Same. A lookup column is declared in the schema catalog (.Lookup("contact", "account_id", "account")) and reads back as an ArxEntityReference, with the name .DisplayName("account", "name") gives. Its id is reference.RecordId: the obsolete Id property holds numbers only, so it reads 0 for a Guid or text id
Guid ids everywhere The id column you declare, of any type (ArxIdColumn.Generated<long>("id"), Supplied<string>("code"), a Guid) Different: CreateAsync returns an ArxRecordId, not a Guid
IOrganizationService IArxDataService, implemented by each provider's service (ArxMsSqlDbService, …) Same operations, asynchronous: CreateAsync, RetrieveAsync, RetrieveMultipleAsync, UpdateAsync, DeleteAsync
QueryExpression ArxQueryExpression Same shape: ColumnSet, Criteria, Orders, LinkEntities, PageInfo
ColumnSet(true), new ColumnSet(), ColumnSet(false) ArxColumnSet.All, and an empty new ArxColumnSet(), read every column; there is no bool constructor (from 1.0.0-rc.1) Different: an empty column set means all columns, where XRM's (and ColumnSet(false)) means the id only. To read only the id, name it: new ArxColumnSet("accountid")
FilterExpression, LogicalOperator.Or query.Criteria.AddFilter(ArxLogicalOperator.Or) Same
ConditionExpression, ConditionOperator AddCondition("name", ArxConditionOperator.Equal, "Contoso") Same; a value is always a parameter
LinkEntity, EntityAlias, LinkCriteria, JoinOperator query.AddLink("contact", "id", "account_id", ArxJoinOperator.Left), with the same properties (ArxJoinOperator is in ARX.SDK.Query, with the other query types). The constructor takes four arguments, new ArxLinkEntity(linkToTable, linkFromColumn, linkToColumn, join): the table it links from is the one it's added to, so there's no linkFromEntityName Different on read: a linked column is row["alias.column"], the plain value, not an AliasedValue. That includes a lookup: through a link it is its raw id (a long, say), not an ArxEntityReference, so read it with GetAttributeValue<long>, not a cast. Without an alias the prefix is the linked table's name (row["account.name"]), not XRM's numbered account1.
OptionSetValue A selection set: the raw value is stored; labels come from the catalog (.Add("Status", …)) and entity.GetSelectionSet(…) (ARX.SDK.Extensions) Different: a set isn't bound to a column, so name the set when you write (new ArxSelectionSetValue("Status", 2)) and when you read; ARX checks the value against the set you name, not against the column. A plain int written to the column isn't checked at all
FormattedValues None: ArxEntity has no FormattedValues (removed in 1.0.0-rc.1; no provider ever filled it) Missing. For an option's label, use entity.GetSelectionSetDisplayText("industrycode", "Industry", catalog) (ARX.SDK.Extensions); for a lookup's name, the reference's Name
Money decimal Same value, no wrapper
PagingCookie, MoreRecords PageInfo.PagingCookie, HasNextPage Cookies on MongoDB and DynamoDB; the SQL providers page by number and refuse a cookie (ARX8001)
ExecuteMultipleRequest CreateMultipleAsync / UpdateMultipleAsync / DeleteMultipleAsync, or a batch scope (RunBatchAsync) Different. The Multiple calls are all or nothing. A batch scope runs many calls in one transaction: with savepoints (new ArxBatchOptions { Savepoints = true }, on PostgreSQL, SQL Server and MySQL) a call that fails is undone alone, you catch its error and the others still commit, like ContinueOnError = true inside one transaction; without savepoints (the default) the first failure rolls the scope back and RunBatchAsync throws ARX1015. A failed Multiple call doesn't say which entity failed: the database checks the statement as a whole, and there's no RequestIndex. To find the entity, send one call per entity in a scope with savepoints, where the error's BatchNumber is the number of the failed call
Execute(OrganizationRequest) ExecuteAsync(ArxProcedureExpression) Different: it calls one of your database's procedures or functions
FaultException<OrganizationServiceFault> ArxException with a stable Code (ARX2002 RecordNotFound, …) Catch by code (Errors in practice)

Where the similar names behave differently

  • A record that isn't there. RetrieveAsync returns null and DeleteAsync returns false where the Dynamics SDK throws; UpdateAsync throws ARX2002, as it does.
  • Attribute names. They are your column names, and ARX matches them ignoring case: entity["Name"] reads and writes the column name, keeping the spelling it was first given. XRM's logical names are lowercase and matched exactly, so code that already uses lowercase names behaves the same.
  • Table names are matched exactly. new ArxEntity("Account") isn't the catalog's account: it's ARX1016, with a "Did you mean 'account'?" hint. Write table names as the catalog declares them.
  • Namespaces. ArxEntity, ArxEntityReference, ArxColumnSet and ArxLinkEntity are in ARX.SDK.Services. ArxQueryExpression, conditions, filters, orders and ArxJoinOperator are in ARX.SDK.Query, and the selection-set helpers in ARX.SDK.Extensions.
  • What isn't there. There's no Upsert, no N:N Associate or Disassociate (write the intersect table's rows), no FetchXML, and no relative date operators (LastXDays, ThisMonth and the like): compute the dates and use GreaterEqual and LessThan.
  • Bulk update and delete. Where XRM has a bulk-delete job and no update by a query, ARX changes or deletes every row a filter matches in one call: UpdateMultipleAsync(values, filter) with an entity that carries only the values (no id: ARX1001), and DeleteMultipleAsync("account", filter). They run at once, all or nothing, and return the count.
  • Keys. .Key("account", "account_number") declares a business key ARX can find rows by (ArxMatch.Key("account_number")); the database's unique constraint is what enforces it.
  • No platform. No plugins, workflows, security roles, audit or business rules: put that logic in your application, or in the database.

Where to start

Installation sets up a service; each provider's operation pages have samples for every operation in the same order: Create, CreateMultiple, Update, UpdateMultiple, Delete, DeleteMultiple, Retrieve, RetrieveMultiple, Execute.