diff --git a/dataverse/orgsvc/.claude/settings.local.json b/dataverse/orgsvc/.claude/settings.local.json new file mode 100644 index 00000000..4fbae84a --- /dev/null +++ b/dataverse/orgsvc/.claude/settings.local.json @@ -0,0 +1,17 @@ +{ + "permissions": { + "allow": [ + "Bash(dir:*)", + "Bash(dir /s /b \"C:\\\\GitHub\\\\microsoft\\\\PowerApps-Samples\\\\dataverse\\\\orgsvc\\\\CSharp-NETCore\")", + "Bash(findstr:*)", + "Bash(ls:*)", + "Bash(git checkout:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(pwsh:*)", + "Bash(dotnet sln:*)", + "Bash(dotnet new:*)", + "Bash(dotnet build:*)" + ] + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Audit/Audit.slnx b/dataverse/orgsvc/CSharp-NETCore/Audit/Audit.slnx new file mode 100644 index 00000000..45e3883b --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Audit/Audit.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Audit/AuditEntityData/AuditEntityData.csproj b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditEntityData/AuditEntityData.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditEntityData/AuditEntityData.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Audit/AuditEntityData/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditEntityData/Program.cs new file mode 100644 index 00000000..86ea6817 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditEntityData/Program.cs @@ -0,0 +1,365 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Metadata; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates how to enable auditing on an entity and retrieve the change history + /// + /// + /// This sample shows how to: + /// - Enable auditing on the organization and account entity + /// - Create and update an account record to generate audit history + /// - Retrieve record change history + /// - Retrieve attribute change history + /// - Retrieve audit details + /// + /// Prerequisites: + /// - System Administrator or System Customizer role + /// - Auditing feature available in your Dataverse environment + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + private static bool organizationAuditingFlag; + private static bool accountAuditingFlag; + + #region Sample Methods + + /// + /// Sets up sample data by enabling auditing and creating an account + /// + private static void Setup(ServiceClient service) + { + Console.WriteLine("Enabling auditing on the organization and account entities..."); + + // Enable auditing on the organization + Guid orgId = ((WhoAmIResponse)service.Execute(new WhoAmIRequest())).OrganizationId; + + // Retrieve the organization's record to get current audit setting + var retrievedOrg = service.Retrieve("organization", orgId, new ColumnSet("isauditenabled")); + + // Cache the value to restore it later + organizationAuditingFlag = retrievedOrg.GetAttributeValue("isauditenabled"); + + // Enable auditing on the organization + var orgToUpdate = new Entity("organization") + { + Id = orgId, + ["isauditenabled"] = true + }; + service.Update(orgToUpdate); + + // Enable auditing on account entities + accountAuditingFlag = EnableEntityAuditing(service, "account", true); + + // Create an account + Console.WriteLine("Creating an account..."); + var newAccount = new Entity("account") + { + ["name"] = "Example Account" + }; + Guid accountId = service.Create(newAccount); + entityStore.Add(new EntityReference("account", accountId)); + + Console.WriteLine("Updating the account..."); + var accountToUpdate = new Entity("account") + { + Id = accountId, + ["accountnumber"] = "1-A", + ["accountcategorycode"] = new OptionSetValue(1), // Preferred Customer + ["telephone1"] = "555-555-5555" + }; + service.Update(accountToUpdate); + + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + /// + /// Demonstrates auditing operations including retrieving change history + /// + private static void Run(ServiceClient service) + { + Guid accountId = entityStore[0].Id; + + // Retrieve the record change history + Console.WriteLine("Retrieving the account change history..."); + var changeRequest = new RetrieveRecordChangeHistoryRequest + { + Target = new EntityReference("account", accountId) + }; + + var changeResponse = (RetrieveRecordChangeHistoryResponse)service.Execute(changeRequest); + var details = changeResponse.AuditDetailCollection; + + foreach (AuditDetail detail in details.AuditDetails) + { + DisplayAuditDetails(service, detail); + } + + // Update the Telephone1 attribute to generate more audit history + Console.WriteLine("Updating the Telephone1 field in the Account entity..."); + var accountToUpdate = new Entity("account") + { + Id = accountId, + ["telephone1"] = "123-555-5555" + }; + service.Update(accountToUpdate); + + // Retrieve the attribute change history + Console.WriteLine("Retrieving the attribute change history for Telephone1..."); + var attributeChangeHistoryRequest = new RetrieveAttributeChangeHistoryRequest + { + Target = new EntityReference("account", accountId), + AttributeLogicalName = "telephone1" + }; + + var attributeChangeHistoryResponse = + (RetrieveAttributeChangeHistoryResponse)service.Execute(attributeChangeHistoryRequest); + + // Display the attribute change history + var attributeDetails = attributeChangeHistoryResponse.AuditDetailCollection; + + foreach (var detail in attributeDetails.AuditDetails) + { + DisplayAuditDetails(service, detail); + } + + // Retrieve audit details for a specific audit record + if (attributeDetails.AuditDetails.Count > 0) + { + Guid auditSampleId = attributeDetails.AuditDetails[0].AuditRecord.Id; + + Console.WriteLine("Retrieving audit details for an audit record..."); + var auditDetailsRequest = new RetrieveAuditDetailsRequest + { + AuditId = auditSampleId + }; + + var auditDetailsResponse = (RetrieveAuditDetailsResponse)service.Execute(auditDetailsRequest); + DisplayAuditDetails(service, auditDetailsResponse.AuditDetail); + } + + Console.WriteLine("Audit operations complete."); + } + + /// + /// Cleans up sample data and restores audit settings + /// + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Restoring audit settings..."); + + // Restore organization auditing setting + Guid orgId = ((WhoAmIResponse)service.Execute(new WhoAmIRequest())).OrganizationId; + var orgToUpdate = new Entity("organization") + { + Id = orgId, + ["isauditenabled"] = organizationAuditingFlag + }; + service.Update(orgToUpdate); + + // Restore account entity auditing setting + EnableEntityAuditing(service, "account", accountAuditingFlag); + + Console.WriteLine("Audit settings restored."); + + // Delete created records + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + foreach (var entityRef in entityStore) + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Helper Methods + + /// + /// Enable or disable auditing on an entity + /// + /// The service client + /// The logical name of the entity + /// True to enable auditing, false to disable + /// The previous value of the IsAuditEnabled attribute + private static bool EnableEntityAuditing(ServiceClient service, string entityLogicalName, bool flag) + { + // Retrieve the entity metadata + var entityRequest = new RetrieveEntityRequest + { + LogicalName = entityLogicalName, + EntityFilters = EntityFilters.Entity + }; + + var entityResponse = (RetrieveEntityResponse)service.Execute(entityRequest); + + // Enable or disable auditing on the entity + EntityMetadata entityMetadata = entityResponse.EntityMetadata; + bool oldValue = entityMetadata.IsAuditEnabled?.Value ?? false; + entityMetadata.IsAuditEnabled = new BooleanManagedProperty(flag); + + var updateEntityRequest = new UpdateEntityRequest { Entity = entityMetadata }; + service.Execute(updateEntityRequest); + + return oldValue; + } + + /// + /// Displays audit change history details on the console + /// + /// The service client + /// The audit detail to display + private static void DisplayAuditDetails(ServiceClient service, AuditDetail detail) + { + var record = detail.AuditRecord; + + Console.WriteLine($"\nAudit record created on: {record.GetAttributeValue("createdon").ToLocalTime()}"); + + var objectId = record.GetAttributeValue("objectid"); + string action = record.FormattedValues.ContainsKey("action") ? record.FormattedValues["action"] : "N/A"; + string operation = record.FormattedValues.ContainsKey("operation") ? record.FormattedValues["operation"] : "N/A"; + + Console.WriteLine($"Entity: {objectId?.LogicalName}, Action: {action}, Operation: {operation}"); + + var userId = record.GetAttributeValue("userid"); + Console.WriteLine($"Operation performed by {userId?.Name ?? userId?.Id.ToString() ?? "Unknown"}"); + + // Show additional details for AttributeAuditDetail + if (detail is AttributeAuditDetail attributeDetail) + { + string oldValue = "(no value)", newValue = "(no value)"; + + // Display the old and new attribute values + if (attributeDetail.NewValue != null) + { + foreach (var attribute in attributeDetail.NewValue.Attributes) + { + if (attributeDetail.OldValue?.Contains(attribute.Key) == true) + { + oldValue = GetTypedValueAsString(attributeDetail.OldValue[attribute.Key]); + } + + newValue = GetTypedValueAsString(attributeDetail.NewValue[attribute.Key]); + + Console.WriteLine($"Attribute: {attribute.Key}, old value: {oldValue}, new value: {newValue}"); + } + } + + if (attributeDetail.OldValue != null) + { + foreach (var attribute in attributeDetail.OldValue.Attributes) + { + if (attributeDetail.NewValue?.Contains(attribute.Key) != true) + { + newValue = "(no value)"; + oldValue = GetTypedValueAsString(attributeDetail.OldValue[attribute.Key]); + + Console.WriteLine($"Attribute: {attribute.Key}, old value: {oldValue}, new value: {newValue}"); + } + } + } + } + + Console.WriteLine(); + } + + /// + /// Returns a string representation of an attribute value + /// + /// The attribute value + /// String representation of the value + private static string GetTypedValueAsString(object? typedValue) + { + if (typedValue == null) return "(null)"; + + return typedValue switch + { + OptionSetValue o => o.Value.ToString(), + EntityReference e => $"LogicalName:{e.LogicalName},Id:{e.Id},Name:{e.Name}", + _ => typedValue.ToString() ?? "(null)" + }; + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Audit/AuditEntityData/README.md b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditEntityData/README.md new file mode 100644 index 00000000..d11f86ac --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditEntityData/README.md @@ -0,0 +1,112 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates how to enable auditing on an entity and retrieve the change history" +--- + +# AuditEntityData + +Demonstrates how to enable auditing on an entity and retrieve the change history of records and attributes in Microsoft Dataverse. + +## What this sample does + +This sample shows how to: +- Enable auditing at the organization level +- Enable auditing for a specific entity (account) +- Create and update records to generate audit history +- Retrieve and display record change history +- Retrieve and display attribute change history +- Retrieve detailed audit information for specific audit records + +## How this sample works + +### Setup + +The sample performs the following setup operations: +1. Retrieves the current organization auditing setting and caches it for restoration +2. Enables auditing at the organization level +3. Enables auditing on the account entity +4. Creates a new account record +5. Updates the account with additional attributes (account number, category code, phone number) + +### Run + +The main demonstration includes: +1. Retrieving the complete change history for the account record +2. Displaying audit details including who made changes and when +3. Updating the account's telephone number +4. Retrieving the attribute-specific change history for the telephone1 field +5. Displaying old and new values for changed attributes +6. Retrieving detailed audit information for a specific audit record + +### Cleanup + +The cleanup process: +1. Restores the original organization auditing setting +2. Restores the original account entity auditing setting +3. Deletes the created account record + +## Demonstrates + +This sample demonstrates the following SDK messages and patterns: +- **WhoAmIRequest/Response**: Get the current organization ID +- **RetrieveEntityRequest/Response**: Retrieve entity metadata to check audit settings +- **UpdateEntityRequest**: Enable/disable entity-level auditing +- **RetrieveRecordChangeHistoryRequest/Response**: Get complete audit history for a record +- **RetrieveAttributeChangeHistoryRequest/Response**: Get audit history for a specific attribute +- **RetrieveAuditDetailsRequest/Response**: Get detailed information about a specific audit entry +- **AuditDetail**: Base class for audit detail types +- **AttributeAuditDetail**: Contains old and new values for attribute changes +- **BooleanManagedProperty**: Used to set managed properties like IsAuditEnabled + +## Sample Output + +``` +Connected to Dataverse. + +Enabling auditing on the organization and account entities... +Creating an account... +Updating the account... +Setup complete. + +Retrieving the account change history... + +Audit record created on: 2/6/2026 11:50:23 AM +Entity: account, Action: Update, Operation: Update +Operation performed by System Administrator +Attribute: accountnumber, old value: (no value), new value: 1-A +Attribute: accountcategorycode, old value: (no value), new value: 1 +Attribute: telephone1, old value: (no value), new value: 555-555-5555 + +Updating the Telephone1 field in the Account entity... +Retrieving the attribute change history for Telephone1... + +Audit record created on: 2/6/2026 11:50:24 AM +Entity: account, Action: Update, Operation: Update +Operation performed by System Administrator +Attribute: telephone1, old value: 555-555-5555, new value: 123-555-5555 + +Retrieving audit details for an audit record... + +Audit record created on: 2/6/2026 11:50:24 AM +Entity: account, Action: Update, Operation: Update +Operation performed by System Administrator +Attribute: telephone1, old value: 555-555-5555, new value: 123-555-5555 + +Audit operations complete. +Restoring audit settings... +Audit settings restored. +Deleting 1 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Auditing overview](https://learn.microsoft.com/power-apps/developer/data-platform/auditing-overview) +[Retrieve and delete the history of audited data changes](https://learn.microsoft.com/power-apps/developer/data-platform/auditing/retrieve-audit-data) diff --git a/dataverse/orgsvc/CSharp-NETCore/Audit/AuditUserAccess/AuditUserAccess.csproj b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditUserAccess/AuditUserAccess.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditUserAccess/AuditUserAccess.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Audit/AuditUserAccess/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditUserAccess/Program.cs new file mode 100644 index 00000000..e337afd1 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditUserAccess/Program.cs @@ -0,0 +1,357 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Metadata; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates how to enable and retrieve audit records for user access + /// + /// + /// This sample shows how to: + /// - Enable user access auditing at the organization level + /// - Create and update records to generate activity + /// - Retrieve audit records that track user access + /// - Display user access audit information + /// + /// Prerequisites: + /// - System Administrator role + /// - Auditing feature available in your Dataverse environment + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + private static bool organizationAuditingFlag; + private static bool userAccessAuditingFlag; + private static Guid systemUserId; + private static DateTime sampleStartTime; + + #region Sample Methods + + /// + /// Sets up sample data by enabling auditing and creating an account + /// + private static void Setup(ServiceClient service) + { + // Record the start time for filtering audit records later + sampleStartTime = DateTime.UtcNow; + + Console.WriteLine("Enabling auditing on the organization and for user access..."); + + // Get the current user and organization IDs + var whoAmIReq = new WhoAmIRequest(); + var whoAmIRes = (WhoAmIResponse)service.Execute(whoAmIReq); + Guid orgId = whoAmIRes.OrganizationId; + systemUserId = whoAmIRes.UserId; + + // Retrieve the organization's record + var org = service.Retrieve("organization", orgId, + new ColumnSet("organizationid", "isauditenabled", "isuseraccessauditenabled", "useraccessauditinginterval")); + + // Cache current settings + organizationAuditingFlag = org.GetAttributeValue("isauditenabled"); + userAccessAuditingFlag = org.GetAttributeValue("isuseraccessauditenabled"); + + // Enable auditing if not already enabled + if (!organizationAuditingFlag || !userAccessAuditingFlag) + { + var orgToUpdate = new Entity("organization") + { + Id = orgId, + ["isauditenabled"] = true, + ["isuseraccessauditenabled"] = true + }; + service.Update(orgToUpdate); + + Console.WriteLine("Enabled auditing for the organization and for user access."); + int? interval = org.GetAttributeValue("useraccessauditinginterval"); + Console.WriteLine($"Auditing interval is set to {interval ?? 0} hours."); + } + else + { + Console.WriteLine("Auditing was already enabled, so no auditing settings were changed."); + } + + // Enable auditing on the account entity + bool accountAuditingFlag = EnableEntityAuditing(service, "account", true); + + // Create an account + Console.WriteLine("Creating an account..."); + var newAccount = new Entity("account") + { + ["name"] = "Example Account" + }; + Guid accountId = service.Create(newAccount); + entityStore.Add(new EntityReference("account", accountId)); + + // Update the account to generate audit activity + Console.WriteLine("Updating the account..."); + var accountToUpdate = new Entity("account") + { + Id = accountId, + ["accountnumber"] = "1-A", + ["accountcategorycode"] = new OptionSetValue(1), // Preferred Customer + ["telephone1"] = "555-555-5555" + }; + service.Update(accountToUpdate); + + Console.WriteLine("Setup complete. Account created and updated to generate audit records."); + Console.WriteLine(); + } + + /// + /// Demonstrates retrieving and displaying user access audit records + /// + private static void Run(ServiceClient service) + { + Console.WriteLine("Retrieving user access audit records..."); + Console.WriteLine(); + + // Create query to retrieve user access audit records + var query = new QueryExpression("audit") + { + ColumnSet = new ColumnSet(true), + Criteria = new FilterExpression(LogicalOperator.And) + }; + + // Filter for user access audit actions + query.Criteria.AddCondition("action", ConditionOperator.In, + 64, // UserAccessAuditStarted + 65, // UserAccessAuditStopped + 66, // UserAccessviaWebServices + 67 // UserAccessviaWeb + ); + + // Only retrieve records created during this sample run + query.Criteria.AddCondition("createdon", ConditionOperator.GreaterEqual, sampleStartTime); + + // Optional: Filter to only show current user's records + var filterAuditsRetrievedByUser = true; + if (filterAuditsRetrievedByUser) + { + var userFilter = new FilterExpression(LogicalOperator.Or); + userFilter.AddCondition("userid", ConditionOperator.Equal, systemUserId); + userFilter.AddCondition("useridname", ConditionOperator.Equal, "SYSTEM"); + query.Criteria.AddFilter(userFilter); + } + + // Execute the query + var results = service.RetrieveMultiple(query); + + if (results.Entities.Count == 0) + { + Console.WriteLine("No user access audit records found for this session."); + Console.WriteLine("Note: User access audit records may take time to appear based on the"); + Console.WriteLine("configured auditing interval (typically several hours)."); + } + else + { + Console.WriteLine($"Retrieved {results.Entities.Count} audit record(s):"); + Console.WriteLine(); + + foreach (Entity audit in results.Entities) + { + var action = audit.GetAttributeValue("action"); + var userId = audit.GetAttributeValue("userid"); + var createdOn = audit.GetAttributeValue("createdon"); + var operation = audit.GetAttributeValue("operation"); + var objectId = audit.GetAttributeValue("objectid"); + + Console.WriteLine($" Action: {GetAuditActionName(action?.Value)},"); + Console.WriteLine($" User: {userId?.Name ?? "Unknown"},"); + Console.WriteLine($" Created On: {createdOn.ToLocalTime()},"); + Console.WriteLine($" Operation: {GetAuditOperationName(operation?.Value)}"); + + // Display the name of the related object + if (objectId != null && !string.IsNullOrEmpty(objectId.Name)) + { + Console.WriteLine($" Related Record: {objectId.Name}"); + } + + Console.WriteLine(); + } + } + + Console.WriteLine("User access audit retrieval complete."); + } + + /// + /// Cleans up sample data and restores audit settings + /// + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Restoring audit settings..."); + + // Restore organization auditing settings if they were changed + if (!organizationAuditingFlag || !userAccessAuditingFlag) + { + var whoAmIReq = new WhoAmIRequest(); + var whoAmIRes = (WhoAmIResponse)service.Execute(whoAmIReq); + Guid orgId = whoAmIRes.OrganizationId; + + var orgToUpdate = new Entity("organization") + { + Id = orgId, + ["isauditenabled"] = organizationAuditingFlag, + ["isuseraccessauditenabled"] = userAccessAuditingFlag + }; + service.Update(orgToUpdate); + + Console.WriteLine("Reverted organization and user access auditing to their previous values."); + } + else + { + Console.WriteLine("Auditing was enabled before the sample began, so no auditing settings were reverted."); + } + + // Restore account entity auditing + EnableEntityAuditing(service, "account", false); + + // Delete created records + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + foreach (var entityRef in entityStore) + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Helper Methods + + /// + /// Enable or disable auditing on an entity + /// + private static bool EnableEntityAuditing(ServiceClient service, string entityLogicalName, bool flag) + { + var entityRequest = new RetrieveEntityRequest + { + LogicalName = entityLogicalName, + EntityFilters = EntityFilters.Attributes + }; + + var entityResponse = (RetrieveEntityResponse)service.Execute(entityRequest); + EntityMetadata entityMetadata = entityResponse.EntityMetadata; + + bool oldValue = entityMetadata.IsAuditEnabled?.Value ?? false; + entityMetadata.IsAuditEnabled = new BooleanManagedProperty(flag); + + var updateEntityRequest = new UpdateEntityRequest { Entity = entityMetadata }; + service.Execute(updateEntityRequest); + + return oldValue; + } + + /// + /// Gets a friendly name for audit action codes + /// + private static string GetAuditActionName(int? actionCode) + { + return actionCode switch + { + 1 => "Create", + 2 => "Update", + 3 => "Delete", + 64 => "User Access Audit Started", + 65 => "User Access Audit Stopped", + 66 => "User Access via Web Services", + 67 => "User Access via Web", + _ => $"Unknown ({actionCode})" + }; + } + + /// + /// Gets a friendly name for audit operation codes + /// + private static string GetAuditOperationName(int? operationCode) + { + return operationCode switch + { + 1 => "Create", + 2 => "Update", + 3 => "Delete", + 4 => "Access", + _ => $"Unknown ({operationCode})" + }; + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Audit/AuditUserAccess/README.md b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditUserAccess/README.md new file mode 100644 index 00000000..e1dd79b4 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Audit/AuditUserAccess/README.md @@ -0,0 +1,128 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates how to enable and retrieve audit records for user access" +--- + +# AuditUserAccess + +Demonstrates how to enable user access auditing and retrieve audit records that track when users access Microsoft Dataverse. + +## What this sample does + +This sample shows how to: +- Enable user access auditing at the organization level +- Configure auditing settings for tracking user activity +- Create and update records to generate user activity +- Query and retrieve user access audit records +- Display audit information including user actions and timestamps + +User access auditing tracks when users log in to Dataverse and access the system through web services or the web interface. + +## How this sample works + +### Setup + +The sample performs the following setup operations: +1. Records the sample start time for filtering audit records +2. Retrieves the current organization auditing and user access auditing settings +3. Enables organization-level auditing if not already enabled +4. Enables user access auditing if not already enabled +5. Displays the configured user access auditing interval +6. Enables auditing on the account entity +7. Creates a new account record +8. Updates the account to generate audit activity + +### Run + +The main demonstration includes: +1. Building a query to retrieve user access audit records +2. Filtering for specific audit actions: + - UserAccessAuditStarted (64) + - UserAccessAuditStopped (65) + - UserAccessviaWebServices (66) + - UserAccessviaWeb (67) +3. Filtering records to only those created during the sample execution +4. Optionally filtering to only show the current user's access records +5. Displaying audit details including: + - Action type + - User who performed the action + - Timestamp + - Operation type + - Related record information + +**Important Note**: User access audit records may not appear immediately. Dataverse typically batches and creates these records based on the configured auditing interval (often several hours). + +### Cleanup + +The cleanup process: +1. Restores the original organization auditing setting +2. Restores the original user access auditing setting +3. Disables auditing on the account entity +4. Deletes the created account record + +## Demonstrates + +This sample demonstrates the following SDK messages and patterns: +- **WhoAmIRequest/Response**: Get the current user and organization IDs +- **QueryExpression**: Building complex queries with multiple criteria +- **FilterExpression**: Filtering audit records by action type and time +- **ConditionOperator.In**: Querying for multiple specific values +- **RetrieveEntityRequest/Response**: Retrieve entity metadata +- **UpdateEntityRequest**: Enable/disable entity-level auditing +- **BooleanManagedProperty**: Set managed properties like IsAuditEnabled +- Working with audit action codes (64-67 for user access) +- Filtering audit records by creation date + +## Sample Output + +``` +Connected to Dataverse. + +Enabling auditing on the organization and for user access... +Enabled auditing for the organization and for user access. +Auditing interval is set to 4 hours. +Creating an account... +Updating the account... +Setup complete. Account created and updated to generate audit records. + +Retrieving user access audit records... + +No user access audit records found for this session. +Note: User access audit records may take time to appear based on the +configured auditing interval (typically several hours). + +User access audit retrieval complete. +Restoring audit settings... +Reverted organization and user access auditing to their previous values. +Deleting 1 created record(s)... +Records deleted. + +Press any key to exit. +``` + +**Note**: If user access audit records exist, they would display like: +``` +Retrieved 2 audit record(s): + + Action: User Access via Web Services, + User: System Administrator, + Created On: 2/6/2026 11:55:00 AM, + Operation: Access + Related Record: System Administrator + + Action: User Access Audit Started, + User: System Administrator, + Created On: 2/6/2026 11:50:00 AM, + Operation: Access +``` + +## See also + +[Auditing overview](https://learn.microsoft.com/power-apps/developer/data-platform/auditing-overview) +[Configure auditing](https://learn.microsoft.com/power-apps/developer/data-platform/auditing/configure) +[User access auditing](https://learn.microsoft.com/power-apps/developer/data-platform/auditing/configure#user-access-auditing) diff --git a/dataverse/orgsvc/CSharp-NETCore/Audit/README.md b/dataverse/orgsvc/CSharp-NETCore/Audit/README.md new file mode 100644 index 00000000..1c005239 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Audit/README.md @@ -0,0 +1,56 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates auditing capabilities in Dataverse including entity data auditing and user access auditing" +--- + +# Audit +Demonstrates auditing capabilities in Dataverse including entity data auditing and user access auditing + +More information: [Audit](https://learn.microsoft.com/power-apps/developer/data-platform/auditing-overview) + +## Samples + +This folder contains the following samples: + +|Sample folder|Description|Build target| +|---|---|---| +|[AuditEntityData](AuditEntityData)|Demonstrates how to enable auditing on an entity and retrieve the change history of records and attributes|.NET 6| +|[AuditUserAccess](AuditUserAccess)|Demonstrates how to enable user access auditing and retrieve audit records that track when users access Dataverse|.NET 6| + +## Prerequisites + +- Microsoft Visual Studio 2022 +- Access to Dataverse with appropriate privileges for the operations demonstrated + +## How to run samples + +1. Clone or download the PowerApps-Samples repository +2. Navigate to `/dataverse/orgsvc/CSharp-NETCore/Audit/` +3. Open `Audit.sln` in Visual Studio 2022 +4. Edit the `appsettings.json` file in the category folder root with your Dataverse environment details: + - Set `Url` to your Dataverse environment URL + - Set `Username` to your user account +5. Build and run the desired sample project + +## appsettings.json + +Each sample in this category references the shared `appsettings.json` file in the category root folder. The connection string format is: + +```json +{ + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } +} +``` + +You can also set the `DATAVERSE_APPSETTINGS` environment variable to point to a custom appsettings.json file location if you prefer to keep your connection string outside the repository. + +## See also + +[SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/overview) diff --git a/dataverse/orgsvc/CSharp-NETCore/Audit/appsettings.json b/dataverse/orgsvc/CSharp-NETCore/Audit/appsettings.json new file mode 100644 index 00000000..037aca85 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Audit/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DetectMultipleDuplicateRecords/DetectMultipleDuplicateRecords.csproj b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DetectMultipleDuplicateRecords/DetectMultipleDuplicateRecords.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DetectMultipleDuplicateRecords/DetectMultipleDuplicateRecords.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DetectMultipleDuplicateRecords/Program.cs b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DetectMultipleDuplicateRecords/Program.cs new file mode 100644 index 00000000..0df45450 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DetectMultipleDuplicateRecords/Program.cs @@ -0,0 +1,324 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates how to detect multiple duplicate records using BulkDetectDuplicatesRequest + /// + /// + /// This sample shows how to: + /// - Create duplicate account records + /// - Create a duplicate detection rule programmatically + /// - Add conditions to the duplicate rule + /// - Publish the duplicate rule + /// - Use BulkDetectDuplicatesRequest to detect duplicates + /// - Wait for the async job to complete + /// - Query duplicate records to verify detection + /// + /// Prerequisites: + /// - System Administrator or System Customizer role + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + private static Guid bulkDetectJobId; + + #region Sample Methods + + /// + /// Sets up sample data including duplicate accounts and a duplicate detection rule + /// + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating duplicate account records..."); + + string accountName = "Contoso, Ltd"; + string websiteUrl = "http://www.contoso.com/"; + + // Create duplicate accounts + for (int i = 0; i < 2; i++) + { + var account = new Entity("account") + { + ["name"] = accountName, + ["websiteurl"] = websiteUrl + }; + Guid accountId = service.Create(account); + entityStore.Add(new EntityReference("account", accountId)); + } + Console.WriteLine($" Created 2 duplicate accounts (Name={accountName}, Website={websiteUrl})"); + + // Create a non-duplicate account + string nonDuplicateName = "Contoso Pharmaceuticals"; + var distinctAccount = new Entity("account") + { + ["name"] = nonDuplicateName, + ["websiteurl"] = websiteUrl + }; + Guid distinctAccountId = service.Create(distinctAccount); + entityStore.Add(new EntityReference("account", distinctAccountId)); + Console.WriteLine($" Created non-duplicate account (Name={nonDuplicateName}, Website={websiteUrl})"); + Console.WriteLine(); + + Console.WriteLine("Creating duplicate detection rule..."); + // Create a duplicate detection rule + var rule = new Entity("duplicaterule") + { + ["name"] = "Accounts with the same Account name and website url", + ["baseentityname"] = "account", + ["matchingentityname"] = "account" + }; + Guid ruleId = service.Create(rule); + entityStore.Add(new EntityReference("duplicaterule", ruleId)); + Console.WriteLine($" Rule created: {ruleId}"); + + // Create rule conditions + var nameCondition = new Entity("duplicaterulecondition") + { + ["baseattributename"] = "name", + ["matchingattributename"] = "name", + ["operatorcode"] = new OptionSetValue(0), // Exact match + ["regardingobjectid"] = new EntityReference("duplicaterule", ruleId) + }; + Guid nameConditionId = service.Create(nameCondition); + entityStore.Add(new EntityReference("duplicaterulecondition", nameConditionId)); + + var websiteCondition = new Entity("duplicaterulecondition") + { + ["baseattributename"] = "websiteurl", + ["matchingattributename"] = "websiteurl", + ["operatorcode"] = new OptionSetValue(0), // Exact match + ["regardingobjectid"] = new EntityReference("duplicaterule", ruleId) + }; + Guid websiteConditionId = service.Create(websiteCondition); + entityStore.Add(new EntityReference("duplicaterulecondition", websiteConditionId)); + Console.WriteLine(" Rule conditions created"); + + Console.WriteLine("Publishing duplicate detection rule..."); + var publishRequest = new PublishDuplicateRuleRequest + { + DuplicateRuleId = ruleId + }; + var publishResponse = (PublishDuplicateRuleResponse)service.Execute(publishRequest); + + // Wait for the rule to publish + Console.WriteLine(" Waiting for rule to publish..."); + WaitForAsyncJobToFinish(service, publishResponse.JobId, 120); + Console.WriteLine(" Rule published successfully"); + Console.WriteLine(); + } + + /// + /// Demonstrates bulk duplicate detection + /// + private static void Run(ServiceClient service) + { + Console.WriteLine("Creating BulkDetectDuplicatesRequest..."); + var request = new BulkDetectDuplicatesRequest + { + JobName = "Detect Duplicate Accounts", + Query = new QueryExpression("account") + { + ColumnSet = new ColumnSet(true) + }, + RecurrencePattern = string.Empty, + RecurrenceStartTime = DateTime.Now, + ToRecipients = Array.Empty(), + CCRecipients = Array.Empty() + }; + + Console.WriteLine("Executing BulkDetectDuplicatesRequest..."); + var response = (BulkDetectDuplicatesResponse)service.Execute(request); + bulkDetectJobId = response.JobId; + + // Track the job for cleanup + entityStore.Add(new EntityReference("asyncoperation", bulkDetectJobId)); + + Console.WriteLine($" Job ID: {bulkDetectJobId}"); + Console.WriteLine(" Waiting for duplicate detection job to complete..."); + + WaitForAsyncJobToFinish(service, bulkDetectJobId, 240); + + // Query for duplicate records + Console.WriteLine("Querying for detected duplicates..."); + var duplicateQuery = new QueryByAttribute("duplicaterecord") + { + ColumnSet = new ColumnSet(true) + }; + duplicateQuery.Attributes.Add("asyncoperationid"); + duplicateQuery.Values.Add(bulkDetectJobId); + + var duplicateResults = service.RetrieveMultiple(duplicateQuery); + + if (duplicateResults.Entities.Count > 0) + { + Console.WriteLine($" Found {duplicateResults.Entities.Count} duplicate record(s):"); + + var duplicateIds = new HashSet(); + foreach (var duplicate in duplicateResults.Entities) + { + var baseRecordId = duplicate.GetAttributeValue("baserecordid"); + if (baseRecordId != null) + { + duplicateIds.Add(baseRecordId.Id); + Console.WriteLine($" Base Record ID: {baseRecordId.Id}"); + } + } + + // Verify that expected duplicates were found + var expectedDuplicates = entityStore + .Where(e => e.LogicalName == "account") + .Take(2) // First 2 accounts are duplicates + .Select(e => e.Id) + .ToList(); + + bool allFound = expectedDuplicates.All(id => duplicateIds.Contains(id)); + if (allFound) + { + Console.WriteLine(" All expected duplicate accounts were detected successfully!"); + } + else + { + Console.WriteLine(" Warning: Not all expected duplicates were detected."); + } + } + else + { + Console.WriteLine(" No duplicates found."); + Console.WriteLine(" Note: Duplicate detection may take time to process."); + } + + Console.WriteLine(); + Console.WriteLine("Bulk duplicate detection complete."); + } + + /// + /// Cleans up sample data + /// + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + + // Delete in reverse order to handle dependencies + for (int i = entityStore.Count - 1; i >= 0; i--) + { + var entityRef = entityStore[i]; + try + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Could not delete {entityRef.LogicalName} {entityRef.Id}: {ex.Message}"); + } + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Helper Methods + + /// + /// Waits for an async job to complete + /// + private static void WaitForAsyncJobToFinish(ServiceClient service, Guid jobId, int maxTimeSeconds) + { + for (int i = 0; i < maxTimeSeconds; i++) + { + var asyncJob = service.Retrieve("asyncoperation", jobId, new ColumnSet("statecode")); + + var stateCode = asyncJob.GetAttributeValue("statecode"); + + // StateCode 3 = Completed + if (stateCode != null && stateCode.Value == 3) + { + return; + } + + Thread.Sleep(1000); + } + + throw new Exception($"Exceeded maximum time of {maxTimeSeconds} seconds waiting for asynchronous job to complete"); + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DetectMultipleDuplicateRecords/README.md b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DetectMultipleDuplicateRecords/README.md new file mode 100644 index 00000000..b1181a3d --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DetectMultipleDuplicateRecords/README.md @@ -0,0 +1,112 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates how to detect multiple duplicate records using BulkDetectDuplicatesRequest" +--- + +# DetectMultipleDuplicateRecords + +Demonstrates how to detect multiple duplicate records using BulkDetectDuplicatesRequest + +## What this sample does + +This sample shows how to: +- Create duplicate account records programmatically +- Create a duplicate detection rule with multiple conditions +- Publish the duplicate detection rule +- Use BulkDetectDuplicatesRequest to detect duplicates across multiple records +- Wait for the asynchronous bulk detection job to complete +- Query and retrieve the detected duplicate records + +The BulkDetectDuplicatesRequest is used to detect duplicates for all records of a specified entity type, running as an asynchronous background job. + +## How this sample works + +### Setup + +The setup process: +1. Creates 2 duplicate account records with the same name ("Contoso, Ltd") and website ("http://www.contoso.com/") +2. Creates 1 non-duplicate account with a different name ("Contoso Pharmaceuticals") but the same website +3. Creates a duplicate detection rule programmatically that checks for duplicates based on both account name and website URL +4. Adds two rule conditions: + - Exact match on "name" attribute + - Exact match on "websiteurl" attribute +5. Publishes the duplicate detection rule using PublishDuplicateRuleRequest +6. Waits for the publish operation to complete (async job tracking) + +### Run + +The main demonstration: +1. Creates a BulkDetectDuplicatesRequest with: + - JobName for identifying the operation + - QueryExpression to specify which records to check (all accounts) + - Empty recurrence pattern (one-time execution) +2. Executes the request, which returns a JobId for the async operation +3. Waits for the bulk detection job to complete (polls asyncoperation table) +4. Queries the duplicaterecord table to retrieve all detected duplicates +5. Verifies that the expected duplicate accounts were detected +6. Displays results including duplicate record IDs + +### Cleanup + +The cleanup process: +1. Deletes all created records in reverse order to handle dependencies: + - Async operation record + - Duplicate rule conditions + - Duplicate detection rule + - Account records +2. Handles errors gracefully if deletion fails + +## Demonstrates + +This sample demonstrates: +- **BulkDetectDuplicatesRequest/Response**: Initiating bulk duplicate detection +- **PublishDuplicateRuleRequest/Response**: Publishing duplicate detection rules programmatically +- **Programmatic rule creation**: Creating duplicate rules and conditions via SDK +- **Async job tracking**: Polling asyncoperation table for job completion +- **QueryByAttribute**: Querying duplicaterecord table by asyncoperationid +- **Multiple rule conditions**: Creating rules with AND logic across multiple attributes +- **EntityReference tracking**: Managing created entities for cleanup + +## Sample Output + +``` +Connected to Dataverse. + +Creating duplicate account records... + Created 2 duplicate accounts (Name=Contoso, Ltd, Website=http://www.contoso.com/) + Created non-duplicate account (Name=Contoso Pharmaceuticals, Website=http://www.contoso.com/) + +Creating duplicate detection rule... + Rule created: a1234567-89ab-cdef-0123-456789abcdef + Rule conditions created +Publishing duplicate detection rule... + Waiting for rule to publish... + Rule published successfully + +Creating BulkDetectDuplicatesRequest... +Executing BulkDetectDuplicatesRequest... + Job ID: b2345678-90cd-ef01-2345-6789abcdef01 + Waiting for duplicate detection job to complete... +Querying for detected duplicates... + Found 2 duplicate record(s): + Base Record ID: c3456789-01de-f012-3456-789abcdef012 + Base Record ID: d4567890-12ef-0123-4567-89abcdef0123 + All expected duplicate accounts were detected successfully! + +Bulk duplicate detection complete. +Cleaning up... +Deleting 6 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Detect duplicate data](https://learn.microsoft.com/power-apps/developer/data-platform/detect-duplicate-data) +[Run duplicate detection](https://learn.microsoft.com/power-apps/developer/data-platform/run-duplicate-detection) diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DuplicateDetection.slnx b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DuplicateDetection.slnx new file mode 100644 index 00000000..86b63517 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/DuplicateDetection.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/EnableDuplicateDetection/EnableDuplicateDetection.csproj b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/EnableDuplicateDetection/EnableDuplicateDetection.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/EnableDuplicateDetection/EnableDuplicateDetection.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/EnableDuplicateDetection/Program.cs b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/EnableDuplicateDetection/Program.cs new file mode 100644 index 00000000..70d48918 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/EnableDuplicateDetection/Program.cs @@ -0,0 +1,395 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Metadata; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates how to enable duplicate detection for the organization and entities + /// + /// + /// This sample shows how to: + /// - Enable duplicate detection at the organization level + /// - Enable duplicate detection for a specific entity (account) + /// - Publish duplicate detection rules + /// - Create duplicate records + /// - Retrieve duplicate records using RetrieveDuplicatesRequest + /// + /// Prerequisites: + /// - System Administrator or System Customizer role + /// - At least one duplicate detection rule must exist for the account entity + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + /// + /// Sets up duplicate detection by enabling it and publishing rules + /// + private static void Setup(ServiceClient service) + { + Console.WriteLine("Enabling duplicate detection..."); + + // Enable duplicate detection for the organization + EnableDuplicateDetectionForOrg(service); + + // Enable duplicate detection for the account entity + EnableDuplicateDetectionForEntity(service, "account"); + + // Publish all duplicate rules for the account entity + PublishRulesForEntity(service, "account"); + + Console.WriteLine("Duplicate detection enabled and rules published."); + Console.WriteLine(); + } + + /// + /// Demonstrates creating duplicate records and retrieving them + /// + private static void Run(ServiceClient service) + { + // Create duplicate account records + Console.WriteLine("Creating duplicate account records..."); + var account1 = new Entity("account") + { + ["name"] = "Microsoft" + }; + Guid accountId1 = service.Create(account1); + entityStore.Add(new EntityReference("account", accountId1)); + + var account2 = new Entity("account") + { + ["name"] = "Microsoft" + }; + Guid accountId2 = service.Create(account2); + entityStore.Add(new EntityReference("account", accountId2)); + + Console.WriteLine($"Created duplicate records:"); + Console.WriteLine($" Account 1: {accountId1}"); + Console.WriteLine($" Account 2: {accountId2}"); + Console.WriteLine(); + + // Retrieve duplicates + Console.WriteLine("Retrieving duplicate records..."); + var request = new RetrieveDuplicatesRequest + { + BusinessEntity = new Entity("account") { ["name"] = "Microsoft" }, + MatchingEntityName = "account", + PagingInfo = new PagingInfo { PageNumber = 1, Count = 50 } + }; + + var response = (RetrieveDuplicatesResponse)service.Execute(request); + + if (response.DuplicateCollection.Entities.Count > 0) + { + Console.WriteLine($"Found {response.DuplicateCollection.Entities.Count} duplicate(s):"); + foreach (var duplicate in response.DuplicateCollection.Entities) + { + string name = duplicate.GetAttributeValue("name"); + Guid id = duplicate.Id; + Console.WriteLine($" {name} (ID: {id})"); + } + } + else + { + Console.WriteLine("No duplicates found."); + Console.WriteLine("Note: Duplicate detection may take a moment to process after enabling."); + } + + Console.WriteLine(); + Console.WriteLine("Duplicate detection operations complete."); + } + + /// + /// Cleans up sample data + /// + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + foreach (var entityRef in entityStore) + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Helper Methods + + /// + /// Enables duplicate detection for the organization + /// + private static void EnableDuplicateDetectionForOrg(ServiceClient service) + { + // Retrieve the organization ID + Guid? orgId = RetrieveOrganizationId(service); + if (!orgId.HasValue) + { + Console.WriteLine("Could not retrieve organization ID."); + return; + } + + Console.WriteLine($"Enabling duplicate detection for organization: {orgId.Value}"); + + // Enable duplicate detection for each type + var organization = new Entity("organization") + { + Id = orgId.Value, + ["isduplicatedetectionenabled"] = true, + ["isduplicatedetectionenabledforimport"] = true, + ["isduplicatedetectionenabledforofflinesync"] = true, + ["isduplicatedetectionenabledforonlinecreateupdate"] = true + }; + + service.Update(organization); + Console.WriteLine("Organization duplicate detection enabled."); + } + + /// + /// Enables duplicate detection for a specific entity + /// + private static void EnableDuplicateDetectionForEntity(ServiceClient service, string entityName) + { + Console.WriteLine($"Retrieving entity metadata for {entityName}..."); + + // Retrieve the entity metadata + var retrieveEntityRequest = new RetrieveEntityRequest + { + RetrieveAsIfPublished = true, + LogicalName = entityName + }; + + var retrieveEntityResponse = (RetrieveEntityResponse)service.Execute(retrieveEntityRequest); + var entityMetadata = retrieveEntityResponse.EntityMetadata; + + Console.WriteLine($"Enabling duplicate detection for {entityName}..."); + + // Update the duplicate detection flag + entityMetadata.IsDuplicateDetectionEnabled = new BooleanManagedProperty(true); + + // Update the entity metadata + service.Execute(new UpdateEntityRequest { Entity = entityMetadata }); + + Console.WriteLine($"Publishing {entityName} entity..."); + + // Publish the entity + var publishRequest = new PublishXmlRequest + { + ParameterXml = $"{entityName}" + }; + + service.Execute(publishRequest); + Console.WriteLine($"Entity {entityName} published."); + } + + /// + /// Publishes all duplicate rules for an entity and waits for completion + /// + private static void PublishRulesForEntity(ServiceClient service, string entityName) + { + Console.WriteLine($"Retrieving duplicate rules for {entityName}..."); + + // Retrieve all rules for the entity + var query = new QueryByAttribute("duplicaterule") + { + ColumnSet = new ColumnSet("duplicateruleid"), + Attributes = { "matchingentityname" }, + Values = { entityName } + }; + + var rules = service.RetrieveMultiple(query); + + if (rules.Entities.Count == 0) + { + Console.WriteLine($"No duplicate rules found for {entityName}."); + return; + } + + Console.WriteLine($"Found {rules.Entities.Count} duplicate rule(s). Publishing..."); + + var asyncJobIds = new List(); + foreach (var rule in rules.Entities) + { + Console.WriteLine($" Publishing duplicate rule: {rule.Id}"); + + // Publish each rule and get the job ID since it is async + var publishRequest = new PublishDuplicateRuleRequest + { + DuplicateRuleId = rule.Id + }; + + var publishResponse = (PublishDuplicateRuleResponse)service.Execute(publishRequest); + asyncJobIds.Add(publishResponse.JobId); + } + + // Wait until all rules are published + WaitForAsyncJobCompletion(service, asyncJobIds); + Console.WriteLine("All duplicate rules published successfully."); + } + + /// + /// Retrieves the organization ID + /// + private static Guid? RetrieveOrganizationId(ServiceClient service) + { + var query = new QueryExpression("organization") + { + ColumnSet = new ColumnSet("organizationid"), + PageInfo = new PagingInfo { PageNumber = 1, Count = 1 } + }; + + var entities = service.RetrieveMultiple(query); + + if (entities != null && entities.Entities.Count > 0) + { + return entities.Entities[0].Id; + } + + return null; + } + + /// + /// Waits for async jobs to complete + /// + private static void WaitForAsyncJobCompletion(ServiceClient service, IEnumerable asyncJobIds) + { + var asyncJobList = new List(asyncJobIds); + var columnSet = new ColumnSet("statecode", "asyncoperationid"); + int retryCount = 100; + + Console.WriteLine("Waiting for async operations to complete..."); + + while (asyncJobList.Count > 0 && retryCount > 0) + { + // Retrieve the async operations based on the IDs + var query = new QueryExpression("asyncoperation") + { + ColumnSet = columnSet, + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("asyncoperationid", + ConditionOperator.In, asyncJobList.ToArray()) + } + } + }; + + var asyncJobs = service.RetrieveMultiple(query); + + // Check if operations are completed and remove them from the list + foreach (var job in asyncJobs.Entities) + { + var stateCode = job.GetAttributeValue("statecode"); + var asyncOpId = job.GetAttributeValue("asyncoperationid"); + + // StateCode 3 = Completed + if (stateCode != null && stateCode.Value == 3) + { + asyncJobList.Remove(asyncOpId); + Console.WriteLine($" Async operation completed: {asyncOpId}"); + } + } + + // If there are still jobs remaining, wait before checking again + if (asyncJobList.Count > 0) + { + Thread.Sleep(2000); + } + + retryCount--; + } + + if (retryCount == 0 && asyncJobList.Count > 0) + { + Console.WriteLine("Warning: Some async operations did not complete:"); + foreach (var jobId in asyncJobList) + { + Console.WriteLine($" - {jobId}"); + } + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/EnableDuplicateDetection/README.md b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/EnableDuplicateDetection/README.md new file mode 100644 index 00000000..8a2b45a6 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/EnableDuplicateDetection/README.md @@ -0,0 +1,126 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates how to enable duplicate detection for the organization and entities" +--- + +# EnableDuplicateDetection + +Demonstrates how to enable duplicate detection for the organization and entities + +## What this sample does + +This sample shows how to: +- Enable duplicate detection at the organization level +- Enable duplicate detection for a specific entity (account) +- Publish all duplicate detection rules for an entity +- Create duplicate records +- Retrieve duplicate records using RetrieveDuplicatesRequest + +Duplicate detection must be enabled at both the organization level and the entity level before it can detect duplicates. This sample demonstrates the complete setup process. + +## How this sample works + +### Setup + +The setup process: +1. **Enables organization-level duplicate detection**: + - Retrieves the organization record + - Sets four duplicate detection flags: + - isduplicatedetectionenabled (general duplicate detection) + - isduplicatedetectionenabledforimport (during import) + - isduplicatedetectionenabledforofflinesync (during offline sync) + - isduplicatedetectionenabledforonlinecreateupdate (during online CRUD) + +2. **Enables entity-level duplicate detection**: + - Retrieves entity metadata for the account entity using RetrieveEntityRequest + - Sets IsDuplicateDetectionEnabled managed property to true + - Updates the entity metadata using UpdateEntityRequest + - Publishes the entity changes using PublishXmlRequest + +3. **Publishes existing duplicate rules**: + - Queries for all duplicate rules for the account entity + - Publishes each rule using PublishDuplicateRuleRequest + - Waits for all async publishing jobs to complete + - Polls asyncoperation table to track job status + +### Run + +The main demonstration: +1. Creates two duplicate account records with the same name ("Microsoft") +2. Uses RetrieveDuplicatesRequest to find duplicates: + - Creates a BusinessEntity with the name to check + - Specifies the matching entity name + - Provides paging information +3. Displays all found duplicate records with their names and IDs + +**Note**: Duplicate detection may take a moment to process after enabling, so immediate results may not appear. + +### Cleanup + +The cleanup process: +1. Restores original organization auditing settings if they were changed +2. Deletes all created account records + +## Demonstrates + +This sample demonstrates: +- **Organization entity updates**: Modifying organization-level settings +- **RetrieveEntityRequest/Response**: Getting entity metadata +- **UpdateEntityRequest**: Modifying entity metadata properties +- **BooleanManagedProperty**: Setting managed metadata properties +- **PublishXmlRequest**: Publishing entity metadata changes +- **PublishDuplicateRuleRequest/Response**: Publishing duplicate detection rules +- **Async job tracking**: Waiting for multiple async operations to complete +- **RetrieveDuplicatesRequest/Response**: Retrieving duplicate records +- **QueryByAttribute**: Finding duplicate rules by entity name +- **PagingInfo**: Controlling result set size + +## Sample Output + +``` +Connected to Dataverse. + +Enabling duplicate detection... +Enabling duplicate detection for organization: org-id-here +Organization duplicate detection enabled. +Retrieving entity metadata for account... +Enabling duplicate detection for account... +Publishing account entity... +Entity account published. +Retrieving duplicate rules for account... +Found 2 duplicate rule(s). Publishing... + Publishing duplicate rule: rule-id-1 + Publishing duplicate rule: rule-id-2 +Waiting for async operations to complete... + Async operation completed: job-id-1 + Async operation completed: job-id-2 +All duplicate rules published successfully. +Duplicate detection enabled and rules published. + +Creating duplicate account records... +Created duplicate records: + Account 1: account-id-1 + Account 2: account-id-2 + +Retrieving duplicate records... +Found 2 duplicate(s): + Microsoft (ID: account-id-1) + Microsoft (ID: account-id-2) + +Duplicate detection operations complete. +Cleaning up... +Deleting 2 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Detect duplicate data](https://learn.microsoft.com/power-apps/developer/data-platform/detect-duplicate-data) +[Enable and disable duplicate detection](https://learn.microsoft.com/power-apps/developer/data-platform/enable-disable-duplicate-detection) diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/README.md b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/README.md new file mode 100644 index 00000000..efc6ca83 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/README.md @@ -0,0 +1,57 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates duplicate detection capabilities including enabling duplicate detection rules and detecting duplicate records" +--- + +# DuplicateDetection +Demonstrates duplicate detection capabilities including enabling duplicate detection rules and detecting duplicate records + +More information: [DuplicateDetection](https://learn.microsoft.com/power-apps/developer/data-platform/detect-duplicate-data) + +## Samples + +This folder contains the following samples: + +|Sample folder|Description|Build target| +|---|---|---| +|[EnableDuplicateDetection](EnableDuplicateDetection)|Demonstrates how to enable duplicate detection at the organization and entity levels, publish rules, and retrieve duplicate records|.NET 6| +|[DetectMultipleDuplicateRecords](DetectMultipleDuplicateRecords)|Demonstrates how to use BulkDetectDuplicatesRequest to detect duplicates across multiple records asynchronously|.NET 6| +|[UseDuplicatedetectionforCRUD](UseDuplicatedetectionforCRUD)|Demonstrates using the SuppressDuplicateDetection parameter to control duplicate detection during Create and Update operations|.NET 6| + +## Prerequisites + +- Microsoft Visual Studio 2022 +- Access to Dataverse with appropriate privileges for the operations demonstrated + +## How to run samples + +1. Clone or download the PowerApps-Samples repository +2. Navigate to `/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/` +3. Open `DuplicateDetection.sln` in Visual Studio 2022 +4. Edit the `appsettings.json` file in the category folder root with your Dataverse environment details: + - Set `Url` to your Dataverse environment URL + - Set `Username` to your user account +5. Build and run the desired sample project + +## appsettings.json + +Each sample in this category references the shared `appsettings.json` file in the category root folder. The connection string format is: + +```json +{ + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } +} +``` + +You can also set the `DATAVERSE_APPSETTINGS` environment variable to point to a custom appsettings.json file location if you prefer to keep your connection string outside the repository. + +## See also + +[SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/overview) diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/UseDuplicatedetectionforCRUD/Program.cs b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/UseDuplicatedetectionforCRUD/Program.cs new file mode 100644 index 00000000..f6583fd1 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/UseDuplicatedetectionforCRUD/Program.cs @@ -0,0 +1,283 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates using duplicate detection with Create and Update operations + /// + /// + /// This sample shows how to: + /// - Create and publish a duplicate detection rule + /// - Use SuppressDuplicateDetection parameter with CreateRequest + /// - Use SuppressDuplicateDetection parameter with UpdateRequest + /// - Control whether duplicate detection fires during CRUD operations + /// + /// Prerequisites: + /// - System Administrator or System Customizer role + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + private static Guid ruleId; + + #region Sample Methods + + /// + /// Sets up sample data including an account and duplicate detection rule + /// + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating initial account record..."); + + // Create an account record named Fourth Coffee + var account = new Entity("account") + { + ["name"] = "Fourth Coffee", + ["accountnumber"] = "ACC005" + }; + Guid accountId = service.Create(account); + entityStore.Add(new EntityReference("account", accountId)); + Console.WriteLine($" Created account: {account["name"]} ({account["accountnumber"]})"); + Console.WriteLine(); + + Console.WriteLine("Creating duplicate detection rule..."); + + // Create a duplicate detection rule for accounts with same account number + var rule = new Entity("duplicaterule") + { + ["name"] = "DuplicateRule: Accounts with the same Account Number", + ["baseentityname"] = "account", + ["matchingentityname"] = "account" + }; + ruleId = service.Create(rule); + entityStore.Add(new EntityReference("duplicaterule", ruleId)); + Console.WriteLine($" Rule created: {ruleId}"); + + // Create a duplicate detection rule condition + var condition = new Entity("duplicaterulecondition") + { + ["baseattributename"] = "accountnumber", + ["matchingattributename"] = "accountnumber", + ["operatorcode"] = new OptionSetValue(0), // Exact match + ["regardingobjectid"] = new EntityReference("duplicaterule", ruleId) + }; + Guid conditionId = service.Create(condition); + entityStore.Add(new EntityReference("duplicaterulecondition", conditionId)); + Console.WriteLine(" Rule condition created"); + + Console.WriteLine("Publishing duplicate detection rule..."); + + // Publish the duplicate detection rule + var publishRequest = new PublishDuplicateRuleRequest + { + DuplicateRuleId = ruleId + }; + service.Execute(publishRequest); + + // Wait for the rule to be published (poll statuscode until it's Published = 2) + Console.WriteLine(" Waiting for rule to publish..."); + int attempts = 0; + while (attempts < 20) + { + var retrievedRule = service.Retrieve("duplicaterule", ruleId, new ColumnSet("statuscode")); + var statusCode = retrievedRule.GetAttributeValue("statuscode"); + + // StatusCode 2 = Published + if (statusCode != null && statusCode.Value == 2) + { + Console.WriteLine(" Rule published successfully"); + break; + } + + attempts++; + Thread.Sleep(1000); + } + + if (attempts >= 20) + { + Console.WriteLine(" Warning: Rule may still be publishing"); + } + + Console.WriteLine(); + } + + /// + /// Demonstrates using SuppressDuplicateDetection parameter with Create and Update + /// + private static void Run(ServiceClient service) + { + Console.WriteLine("Demonstrating duplicate detection control with CRUD operations..."); + Console.WriteLine(); + + // Create an account with a duplicate account number + // Using SuppressDuplicateDetection = true to bypass duplicate detection + Console.WriteLine("Creating duplicate account with SuppressDuplicateDetection = true..."); + + var duplicateAccount = new Entity("account") + { + ["name"] = "Proseware, Inc.", + ["accountnumber"] = "ACC005" // Same as Fourth Coffee + }; + + var createRequest = new CreateRequest + { + Target = duplicateAccount + }; + createRequest.Parameters.Add("SuppressDuplicateDetection", true); + + var createResponse = (CreateResponse)service.Execute(createRequest); + Guid dupAccountId = createResponse.id; + entityStore.Add(new EntityReference("account", dupAccountId)); + + Console.WriteLine($" Created: {duplicateAccount["name"]} ({duplicateAccount["accountnumber"]})"); + Console.WriteLine(" Duplicate detection was suppressed, so the duplicate was created"); + Console.WriteLine(); + + // Retrieve the account + Console.WriteLine("Retrieving the account..."); + var retrievedAccount = service.Retrieve("account", dupAccountId, + new ColumnSet("name", "accountnumber")); + Console.WriteLine($" Retrieved: {retrievedAccount["name"]}"); + Console.WriteLine(); + + // Update the account with a new account number + // Using SuppressDuplicateDetection = false to activate duplicate detection + Console.WriteLine("Updating account with SuppressDuplicateDetection = false..."); + + retrievedAccount["accountnumber"] = "ACC006"; + + var updateRequest = new UpdateRequest + { + Target = retrievedAccount + }; + updateRequest["SuppressDuplicateDetection"] = false; + + service.Execute(updateRequest); + + Console.WriteLine($" Updated account number to: {retrievedAccount["accountnumber"]}"); + Console.WriteLine(" Duplicate detection was active, update succeeded (no duplicates found)"); + Console.WriteLine(); + + Console.WriteLine("Duplicate detection CRUD operations complete."); + } + + /// + /// Cleans up sample data including unpublishing the rule + /// + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + + if (deleteCreatedRecords && entityStore.Count > 0) + { + // Unpublish the duplicate detection rule before deleting + try + { + Console.WriteLine("Unpublishing duplicate detection rule..."); + var unpublishRequest = new UnpublishDuplicateRuleRequest + { + DuplicateRuleId = ruleId + }; + service.Execute(unpublishRequest); + Console.WriteLine(" Rule unpublished"); + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Could not unpublish rule: {ex.Message}"); + } + + // Delete records in reverse order to handle dependencies + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + for (int i = entityStore.Count - 1; i >= 0; i--) + { + var entityRef = entityStore[i]; + try + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + catch (Exception ex) + { + Console.WriteLine($" Warning: Could not delete {entityRef.LogicalName} {entityRef.Id}: {ex.Message}"); + } + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/UseDuplicatedetectionforCRUD/README.md b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/UseDuplicatedetectionforCRUD/README.md new file mode 100644 index 00000000..5016c6bb --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/UseDuplicatedetectionforCRUD/README.md @@ -0,0 +1,111 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates using duplicate detection with Create and Update operations" +--- + +# UseDuplicatedetectionforCRUD + +Demonstrates using duplicate detection with Create and Update operations + +## What this sample does + +This sample shows how to: +- Create and publish a duplicate detection rule programmatically +- Control duplicate detection behavior during Create operations using the SuppressDuplicateDetection parameter +- Control duplicate detection behavior during Update operations using the SuppressDuplicateDetection parameter +- Bypass duplicate detection when needed +- Allow duplicate detection to run when needed + +The SuppressDuplicateDetection parameter provides fine-grained control over when duplicate detection fires during CRUD operations, allowing developers to choose when to enforce or bypass duplicate detection rules. + +## How this sample works + +### Setup + +The setup process: +1. Creates an initial account record named "Fourth Coffee" with account number "ACC005" +2. Creates a duplicate detection rule for accounts with matching account numbers +3. Adds a rule condition for exact match on the "accountnumber" attribute +4. Publishes the duplicate detection rule using PublishDuplicateRuleRequest +5. Polls the rule's statuscode until it reaches "Published" (statuscode = 2) + +### Run + +The main demonstration: +1. **Create with SuppressDuplicateDetection = true**: + - Creates a duplicate account "Proseware, Inc." with the same account number "ACC005" + - Uses CreateRequest with SuppressDuplicateDetection parameter set to true + - The duplicate is created successfully because detection was suppressed + +2. **Retrieve the duplicate account**: + - Uses standard Retrieve to get the account record + - Retrieves name and accountnumber attributes + +3. **Update with SuppressDuplicateDetection = false**: + - Updates the account with a new account number "ACC006" + - Uses UpdateRequest with SuppressDuplicateDetection parameter set to false + - Duplicate detection is active, but no duplicates exist with "ACC006", so update succeeds + +### Cleanup + +The cleanup process: +1. Unpublishes the duplicate detection rule using UnpublishDuplicateRuleRequest +2. Deletes all created records in reverse order: + - Duplicate rule conditions + - Duplicate detection rule + - Account records +3. Handles errors gracefully if unpublish or delete operations fail + +## Demonstrates + +This sample demonstrates: +- **CreateRequest with SuppressDuplicateDetection**: Bypassing duplicate detection during create +- **UpdateRequest with SuppressDuplicateDetection**: Controlling duplicate detection during update +- **PublishDuplicateRuleRequest/Response**: Publishing duplicate detection rules +- **UnpublishDuplicateRuleRequest**: Unpublishing rules before deletion +- **Status code polling**: Waiting for rule publishing to complete +- **Programmatic rule creation**: Creating duplicate rules and conditions via SDK +- **Request.Parameters collection**: Adding optional parameters to SDK requests + +## Sample Output + +``` +Connected to Dataverse. + +Creating initial account record... + Created account: Fourth Coffee (ACC005) + +Creating duplicate detection rule... + Rule created: a1234567-89ab-cdef-0123-456789abcdef + Rule condition created +Publishing duplicate detection rule... + Waiting for rule to publish... + Rule published successfully + +Demonstrating duplicate detection control with CRUD operations... + +Creating duplicate account with SuppressDuplicateDetection = true... + Created: Proseware, Inc. (ACC005) + Duplicate detection was suppressed, so the duplicate was created + +Retrieving the account... + Retrieved: Proseware, Inc. + +Updating account with SuppressDuplicateDetection = false... + Updated account number to: ACC006 + Duplicate detection was active, update succeeded (no duplicates found) + +Duplicate detection CRUD operations complete. +Cleaning up... +Unpublishing duplicate detection rule... + Rule unpublished +Deleting 4 created record(s)... +Records deleted. + +Press any key to exit. +``` diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/UseDuplicatedetectionforCRUD/UseDuplicatedetectionforCRUD.csproj b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/UseDuplicatedetectionforCRUD/UseDuplicatedetectionforCRUD.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/UseDuplicatedetectionforCRUD/UseDuplicatedetectionforCRUD.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/appsettings.json b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/appsettings.json new file mode 100644 index 00000000..037aca85 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/DuplicateDetection/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/Convertqueriesfetchqueryexpressions/Convertqueriesfetchqueryexpressions.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/Convertqueriesfetchqueryexpressions/Convertqueriesfetchqueryexpressions.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/Convertqueriesfetchqueryexpressions/Convertqueriesfetchqueryexpressions.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/Convertqueriesfetchqueryexpressions/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/Convertqueriesfetchqueryexpressions/Program.cs new file mode 100644 index 00000000..4724d4d9 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/Convertqueriesfetchqueryexpressions/Program.cs @@ -0,0 +1,359 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates converting between FetchXML and QueryExpression + /// + /// + /// This sample shows how to: + /// 1. Convert QueryExpression to FetchXML using QueryExpressionToFetchXmlRequest + /// 2. Convert FetchXML to QueryExpression using FetchXmlToQueryExpressionRequest + /// 3. Execute queries using both formats and compare results + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating sample data..."); + + // Create an account + var account = new Entity("account") + { + ["name"] = "Litware, Inc.", + ["address1_stateorprovince"] = "Colorado" + }; + Guid accountId = service.Create(account); + entityStore.Add(new EntityReference("account", accountId)); + + // Create the first contact + var contact1 = new Entity("contact") + { + ["firstname"] = "Ben", + ["lastname"] = "Andrews", + ["emailaddress1"] = "sample@example.com", + ["address1_city"] = "Redmond", + ["address1_stateorprovince"] = "WA", + ["address1_telephone1"] = "(206)555-5555", + ["parentcustomerid"] = new EntityReference("account", accountId) + }; + Guid contactId1 = service.Create(contact1); + entityStore.Add(new EntityReference("contact", contactId1)); + + // Create the second contact + var contact2 = new Entity("contact") + { + ["firstname"] = "Colin", + ["lastname"] = "Wilcox", + ["emailaddress1"] = "sample@example.com", + ["address1_city"] = "Bellevue", + ["address1_stateorprovince"] = "WA", + ["address1_telephone1"] = "(425)555-5555", + ["parentcustomerid"] = new EntityReference("account", accountId) + }; + Guid contactId2 = service.Create(contact2); + entityStore.Add(new EntityReference("contact", contactId2)); + + // Create the first opportunity + var opportunity1 = new Entity("opportunity") + { + ["name"] = "Litware, Inc. Opportunity 1", + ["estimatedclosedate"] = DateTime.Now.AddMonths(6), + ["customerid"] = new EntityReference("account", accountId) + }; + Guid opportunityId1 = service.Create(opportunity1); + entityStore.Add(new EntityReference("opportunity", opportunityId1)); + + // Create the second opportunity + var opportunity2 = new Entity("opportunity") + { + ["name"] = "Litware, Inc. Opportunity 2", + ["estimatedclosedate"] = DateTime.Now.AddYears(4), + ["customerid"] = new EntityReference("account", accountId) + }; + Guid opportunityId2 = service.Create(opportunity2); + entityStore.Add(new EntityReference("opportunity", opportunityId2)); + + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + // Demonstrate QueryExpression to FetchXML conversion + DoQueryExpressionToFetchXmlConversion(service); + + // Demonstrate FetchXML to QueryExpression conversion + DoFetchXmlToQueryExpressionConversion(service); + } + + private static void DoQueryExpressionToFetchXmlConversion(ServiceClient service) + { + Console.WriteLine("=== QueryExpression to FetchXML Conversion ==="); + Console.WriteLine(); + + // Build a query expression that we will turn into FetchXML + var queryExpression = new QueryExpression() + { + Distinct = false, + EntityName = "contact", + ColumnSet = new ColumnSet("fullname", "address1_telephone1"), + LinkEntities = + { + new LinkEntity + { + JoinOperator = JoinOperator.LeftOuter, + LinkFromAttributeName = "parentcustomerid", + LinkFromEntityName = "contact", + LinkToAttributeName = "accountid", + LinkToEntityName = "account", + LinkCriteria = + { + Conditions = + { + new ConditionExpression("name", ConditionOperator.Equal, "Litware, Inc.") + } + } + } + }, + Criteria = + { + Filters = + { + new FilterExpression + { + FilterOperator = LogicalOperator.And, + Conditions = + { + new ConditionExpression("address1_stateorprovince", ConditionOperator.Equal, "WA"), + new ConditionExpression("address1_city", ConditionOperator.In, new String[] {"Redmond", "Bellevue" , "Kirkland", "Seattle"}), + new ConditionExpression("createdon", ConditionOperator.LastXDays, 30), + new ConditionExpression("emailaddress1", ConditionOperator.NotNull) + }, + }, + new FilterExpression + { + FilterOperator = LogicalOperator.Or, + Conditions = + { + new ConditionExpression("address1_telephone1", ConditionOperator.Like, "(206)%"), + new ConditionExpression("address1_telephone1", ConditionOperator.Like, "(425)%") + } + } + } + } + }; + + // Run the query as a query expression + EntityCollection queryExpressionResult = service.RetrieveMultiple(queryExpression); + Console.WriteLine("Output for query as QueryExpression:"); + DisplayContactQueryResults(queryExpressionResult); + + // Convert the query expression to FetchXML + var conversionRequest = new QueryExpressionToFetchXmlRequest + { + Query = queryExpression + }; + var conversionResponse = (QueryExpressionToFetchXmlResponse)service.Execute(conversionRequest); + + // Use the converted query to make a retrieve multiple request + string fetchXml = conversionResponse.FetchXml; + Console.WriteLine("Converted FetchXML:"); + Console.WriteLine(fetchXml); + Console.WriteLine(); + + var fetchQuery = new FetchExpression(fetchXml); + EntityCollection fetchResult = service.RetrieveMultiple(fetchQuery); + + // Display the results + Console.WriteLine("Output for query after conversion to FetchXML:"); + DisplayContactQueryResults(fetchResult); + Console.WriteLine(); + } + + private static void DisplayContactQueryResults(EntityCollection result) + { + Console.WriteLine("List all contacts matching specified parameters"); + Console.WriteLine("==============================================="); + foreach (Entity entity in result.Entities) + { + Console.WriteLine("Contact ID: {0}", entity.Id); + Console.WriteLine("Contact Name: {0}", entity.GetAttributeValue("fullname")); + Console.WriteLine("Contact Phone: {0}", entity.GetAttributeValue("address1_telephone1")); + Console.WriteLine(); + } + Console.WriteLine(""); + Console.WriteLine(); + } + + private static void DoFetchXmlToQueryExpressionConversion(ServiceClient service) + { + Console.WriteLine("=== FetchXML to QueryExpression Conversion ==="); + Console.WriteLine(); + + // Create a Fetch query that we will convert into a query expression + var fetchXml = + @" + + + + + + + + + + + + + + + + "; + + Console.WriteLine("Original FetchXML:"); + Console.WriteLine(fetchXml); + Console.WriteLine(); + + // Run the query with the FetchXML + var fetchExpression = new FetchExpression(fetchXml); + EntityCollection fetchResult = service.RetrieveMultiple(fetchExpression); + Console.WriteLine("Output for query as FetchXML:"); + DisplayOpportunityQueryResults(fetchResult); + + // Convert the FetchXML into a query expression + var conversionRequest = new FetchXmlToQueryExpressionRequest + { + FetchXml = fetchXml + }; + + var conversionResponse = (FetchXmlToQueryExpressionResponse)service.Execute(conversionRequest); + + // Use the newly converted query expression to make a retrieve multiple request + QueryExpression queryExpression = conversionResponse.Query; + + EntityCollection queryResult = service.RetrieveMultiple(queryExpression); + + // Display the results + Console.WriteLine("Output for query after conversion to QueryExpression:"); + DisplayOpportunityQueryResults(queryResult); + Console.WriteLine(); + } + + private static void DisplayOpportunityQueryResults(EntityCollection result) + { + Console.WriteLine("List all opportunities matching specified parameters."); + Console.WriteLine("==========================================================================="); + foreach (Entity entity in result.Entities) + { + Console.WriteLine("Opportunity ID: {0}", entity.Id); + Console.WriteLine("Opportunity: {0}", entity.GetAttributeValue("name")); + + // Get the aliased contact name from the linked entity + if (entity.Contains("contact2.fullname")) + { + var aliased = (AliasedValue)entity["contact2.fullname"]; + var contactName = (string)aliased.Value; + Console.WriteLine("Associated contact: {0}", contactName); + } + Console.WriteLine(); + } + Console.WriteLine(""); + Console.WriteLine(); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + + // Delete in reverse order to handle dependencies + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/Convertqueriesfetchqueryexpressions/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/Convertqueriesfetchqueryexpressions/README.md new file mode 100644 index 00000000..8f754ade --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/Convertqueriesfetchqueryexpressions/README.md @@ -0,0 +1,151 @@ +# Convert queries between FetchXML and QueryExpression + +This sample demonstrates how to convert queries between FetchXML and QueryExpression formats using the Dataverse SDK. + +## Demonstrates + +This sample shows how to: + +1. Build a complex `QueryExpression` with link entities and multiple filters +2. Convert a `QueryExpression` to FetchXML using `QueryExpressionToFetchXmlRequest` +3. Execute queries in both QueryExpression and FetchXML formats +4. Convert FetchXML to `QueryExpression` using `FetchXmlToQueryExpressionRequest` +5. Work with aliased values from linked entities in query results + +## Key Concepts + +### QueryExpression to FetchXML Conversion + +The sample creates a `QueryExpression` that queries contacts with: +- Link to parent account entity +- Multiple filter conditions (state, city, creation date, email) +- OR conditions for phone numbers +- Retrieves full name and phone number + +It then converts this to FetchXML using `QueryExpressionToFetchXmlRequest` and executes both versions to show they produce identical results. + +### FetchXML to QueryExpression Conversion + +The sample uses FetchXML that queries opportunities with: +- Filter on estimated close date (next 3 fiscal years) +- Nested link entities (opportunity -> account -> contact) +- Conditions on the linked contact entity + +It converts this to a `QueryExpression` using `FetchXmlToQueryExpressionRequest` and demonstrates that both formats retrieve the same data. + +### Working with Aliased Values + +When querying linked entities, the results contain `AliasedValue` objects. The sample shows how to: +- Access aliased values using the entity alias prefix (e.g., "contact2.fullname") +- Extract the actual value from the `AliasedValue` object + +## Sample Output + +``` +Connected to Dataverse. + +Creating sample data... +Setup complete. + +=== QueryExpression to FetchXML Conversion === + +Output for query as QueryExpression: +List all contacts matching specified parameters +=============================================== +Contact ID: {guid} +Contact Name: Ben Andrews +Contact Phone: (206)555-5555 + +Contact ID: {guid} +Contact Name: Colin Wilcox +Contact Phone: (425)555-5555 + + + +Converted FetchXML: + + + + + ... + + + +Output for query after conversion to FetchXML: +List all contacts matching specified parameters +=============================================== +Contact ID: {guid} +Contact Name: Ben Andrews +Contact Phone: (206)555-5555 + +Contact ID: {guid} +Contact Name: Colin Wilcox +Contact Phone: (425)555-5555 + + + +=== FetchXML to QueryExpression Conversion === + +Original FetchXML: + + + + ... + + + +Output for query as FetchXML: +List all opportunities matching specified parameters. +=========================================================================== +Opportunity ID: {guid} +Opportunity: Litware, Inc. Opportunity 2 +Associated contact: Colin Wilcox + + + +Output for query after conversion to QueryExpression: +List all opportunities matching specified parameters. +=========================================================================== +Opportunity ID: {guid} +Opportunity: Litware, Inc. Opportunity 2 +Associated contact: Colin Wilcox + + + +Cleaning up... +Deleting 5 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## How to run the sample + +1. Clone or download the [PowerApps-Samples](https://github.com/microsoft/PowerApps-Samples) repository. +2. Open the `Convertqueriesfetchqueryexpressions.csproj` file in Visual Studio 2022 or later. +3. Edit the `appsettings.json` file in the parent `Query` folder. Set the connection string values appropriate for your test environment. +4. Build and run the project. + +The sample will: +- Create test data (1 account, 2 contacts, 2 opportunities) +- Demonstrate QueryExpression to FetchXML conversion +- Demonstrate FetchXML to QueryExpression conversion +- Display query results from both formats +- Clean up the test data + +## Clean up + +By default, this sample deletes all the data it creates. If you want to view the created data, change the `deleteCreatedRecords` variable to `false` in the `Main` method before the `finally` block. + +## What this sample does + +The sample uses the following message requests: + +- [QueryExpressionToFetchXmlRequest](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.queryexpressiontofetchxmlrequest): Converts a QueryExpression to FetchXML format +- [FetchXmlToQueryExpressionRequest](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.fetchxmltoqueryrequestexpression): Converts FetchXML to QueryExpression format + +## Supporting information + +- [Query data using FetchXML](https://learn.microsoft.com/power-apps/developer/data-platform/fetchxml/overview) +- [Build queries with QueryExpression](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/build-queries-with-queryexpression) +- [Use FetchXML to construct a query](https://learn.microsoft.com/power-apps/developer/data-platform/use-fetchxml-construct-query) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/ExportDataUsingFetchXmlToAnnotation/ExportDataUsingFetchXmlToAnnotation.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/ExportDataUsingFetchXmlToAnnotation/ExportDataUsingFetchXmlToAnnotation.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/ExportDataUsingFetchXmlToAnnotation/ExportDataUsingFetchXmlToAnnotation.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/ExportDataUsingFetchXmlToAnnotation/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/ExportDataUsingFetchXmlToAnnotation/Program.cs new file mode 100644 index 00000000..bc143d2c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/ExportDataUsingFetchXmlToAnnotation/Program.cs @@ -0,0 +1,326 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; +using System.Data; +using System.Text; +using System.Xml; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + class Program + { + private static readonly List entityStore = new(); + private static Guid annotationId; + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Setup: Creating sample account records for export..."); + + // Create 5 sample accounts to demonstrate data export + for (int i = 1; i <= 5; i++) + { + var account = new Entity("account") + { + ["name"] = $"Sample Export Account {i}", + ["emailaddress1"] = $"export{i}@contoso.com", + ["telephone1"] = $"555-010{i}", + ["address1_city"] = $"City {i}" + }; + Guid accountId = service.Create(account); + entityStore.Add(new EntityReference("account", accountId)); + } + + Console.WriteLine($"Created {entityStore.Count} sample accounts."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Demonstrating export of query results to annotation (note)..."); + Console.WriteLine(); + + // Create a FetchXML query to retrieve accounts + // This query will select name, email, phone, and city from accounts + string fetchXml = @" + + + + + + + + + + + "; + + Console.WriteLine("Step 1: Executing FetchXML query to retrieve account records..."); + var fetchedRecords = FetchAllDataFromFetchXml(fetchXml, service); + Console.WriteLine($"Retrieved {fetchedRecords.Count} records."); + Console.WriteLine(); + + Console.WriteLine("Step 2: Converting entity data to CSV format..."); + var csvString = ConvertEntitiesToCsv(fetchedRecords); + Console.WriteLine("CSV conversion complete."); + Console.WriteLine(); + Console.WriteLine("CSV Preview (first 500 characters):"); + Console.WriteLine(csvString.Length > 500 ? csvString.Substring(0, 500) + "..." : csvString); + Console.WriteLine(); + + Console.WriteLine("Step 3: Creating annotation (note) record with CSV data..."); + annotationId = CreateAnnotationWithCsvData(service, csvString); + entityStore.Add(new EntityReference("annotation", annotationId)); + Console.WriteLine($"Created annotation with ID: {annotationId}"); + Console.WriteLine(); + + Console.WriteLine("Export complete! The CSV data has been saved as an annotation."); + Console.WriteLine("You can view this annotation in Dataverse under the Notes section."); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("\nCleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Records deleted."); + } + } + + /// + /// Retrieves all records defined by a FetchXML query, handling paging automatically. + /// + /// The FetchXML query definition. + /// The ServiceClient instance. + /// A list of all entities retrieved by the query. + private static List FetchAllDataFromFetchXml(string fetchXml, ServiceClient service) + { + List allRecords = new List(); + int pageNumber = 1; + string? pagingCookie = null; + + // Retrieve first page + EntityCollection result = service.RetrieveMultiple(new FetchExpression(fetchXml)); + allRecords.AddRange(result.Entities); + + // Continue retrieving pages while more records exist + while (result.MoreRecords) + { + pageNumber++; + pagingCookie = result.PagingCookie; + + // Modify FetchXML to include paging information + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(fetchXml); + var attributes = xmlDoc.DocumentElement!.Attributes!; + + if (!string.IsNullOrEmpty(pagingCookie)) + { + var cookieAttribute = xmlDoc.CreateAttribute("paging-cookie"); + cookieAttribute.Value = pagingCookie; + attributes.SetNamedItem(cookieAttribute); + } + + var pageAttribute = attributes.GetNamedItem("page"); + if (pageAttribute != null) + { + pageAttribute.Value = pageNumber.ToString(); + } + else + { + pageAttribute = xmlDoc.CreateAttribute("page"); + pageAttribute.Value = pageNumber.ToString(); + attributes.SetNamedItem(pageAttribute); + } + + // Retrieve next page + using (var stringWriter = new StringWriter()) + using (var xmlTextWriter = XmlWriter.Create(stringWriter)) + { + xmlDoc.WriteTo(xmlTextWriter); + xmlTextWriter.Flush(); + result = service.RetrieveMultiple(new FetchExpression(stringWriter.GetStringBuilder().ToString())); + allRecords.AddRange(result.Entities); + } + } + + return allRecords; + } + + /// + /// Converts a list of entities to CSV format. + /// + /// The entities to convert. + /// A CSV string representation of the entity data. + private static string ConvertEntitiesToCsv(List entities) + { + if (entities.Count == 0) + { + return string.Empty; + } + + // Build a DataTable to organize the entity data + DataTable dataTable = new DataTable(); + + // Populate the DataTable with entity attributes + foreach (var entity in entities) + { + var dataRow = dataTable.NewRow(); + foreach (var attribute in entity.Attributes) + { + // Add column if it doesn't exist + if (!dataTable.Columns.Contains(attribute.Key)) + { + dataTable.Columns.Add(attribute.Key); + } + + // Serialize the attribute value appropriately + dataRow[attribute.Key] = SerializeAttributeValue(attribute.Value); + } + dataTable.Rows.Add(dataRow); + } + + // Convert DataTable to CSV + return ConvertDataTableToCsv(dataTable); + } + + /// + /// Serializes Dataverse attribute values to string format suitable for CSV. + /// + /// The attribute value to serialize. + /// A string representation of the value. + private static string SerializeAttributeValue(object value) + { + return value switch + { + Money money => money.Value.ToString(), + OptionSetValue optionSetValue => optionSetValue.Value.ToString(), + EntityReference entityReference => entityReference.Id.ToString(), + OptionSetValueCollection optionSetValueCollection => + string.Join(",", optionSetValueCollection.Select(option => option.Value)), + DateTime datetime => datetime.ToString("yyyy-MM-ddTHH:mm:ssZ"), + AliasedValue aliasedValue => SerializeAttributeValue(aliasedValue.Value), + _ => value?.ToString() ?? string.Empty + }; + } + + /// + /// Converts a DataTable to CSV format. + /// + /// The DataTable to convert. + /// A CSV string. + private static string ConvertDataTableToCsv(DataTable dataTable) + { + var csv = new StringBuilder(); + + // Add header row with column names + var columnNames = dataTable.Columns.Cast().Select(column => column.ColumnName); + csv.AppendLine(string.Join(",", columnNames)); + + // Add data rows + foreach (DataRow row in dataTable.Rows) + { + var fields = row.ItemArray.Select(field => + { + // Escape quotes and wrap in quotes for CSV format + string fieldValue = field?.ToString() ?? string.Empty; + return $"\"{fieldValue.Replace("\"", "\"\"")}\""; + }); + csv.AppendLine(string.Join(",", fields)); + } + + return csv.ToString(); + } + + /// + /// Creates an annotation (note) record with CSV data as an attachment. + /// + /// The ServiceClient instance. + /// The CSV data to store in the annotation. + /// The ID of the created annotation record. + private static Guid CreateAnnotationWithCsvData(ServiceClient service, string csvString) + { + Entity annotation = new Entity("annotation") + { + ["subject"] = "Export Data Using FetchXml To Csv", + ["documentbody"] = Convert.ToBase64String(Encoding.UTF8.GetBytes(csvString)), + ["filename"] = "exportdatausingfetchxml.csv", + ["mimetype"] = "text/csv" + }; + + return service.Create(annotation); + } + + #endregion + + #region Application Setup + + IConfiguration Configuration { get; } + + Program() + { + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + Console.WriteLine(); + Console.WriteLine("Stack Trace:"); + Console.WriteLine(ex.StackTrace); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/ExportDataUsingFetchXmlToAnnotation/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/ExportDataUsingFetchXmlToAnnotation/README.md new file mode 100644 index 00000000..97ef66bf --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/ExportDataUsingFetchXmlToAnnotation/README.md @@ -0,0 +1,182 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates exporting FetchXML query results to CSV format and storing them as annotation (note) attachments" +--- + +# ExportDataUsingFetchXmlToAnnotation + +Demonstrates exporting FetchXML query results to CSV format and storing them as annotation (note) attachments in Dataverse + +## What this sample does + +This sample shows how to: +- Execute FetchXML queries to retrieve entity records +- Handle FetchXML paging automatically to retrieve all records +- Convert entity data to CSV format +- Handle various Dataverse data types (Money, EntityReference, OptionSetValue, DateTime, etc.) +- Create annotation (note) records with CSV file attachments +- Store base64-encoded file data in the documentbody attribute + +This pattern is useful for: +- Creating data exports that can be viewed or downloaded from Dataverse +- Generating reports as CSV files attached to records +- Creating audit trails or data snapshots +- Building custom export functionality + +## How this sample works + +### Setup + +The setup process: +1. Creates 5 sample account records with name, email, phone, and city data +2. Stores entity references in entityStore for cleanup + +### Run + +The main demonstration: +1. **Execute FetchXML Query**: Creates a FetchXML query to retrieve accounts matching "Sample Export Account" +2. **Fetch All Data with Paging**: Calls `FetchAllDataFromFetchXml()` which: + - Executes the initial FetchXML query + - Checks for MoreRecords flag + - Automatically handles paging by modifying FetchXML with paging-cookie and page attributes + - Continues retrieving pages until all records are fetched +3. **Convert to CSV**: Calls `ConvertEntitiesToCsv()` which: + - Builds a DataTable from entity attributes + - Dynamically adds columns for all attributes found + - Serializes Dataverse-specific types (Money, EntityReference, OptionSetValue, DateTime, AliasedValue) + - Converts DataTable to CSV format with proper quoting and escaping +4. **Create Annotation**: Calls `CreateAnnotationWithCsvData()` which: + - Creates an annotation entity + - Sets subject and filename attributes + - Encodes CSV string to base64 and stores in documentbody + - Sets mimetype to "text/csv" + - Returns the annotation ID + +### Cleanup + +The cleanup process deletes all created records (accounts and annotation) in reverse order. + +## Demonstrates + +This sample demonstrates: +- **FetchXML**: Building and executing XML-based queries +- **Automatic Paging**: Handling large result sets with paging-cookie +- **Data Type Serialization**: Converting Dataverse types to string format +- **CSV Generation**: Creating properly formatted CSV with headers and quoted fields +- **Annotations**: Creating note records with file attachments +- **Base64 Encoding**: Encoding file content for storage in documentbody +- **Dynamic Schema**: Handling entities with varying attributes +- **Late-Bound Entities**: Using Entity class without early-bound types +- **EntityStore Pattern**: Tracking created entities for cleanup + +## Key Methods + +### FetchAllDataFromFetchXml +Retrieves all records from a FetchXML query by automatically handling paging: +- Uses RetrieveMultiple with FetchExpression +- Checks MoreRecords flag +- Modifies FetchXML XML to add paging-cookie and page attributes +- Loops until all records are retrieved + +### ConvertEntitiesToCsv +Converts entity collection to CSV format: +- Builds DataTable dynamically based on entity attributes +- Handles all entities in collection, even with different attribute sets +- Calls SerializeAttributeValue for each attribute +- Converts DataTable to CSV with proper formatting + +### SerializeAttributeValue +Converts Dataverse attribute values to string format: +- **Money**: Extracts decimal value +- **OptionSetValue**: Extracts integer value +- **EntityReference**: Extracts GUID +- **OptionSetValueCollection**: Joins multiple values with comma +- **DateTime**: Formats as ISO 8601 (yyyy-MM-ddTHH:mm:ssZ) +- **AliasedValue**: Recursively serializes inner value +- **Other types**: Uses ToString() + +### ConvertDataTableToCsv +Converts DataTable to CSV string: +- Creates header row with column names +- Wraps all fields in quotes for CSV compliance +- Escapes internal quotes by doubling them (" → "") +- Joins fields with commas + +### CreateAnnotationWithCsvData +Creates annotation record with CSV attachment: +- Sets subject for identification +- Sets filename with .csv extension +- Converts CSV string to UTF8 bytes +- Encodes bytes to base64 string +- Stores in documentbody attribute +- Sets mimetype to text/csv + +## Sample Output + +``` +Connected to Dataverse. + +Setup: Creating sample account records for export... +Created 5 sample accounts. + +Demonstrating export of query results to annotation (note)... + +Step 1: Executing FetchXML query to retrieve account records... +Retrieved 5 records. + +Step 2: Converting entity data to CSV format... +CSV conversion complete. + +CSV Preview (first 500 characters): +name,emailaddress1,telephone1,address1_city +"Sample Export Account 1","export1@contoso.com","555-0101","City 1" +"Sample Export Account 2","export2@contoso.com","555-0102","City 2" +"Sample Export Account 3","export3@contoso.com","555-0103","City 3" +"Sample Export Account 4","export4@contoso.com","555-0104","City 4" +"Sample Export Account 5","export5@contoso.com","555-0105","City 5" + +Step 3: Creating annotation (note) record with CSV data... +Created annotation with ID: 12345678-abcd-1234-abcd-123456789012 + +Export complete! The CSV data has been saved as an annotation. +You can view this annotation in Dataverse under the Notes section. + +Cleaning up... +Deleting 6 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## Real-World Use Cases + +This pattern can be applied to: +1. **Custom Export Reports**: Create scheduled exports of business data +2. **Audit Trails**: Generate snapshots of data at specific points in time +3. **Integration**: Prepare data for external systems that consume CSV +4. **User Downloads**: Allow users to export filtered data from custom pages +5. **Backup Annotations**: Store data backups as attachments to configuration records + +## Extension Ideas + +To extend this sample: +- Add filtering parameters to customize the FetchXML query +- Support exporting to other formats (Excel, JSON, XML) +- Associate annotations with specific records (set objectid and objecttypecode) +- Compress large CSV files before storing +- Add metadata headers to CSV (export date, user, filter criteria) +- Implement incremental exports using modification dates +- Add support for linked entities in FetchXML +- Handle binary attributes (images, files) + +## See also + +[Use FetchXML to construct a query](https://learn.microsoft.com/power-apps/developer/data-platform/use-fetchxml-construct-query) +[Page large result sets with FetchXML](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/page-large-result-sets-with-fetchxml) +[Work with annotations (notes)](https://learn.microsoft.com/power-apps/developer/data-platform/annotation-note-entity) +[Query data using the SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/entity-operations-query-data) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueriesUsingLINQ/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/QueriesUsingLINQ/Program.cs new file mode 100644 index 00000000..f439b982 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueriesUsingLINQ/Program.cs @@ -0,0 +1,628 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Client; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates querying data using Language-Integrated Query (LINQ) + /// + /// + /// This sample shows various LINQ query patterns including: + /// - Simple where clauses and filtering + /// - Joins (inner, left, self) + /// - Operators (equals, not equals, greater than, contains, StartsWith, EndsWith) + /// - Ordering and paging + /// - String and math operations + /// - Late-bound entity queries + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating sample data..."); + + // Create contacts for LINQ samples + var contact1 = new Entity("contact") + { + ["firstname"] = "Colin", + ["lastname"] = "Wilcox", + ["address1_city"] = "Redmond", + ["address1_stateorprovince"] = "WA", + ["address1_postalcode"] = "98052", + ["anniversary"] = new DateTime(2010, 3, 5), + ["creditlimit"] = new Money(300), + ["description"] = "Alpine Ski House", + ["numberofchildren"] = 1, + ["address1_latitude"] = 47.6741667, + ["address1_longitude"] = -122.1202778 + }; + Guid contactId1 = service.Create(contact1); + entityStore.Add(new EntityReference("contact", contactId1)); + + var contact2 = new Entity("contact") + { + ["firstname"] = "Brian", + ["lastname"] = "Smith", + ["address1_city"] = "Bellevue", + ["address1_stateorprovince"] = "WA", + ["address1_postalcode"] = "98008", + ["anniversary"] = new DateTime(2010, 4, 5), + ["creditlimit"] = new Money(30000), + ["description"] = "Coho Winery", + ["numberofchildren"] = 2, + ["address1_latitude"] = 47.6105556, + ["address1_longitude"] = -122.1994444 + }; + Guid contactId2 = service.Create(contact2); + entityStore.Add(new EntityReference("contact", contactId2)); + + var contact3 = new Entity("contact") + { + ["firstname"] = "Darren", + ["lastname"] = "Parker", + ["address1_city"] = "Kirkland", + ["address1_stateorprovince"] = "WA", + ["address1_postalcode"] = "98033", + ["anniversary"] = new DateTime(2010, 10, 5), + ["creditlimit"] = new Money(10000), + ["description"] = "Coho Winery", + ["numberofchildren"] = 2 + }; + Guid contactId3 = service.Create(contact3); + entityStore.Add(new EntityReference("contact", contactId3)); + + var contact4 = new Entity("contact") + { + ["firstname"] = "Ben", + ["lastname"] = "Smith", + ["address1_city"] = "Kirkland", + ["address1_stateorprovince"] = "WA", + ["anniversary"] = new DateTime(2010, 7, 5), + ["creditlimit"] = new Money(12000), + ["description"] = "Coho Winery", + ["numberofchildren"] = 2, + ["creditonhold"] = true + }; + Guid contactId4 = service.Create(contact4); + entityStore.Add(new EntityReference("contact", contactId4)); + + // Create accounts + var account1 = new Entity("account") + { + ["name"] = "Coho Winery", + ["address1_name"] = "Coho Vineyard & Winery", + ["address1_city"] = "Redmond" + }; + Guid accountId1 = service.Create(account1); + entityStore.Add(new EntityReference("account", accountId1)); + + var lead = new Entity("lead") + { + ["firstname"] = "Diogo", + ["lastname"] = "Andrade" + }; + Guid leadId = service.Create(lead); + entityStore.Add(new EntityReference("lead", leadId)); + + var account2 = new Entity("account") + { + ["name"] = "Contoso Ltd", + ["parentaccountid"] = new EntityReference("account", accountId1), + ["address1_name"] = "Contoso Pharmaceuticals", + ["address1_city"] = "Redmond", + ["originatingleadid"] = new EntityReference("lead", leadId), + ["primarycontactid"] = new EntityReference("contact", contactId2) + }; + Guid accountId2 = service.Create(account2); + entityStore.Add(new EntityReference("account", accountId2)); + + // Create additional accounts for simple queries + var account3 = new Entity("account") + { + ["name"] = "Fourth Coffee", + ["address1_stateorprovince"] = "Colorado" + }; + entityStore.Add(new EntityReference("account", service.Create(account3))); + + var account4 = new Entity("account") + { + ["name"] = "School of Fine Art", + ["address1_stateorprovince"] = "Illinois", + ["address1_county"] = "Lake County" + }; + entityStore.Add(new EntityReference("account", service.Create(account4))); + + var account5 = new Entity("account") + { + ["name"] = "Tailspin Toys", + ["address1_stateorprovince"] = "Washington", + ["address1_county"] = "King County" + }; + entityStore.Add(new EntityReference("account", service.Create(account5))); + + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("=".PadRight(70, '=')); + Console.WriteLine("LINQ Query Examples"); + Console.WriteLine("=".PadRight(70, '=')); + Console.WriteLine(); + + using (OrganizationServiceContext orgContext = new OrganizationServiceContext(service)) + { + // Example 1: Simple where clause with Contains + Console.WriteLine("1. Simple where clause - Accounts containing 'Contoso'"); + Console.WriteLine("-".PadRight(70, '-')); + var query1 = from a in orgContext.CreateQuery("account") + where ((string)a["name"]).Contains("Contoso") + select new + { + Name = a["name"], + City = a.GetAttributeValue("address1_city") + }; + foreach (var a in query1) + { + Console.WriteLine($" {a.Name} - {a.City}"); + } + Console.WriteLine(); + + // Example 2: Multiple where clauses + Console.WriteLine("2. Multiple where clauses - Contoso in Redmond"); + Console.WriteLine("-".PadRight(70, '-')); + var query2 = from a in orgContext.CreateQuery("account") + where ((string)a["name"]).Contains("Contoso") + where a.GetAttributeValue("address1_city") == "Redmond" + select new + { + Name = a["name"], + City = a["address1_city"] + }; + foreach (var a in query2) + { + Console.WriteLine($" {a.Name} - {a.City}"); + } + Console.WriteLine(); + + // Example 3: Inner join + Console.WriteLine("3. Inner join - Contacts with their accounts"); + Console.WriteLine("-".PadRight(70, '-')); + var query3 = from c in orgContext.CreateQuery("contact") + join a in orgContext.CreateQuery("account") + on c["contactid"] equals a["primarycontactid"] + select new + { + ContactName = c["fullname"], + AccountName = a["name"] + }; + foreach (var item in query3) + { + Console.WriteLine($" Contact: {item.ContactName}, Account: {item.AccountName}"); + } + Console.WriteLine(); + + // Example 4: Left join + Console.WriteLine("4. Left join - Accounts with and without primary contacts"); + Console.WriteLine("-".PadRight(70, '-')); + var query4 = from a in orgContext.CreateQuery("account") + join c in orgContext.CreateQuery("contact") + on a["primarycontactid"] equals c["contactid"] into gr + from c_joined in gr.DefaultIfEmpty() + select new + { + AccountName = a["name"], + ContactName = c_joined != null ? c_joined.GetAttributeValue("fullname") : "(none)" + }; + foreach (var item in query4) + { + Console.WriteLine($" Account: {item.AccountName}, Contact: {item.ContactName}"); + } + Console.WriteLine(); + + // Example 5: Using Distinct + Console.WriteLine("5. Distinct - Unique contact last names"); + Console.WriteLine("-".PadRight(70, '-')); + var query5 = (from c in orgContext.CreateQuery("contact") + select c.GetAttributeValue("lastname")).Distinct(); + foreach (var lastName in query5) + { + Console.WriteLine($" {lastName}"); + } + Console.WriteLine(); + + // Example 6: Equals operator + Console.WriteLine("6. Equals operator - Contacts named Colin"); + Console.WriteLine("-".PadRight(70, '-')); + var query6 = from c in orgContext.CreateQuery("contact") + where c.GetAttributeValue("firstname") == "Colin" + select new + { + FirstName = c["firstname"], + LastName = c["lastname"], + City = c.GetAttributeValue("address1_city") + }; + foreach (var c in query6) + { + Console.WriteLine($" {c.FirstName} {c.LastName} - {c.City}"); + } + Console.WriteLine(); + + // Example 7: Not equals operator + Console.WriteLine("7. Not equals - Contacts not in Redmond"); + Console.WriteLine("-".PadRight(70, '-')); + var query7 = from c in orgContext.CreateQuery("contact") + where c.GetAttributeValue("address1_city") != "Redmond" + select new + { + FirstName = c["firstname"], + LastName = c["lastname"], + City = c["address1_city"] + }; + foreach (var c in query7) + { + Console.WriteLine($" {c.FirstName} {c.LastName} - {c.City}"); + } + Console.WriteLine(); + + // Example 8: Greater than operator with Money + Console.WriteLine("8. Greater than - Contacts with credit limit > $20,000"); + Console.WriteLine("-".PadRight(70, '-')); + var query8 = from c in orgContext.CreateQuery("contact") + where c.GetAttributeValue("creditlimit") != null + && c.GetAttributeValue("creditlimit").Value > 20000 + select new + { + FirstName = c["firstname"], + LastName = c["lastname"], + CreditLimit = c.GetAttributeValue("creditlimit") + }; + foreach (var c in query8) + { + Console.WriteLine($" {c.FirstName} {c.LastName} - ${c.CreditLimit?.Value}"); + } + Console.WriteLine(); + + // Example 9: Greater than with DateTime + Console.WriteLine("9. Date comparison - Anniversaries after Feb 5, 2010"); + Console.WriteLine("-".PadRight(70, '-')); + var query9 = from c in orgContext.CreateQuery("contact") + where c.GetAttributeValue("anniversary") > new DateTime(2010, 2, 5) + select new + { + FirstName = c["firstname"], + LastName = c["lastname"], + Anniversary = c.GetAttributeValue("anniversary") + }; + foreach (var c in query9) + { + Console.WriteLine($" {c.FirstName} {c.LastName} - {c.Anniversary?.ToShortDateString()}"); + } + Console.WriteLine(); + + // Example 10: Contains operator + Console.WriteLine("10. Contains operator - Description contains 'Alpine'"); + Console.WriteLine("-".PadRight(70, '-')); + var query10 = from c in orgContext.CreateQuery("contact") + where c.GetAttributeValue("description") != null + && ((string)c["description"]).Contains("Alpine") + select new + { + FirstName = c["firstname"], + LastName = c["lastname"] + }; + foreach (var c in query10) + { + Console.WriteLine($" {c.FirstName} {c.LastName}"); + } + Console.WriteLine(); + + // Example 11: StartsWith operator + Console.WriteLine("11. StartsWith - First names starting with 'Bri'"); + Console.WriteLine("-".PadRight(70, '-')); + var query11 = from c in orgContext.CreateQuery("contact") + where c.GetAttributeValue("firstname") != null + && ((string)c["firstname"]).StartsWith("Bri") + select new + { + FirstName = c["firstname"], + LastName = c["lastname"] + }; + foreach (var c in query11) + { + Console.WriteLine($" {c.FirstName} {c.LastName}"); + } + Console.WriteLine(); + + // Example 12: EndsWith operator + Console.WriteLine("12. EndsWith - Last names ending with 'cox'"); + Console.WriteLine("-".PadRight(70, '-')); + var query12 = from c in orgContext.CreateQuery("contact") + where c.GetAttributeValue("lastname") != null + && ((string)c["lastname"]).EndsWith("cox") + select new + { + FirstName = c["firstname"], + LastName = c["lastname"] + }; + foreach (var c in query12) + { + Console.WriteLine($" {c.FirstName} {c.LastName}"); + } + Console.WriteLine(); + + // Example 13: AND/OR operators + Console.WriteLine("13. AND/OR operators - (Redmond OR Bellevue) AND CreditLimit >= $200"); + Console.WriteLine("-".PadRight(70, '-')); + var query13 = from c in orgContext.CreateQuery("contact") + where ((c.GetAttributeValue("address1_city") == "Redmond" || + c.GetAttributeValue("address1_city") == "Bellevue") && + c.GetAttributeValue("creditlimit") != null && + c.GetAttributeValue("creditlimit").Value >= 200) + select new + { + FirstName = c["firstname"], + LastName = c["lastname"], + City = c["address1_city"], + CreditLimit = c.GetAttributeValue("creditlimit") + }; + foreach (var c in query13) + { + Console.WriteLine($" {c.FirstName} {c.LastName} - {c.City} - ${c.CreditLimit?.Value}"); + } + Console.WriteLine(); + + // Example 14: OrderBy descending + Console.WriteLine("14. OrderBy descending - Contacts by credit limit"); + Console.WriteLine("-".PadRight(70, '-')); + var query14 = from c in orgContext.CreateQuery("contact") + where c.GetAttributeValue("creditlimit") != null + orderby c.GetAttributeValue("creditlimit").Value descending + select new + { + FirstName = c["firstname"], + LastName = c["lastname"], + CreditLimit = c.GetAttributeValue("creditlimit") + }; + foreach (var c in query14) + { + Console.WriteLine($" ${c.CreditLimit?.Value} - {c.FirstName} {c.LastName}"); + } + Console.WriteLine(); + + // Example 15: Multiple OrderBy + Console.WriteLine("15. Multiple OrderBy - By last name desc, first name asc"); + Console.WriteLine("-".PadRight(70, '-')); + var query15 = from c in orgContext.CreateQuery("contact") + orderby c.GetAttributeValue("lastname") descending, + c.GetAttributeValue("firstname") ascending + select new + { + FirstName = c["firstname"], + LastName = c["lastname"] + }; + foreach (var c in query15) + { + Console.WriteLine($" {c.LastName}, {c.FirstName}"); + } + Console.WriteLine(); + + // Example 16: Skip and Take (paging) + Console.WriteLine("16. Skip and Take - Skip 1, take 2 contacts"); + Console.WriteLine("-".PadRight(70, '-')); + var query16 = (from c in orgContext.CreateQuery("contact") + where c.GetAttributeValue("lastname") != "Parker" + orderby c.GetAttributeValue("firstname") + select new + { + FirstName = c["firstname"], + LastName = c["lastname"] + }).Skip(1).Take(2); + foreach (var c in query16) + { + Console.WriteLine($" {c.FirstName} {c.LastName}"); + } + Console.WriteLine(); + + // Example 17: First + Console.WriteLine("17. First - Get first contact"); + Console.WriteLine("-".PadRight(70, '-')); + var firstContact = orgContext.CreateQuery("contact").First(); + Console.WriteLine($" {firstContact.GetAttributeValue("fullname")}"); + Console.WriteLine(); + + // Example 18: Count with Distinct + Console.WriteLine("18. Count - Number of accounts with county specified"); + Console.WriteLine("-".PadRight(70, '-')); + var accountsWithCounty = (from a in orgContext.CreateQuery("account") + where a.GetAttributeValue("address1_county") != null + select a).ToArray().Count(); + Console.WriteLine($" Accounts with county: {accountsWithCounty}"); + Console.WriteLine(); + + // Example 19: Distinct count of states + Console.WriteLine("19. Distinct count - Unique states with accounts"); + Console.WriteLine("-".PadRight(70, '-')); + var statesWithAccounts = (from a in orgContext.CreateQuery("account") + where a.GetAttributeValue("address1_stateorprovince") != null + select a.GetAttributeValue("address1_stateorprovince")) + .Distinct().ToArray().Count(); + Console.WriteLine($" Unique states: {statesWithAccounts}"); + Console.WriteLine(); + + // Example 20: Self join + Console.WriteLine("20. Self join - Accounts with parent accounts"); + Console.WriteLine("-".PadRight(70, '-')); + var query20 = from a in orgContext.CreateQuery("account") + join a2 in orgContext.CreateQuery("account") + on a["parentaccountid"] equals a2["accountid"] + select new + { + AccountName = a["name"], + ParentName = a2["name"] + }; + foreach (var a in query20) + { + Console.WriteLine($" {a.AccountName} -> Parent: {a.ParentName}"); + } + Console.WriteLine(); + + // Example 21: Multiple joins + Console.WriteLine("21. Multiple joins - Account, Contact, and Lead"); + Console.WriteLine("-".PadRight(70, '-')); + var query21 = from a in orgContext.CreateQuery("account") + join c in orgContext.CreateQuery("contact") + on a["primarycontactid"] equals c["contactid"] + join l in orgContext.CreateQuery("lead") + on a["originatingleadid"] equals l["leadid"] + select new + { + ContactName = c["fullname"], + AccountName = a["name"], + LeadName = l["fullname"] + }; + foreach (var item in query21) + { + Console.WriteLine($" Contact: {item.ContactName}, Account: {item.AccountName}, Lead: {item.LeadName}"); + } + Console.WriteLine(); + + // Example 22: GetAttributeValue method + Console.WriteLine("22. GetAttributeValue - Strongly typed attribute access"); + Console.WriteLine("-".PadRight(70, '-')); + var query22 = from c in orgContext.CreateQuery("contact") + where c.GetAttributeValue("numberofchildren") > 1 + select new + { + FirstName = c.GetAttributeValue("firstname"), + LastName = c.GetAttributeValue("lastname"), + NumberOfChildren = c.GetAttributeValue("numberofchildren") + }; + foreach (var c in query22) + { + Console.WriteLine($" {c.FirstName} {c.LastName} - Children: {c.NumberOfChildren}"); + } + Console.WriteLine(); + + // Example 23: Math operations + Console.WriteLine("23. Math operations - Round, Floor, Ceiling on latitude"); + Console.WriteLine("-".PadRight(70, '-')); + var query23 = from c in orgContext.CreateQuery("contact") + where c.GetAttributeValue("address1_latitude") != null + select new + { + Name = c.GetAttributeValue("fullname"), + Latitude = c.GetAttributeValue("address1_latitude"), + Round = Math.Round(c.GetAttributeValue("address1_latitude")), + Floor = Math.Floor(c.GetAttributeValue("address1_latitude")), + Ceiling = Math.Ceiling(c.GetAttributeValue("address1_latitude")) + }; + foreach (var c in query23) + { + Console.WriteLine($" {c.Name}: {c.Latitude} -> Round:{c.Round}, Floor:{c.Floor}, Ceiling:{c.Ceiling}"); + } + Console.WriteLine(); + } + + Console.WriteLine("=".PadRight(70, '=')); + Console.WriteLine("All LINQ query examples completed successfully!"); + Console.WriteLine("=".PadRight(70, '=')); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine(); + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + // Delete in reverse order to respect dependencies + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + Console.WriteLine(); + Console.WriteLine("Stack Trace:"); + Console.WriteLine(ex.StackTrace); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueriesUsingLINQ/QueriesUsingLINQ.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/QueriesUsingLINQ/QueriesUsingLINQ.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueriesUsingLINQ/QueriesUsingLINQ.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueriesUsingLINQ/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/QueriesUsingLINQ/README.md new file mode 100644 index 00000000..2157753b --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueriesUsingLINQ/README.md @@ -0,0 +1,123 @@ +# Sample: Query data using LINQ + +This sample demonstrates how to query business data using Language-Integrated Query (LINQ) with the Dataverse SDK for .NET. + +## Summary + +This sample shows various LINQ query patterns for querying Dataverse data, including: + +- Simple where clauses and filtering +- Joins (inner, left, self, multiple) +- Comparison operators (equals, not equals, greater than, less than) +- String operations (Contains, StartsWith, EndsWith) +- Logical operators (AND, OR, NOT) +- Ordering and sorting +- Paging with Skip and Take +- Aggregate operations (Count, Distinct) +- Math operations on numeric fields +- GetAttributeValue for strongly-typed attribute access +- Late-bound entity queries using OrganizationServiceContext + +## How to run this sample + +1. Download or clone the sample so that you have a local copy. +2. Open the sample solution in Visual Studio Code or Visual Studio. +3. Update the connection string in the `appsettings.json` file in the Query folder with your Dataverse environment URL and credentials. +4. Build and run the sample using `dotnet run` or F5 in Visual Studio. + +## What this sample does + +The sample demonstrates 23 different LINQ query patterns: + +1. **Simple where clause** - Filter accounts by name containing text +2. **Multiple where clauses** - Combine multiple filter conditions +3. **Inner join** - Join contacts with their associated accounts +4. **Left join** - Retrieve accounts with or without primary contacts +5. **Distinct** - Get unique values from a field +6. **Equals operator** - Exact match filtering +7. **Not equals** - Exclusion filtering +8. **Greater than with Money** - Compare currency fields +9. **Date comparison** - Filter by date ranges +10. **Contains operator** - Partial text matching +11. **StartsWith** - Prefix matching +12. **EndsWith** - Suffix matching +13. **AND/OR operators** - Complex logical conditions +14. **OrderBy descending** - Sort results in descending order +15. **Multiple OrderBy** - Multi-level sorting +16. **Skip and Take** - Implement paging +17. **First** - Get the first record +18. **Count** - Count filtered results +19. **Distinct count** - Count unique values +20. **Self join** - Join a table to itself +21. **Multiple joins** - Join three or more tables +22. **GetAttributeValue** - Strongly-typed attribute access +23. **Math operations** - Use mathematical functions in queries + +## How this sample works + +### Setup + +The sample creates test data including: +- 4 contacts with various attributes (names, addresses, credit limits, etc.) +- 5 accounts with different locations and properties +- 1 lead record +- Relationships between these entities + +### Demonstrate + +The sample executes 23 different LINQ queries, each demonstrating a specific pattern or technique. All queries use late-bound entities with `OrganizationServiceContext`, making them compatible with any Dataverse environment without requiring early-bound classes. + +Key differences from early-bound LINQ: +- Uses `orgContext.CreateQuery("tablename")` instead of typed sets like `ContactSet` +- Accesses attributes using indexers: `entity["attributename"]` +- Uses `GetAttributeValue()` for strongly-typed access +- Handles lookups as `EntityReference` objects +- Requires explicit casting for certain operations + +### Cleanup + +The sample deletes all created records in reverse order to respect entity dependencies. The cleanup prompt can be disabled by setting `deleteCreatedRecords = false` in the code. + +## Key Concepts + +### OrganizationServiceContext + +The `OrganizationServiceContext` class provides a LINQ query provider for Dataverse. It translates LINQ queries into QueryExpression objects that are executed against the Dataverse API. + +### Late-Bound Entities + +This sample uses late-bound entities, which means: +- No need for early-bound classes generated by CrmSvcUtil +- Works with any Dataverse environment +- Attributes accessed by string names +- Less compile-time type safety but more flexibility + +### Query Translation + +LINQ queries are translated to QueryExpression at runtime. Not all LINQ features are supported - only those that can be translated to valid QueryExpression operations. + +Supported operations: +- Where, Select, OrderBy, ThenBy +- Skip, Take, First, FirstOrDefault, Single, SingleOrDefault +- Count, Distinct +- Join (inner and left) +- String methods: Contains, StartsWith, EndsWith +- Comparison operators: ==, !=, >, <, >=, <= +- Math functions: Round, Floor, Ceiling, Abs + +Unsupported operations that will throw exceptions: +- GroupBy +- Aggregate functions other than Count +- Complex expressions that can't be translated +- Client-side evaluation (all operations must be server-side) + +## Related Documentation + +- [Query data using LINQ](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/linq-query-examples) +- [Use LINQ to construct a query](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/use-linq-construct-query) +- [Late-bound and early-bound programming](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/early-bound-programming) +- [Build queries with LINQ (.NET language-integrated query)](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/build-queries-with-linq-net-language-integrated-query) + +## See Also + +[Query data using the SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/entity-operations-query-data) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryByReciprocalRole/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByReciprocalRole/Program.cs new file mode 100644 index 00000000..ffdd6627 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByReciprocalRole/Program.cs @@ -0,0 +1,235 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates querying connection roles by reciprocal role + /// + /// + /// This sample shows how to query for connection roles that have a specific + /// role listed as a reciprocal role, using QueryExpression with LinkEntity. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + private static Guid primaryConnectionRoleId; + private static Guid reciprocalConnectionRoleId; + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating sample data..."); + + // Define connection role categories + int businessCategory = 1; + + // Create the primary connection role + var primaryConnectionRole = new Entity("connectionrole") + { + ["name"] = "Example Primary Connection Role", + ["category"] = new OptionSetValue(businessCategory) + }; + + primaryConnectionRoleId = service.Create(primaryConnectionRole); + entityStore.Add(new EntityReference("connectionrole", primaryConnectionRoleId)); + Console.WriteLine("Created primary connection role: {0}", primaryConnectionRole["name"]); + + // Create a related Connection Role Object Type Code record for Account + // on the primary role + var accountPrimaryConnectionRoleTypeCode = new Entity("connectionroleobjecttypecode") + { + ["connectionroleid"] = new EntityReference("connectionrole", primaryConnectionRoleId), + ["associatedobjecttypecode"] = "account" + }; + + Guid primaryTypeCodeId = service.Create(accountPrimaryConnectionRoleTypeCode); + entityStore.Add(new EntityReference("connectionroleobjecttypecode", primaryTypeCodeId)); + Console.WriteLine("Created Connection Role Object Type Code for Account on primary role."); + + // Create the reciprocal connection role + var reciprocalConnectionRole = new Entity("connectionrole") + { + ["name"] = "Example Reciprocal Connection Role", + ["category"] = new OptionSetValue(businessCategory) + }; + + reciprocalConnectionRoleId = service.Create(reciprocalConnectionRole); + entityStore.Add(new EntityReference("connectionrole", reciprocalConnectionRoleId)); + Console.WriteLine("Created reciprocal connection role: {0}", reciprocalConnectionRole["name"]); + + // Create a related Connection Role Object Type Code record for Account + // on the reciprocal role + var accountReciprocalConnectionRoleTypeCode = new Entity("connectionroleobjecttypecode") + { + ["connectionroleid"] = new EntityReference("connectionrole", reciprocalConnectionRoleId), + ["associatedobjecttypecode"] = "account" + }; + + Guid reciprocalTypeCodeId = service.Create(accountReciprocalConnectionRoleTypeCode); + entityStore.Add(new EntityReference("connectionroleobjecttypecode", reciprocalTypeCodeId)); + Console.WriteLine("Created Connection Role Object Type Code for Account on reciprocal role."); + + // Associate the connection roles using the connectionroleassociation relationship + var associateRequest = new AssociateRequest + { + Target = new EntityReference("connectionrole", primaryConnectionRoleId), + RelatedEntities = new EntityReferenceCollection() + { + new EntityReference("connectionrole", reciprocalConnectionRoleId) + }, + Relationship = new Relationship() + { + PrimaryEntityRole = EntityRole.Referencing, + SchemaName = "connectionroleassociation_association" + } + }; + + service.Execute(associateRequest); + Console.WriteLine("Associated primary and reciprocal connection roles."); + + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Querying for connection roles by reciprocal role..."); + Console.WriteLine(); + + // This query retrieves all connection roles that have the specified role + // listed as a reciprocal role. + var query = new QueryExpression + { + EntityName = "connectionrole", + ColumnSet = new ColumnSet("connectionroleid", "name"), + LinkEntities = + { + new LinkEntity + { + JoinOperator = JoinOperator.Inner, + LinkFromEntityName = "connectionrole", + LinkFromAttributeName = "connectionroleid", + LinkToEntityName = "connectionroleassociation", + LinkToAttributeName = "connectionroleid", + LinkCriteria = new FilterExpression + { + FilterOperator = LogicalOperator.And, + Conditions = + { + new ConditionExpression + { + AttributeName = "associatedconnectionroleid", + Operator = ConditionOperator.Equal, + Values = { reciprocalConnectionRoleId } + } + } + } + } + } + }; + + EntityCollection results = service.RetrieveMultiple(query); + + Console.WriteLine("Retrieved {0} connection role(s) with reciprocal role association.", results.Entities.Count); + Console.WriteLine(); + + foreach (Entity role in results.Entities) + { + Console.WriteLine("Connection Role ID: {0}", role["connectionroleid"]); + Console.WriteLine("Connection Role Name: {0}", role["name"]); + Console.WriteLine(); + } + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + + // Delete in reverse order to handle dependencies + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryByReciprocalRole/QueryByReciprocalRole.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByReciprocalRole/QueryByReciprocalRole.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByReciprocalRole/QueryByReciprocalRole.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryByReciprocalRole/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByReciprocalRole/README.md new file mode 100644 index 00000000..1b5af3e0 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByReciprocalRole/README.md @@ -0,0 +1,91 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates querying connection roles by reciprocal role" +--- + +# QueryByReciprocalRole + +Demonstrates querying connection roles by reciprocal role + +## What this sample does + +This sample shows how to: +- Create connection roles with reciprocal associations +- Query for connection roles using the connectionroleassociation relationship +- Use QueryExpression with LinkEntity to filter by associated connection role +- Work with connection role object type codes + +Connection roles define the types of relationships that can exist between records in Dataverse. Reciprocal roles define the inverse relationship (e.g., "Manager" and "Report"). + +## How this sample works + +### Setup + +The setup process: +1. Creates a primary connection role ("Example Primary Connection Role") +2. Creates a connection role object type code associating the primary role with the Account entity +3. Creates a reciprocal connection role ("Example Reciprocal Connection Role") +4. Creates a connection role object type code associating the reciprocal role with the Account entity +5. Associates the primary and reciprocal roles using the connectionroleassociation relationship + +### Run + +The main demonstration: +1. Creates a QueryExpression for the "connectionrole" entity +2. Adds a LinkEntity to join with the "connectionroleassociation" entity +3. Filters by the reciprocal connection role ID in the association +4. Executes RetrieveMultiple to retrieve all connection roles that have the specified reciprocal role +5. Displays the retrieved connection role IDs and names + +### Cleanup + +The cleanup process deletes all created connection roles and connection role object type codes in reverse order to handle dependencies. + +## Demonstrates + +This sample demonstrates: +- **Connection Roles**: Creating and configuring connection roles +- **Connection Role Associations**: Associating connection roles as reciprocals +- **AssociateRequest**: Using the Associate message to create many-to-many relationships +- **QueryExpression with LinkEntity**: Querying across related entities +- **Connection Role Object Type Codes**: Defining which entity types a role applies to +- **Entity-based syntax**: Using late-bound Entity objects with string-based attribute access + +## Sample Output + +``` +Connected to Dataverse. + +Creating sample data... +Created primary connection role: Example Primary Connection Role +Created Connection Role Object Type Code for Account on primary role. +Created reciprocal connection role: Example Reciprocal Connection Role +Created Connection Role Object Type Code for Account on reciprocal role. +Associated primary and reciprocal connection roles. +Setup complete. + +Querying for connection roles by reciprocal role... + +Retrieved 1 connection role(s) with reciprocal role association. + +Connection Role ID: +Connection Role Name: Example Primary Connection Role + +Cleaning up... +Deleting 4 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Connection roles](https://learn.microsoft.com/power-apps/developer/data-platform/connection-entities) +[Build queries with QueryExpression](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/build-queries-with-queryexpression) +[Join tables using QueryExpression](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/join-tables-using-queryexpression) +[Use the Associate message](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/entity-operations-associate-disassociate) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryByRecord/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByRecord/Program.cs new file mode 100644 index 00000000..578ffa7f --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByRecord/Program.cs @@ -0,0 +1,256 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates querying connections for a specific record + /// + /// + /// This sample shows how to query connection records to find all connections + /// that a specific entity record is part of using QueryExpression. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Setting up sample data..."); + + // Define connection categories + var Categories = new + { + Business = 1, + Family = 2, + Social = 3, + Sales = 4, + Other = 5 + }; + + // Create a Connection Role + var setupConnectionRole = new Entity("connectionrole") + { + ["name"] = "Example Connection Role", + ["description"] = "This is an example one sided connection role.", + ["category"] = new OptionSetValue(Categories.Business) + }; + + Guid connectionRoleId = service.Create(setupConnectionRole); + entityStore.Add(new EntityReference("connectionrole", connectionRoleId)); + Console.WriteLine("Created connection role: {0}", setupConnectionRole["name"]); + + // Create a related Connection Role Object Type Code record for Account + var newAccountConnectionRoleTypeCode = new Entity("connectionroleobjecttypecode") + { + ["connectionroleid"] = new EntityReference("connectionrole", connectionRoleId), + ["associatedobjecttypecode"] = "account" + }; + + Guid accountTypeCodeId = service.Create(newAccountConnectionRoleTypeCode); + entityStore.Add(new EntityReference("connectionroleobjecttypecode", accountTypeCodeId)); + Console.WriteLine("Created a related Connection Role Object Type Code record for Account."); + + // Create a related Connection Role Object Type Code record for Contact + var newContactConnectionRoleTypeCode = new Entity("connectionroleobjecttypecode") + { + ["connectionroleid"] = new EntityReference("connectionrole", connectionRoleId), + ["associatedobjecttypecode"] = "contact" + }; + + Guid contactTypeCodeId = service.Create(newContactConnectionRoleTypeCode); + entityStore.Add(new EntityReference("connectionroleobjecttypecode", contactTypeCodeId)); + Console.WriteLine("Created a related Connection Role Object Type Code record for Contact."); + + // Create a few account records for use in the connections + var setupAccount1 = new Entity("account") + { + ["name"] = "Example Account 1" + }; + Guid account1Id = service.Create(setupAccount1); + entityStore.Add(new EntityReference("account", account1Id)); + Console.WriteLine("Created {0}.", setupAccount1["name"]); + + var setupAccount2 = new Entity("account") + { + ["name"] = "Example Account 2" + }; + Guid account2Id = service.Create(setupAccount2); + entityStore.Add(new EntityReference("account", account2Id)); + Console.WriteLine("Created {0}.", setupAccount2["name"]); + + // Create a contact used in the connection + var setupContact = new Entity("contact") + { + ["lastname"] = "Example Contact" + }; + Guid contactId = service.Create(setupContact); + entityStore.Add(new EntityReference("contact", contactId)); + Console.WriteLine("Created contact: {0}.", setupContact["lastname"]); + + // Create a new connection between Account 1 and the contact record + var newConnection1 = new Entity("connection") + { + ["record1id"] = new EntityReference("account", account1Id), + ["record1roleid"] = new EntityReference("connectionrole", connectionRoleId), + ["record2id"] = new EntityReference("contact", contactId) + }; + + Guid connection1Id = service.Create(newConnection1); + entityStore.Add(new EntityReference("connection", connection1Id)); + Console.WriteLine("Created a connection between account 1 and the contact."); + + // Create a new connection between the contact and Account 2 record + var newConnection2 = new Entity("connection") + { + ["record1id"] = new EntityReference("contact", contactId), + ["record1roleid"] = new EntityReference("connectionrole", connectionRoleId), + ["record2id"] = new EntityReference("account", account2Id) + }; + + Guid connection2Id = service.Create(newConnection2); + entityStore.Add(new EntityReference("connection", connection2Id)); + Console.WriteLine("Created a connection between the contact and account 2."); + + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Query Connections By Record"); + Console.WriteLine("==============================="); + + // Get the contact ID from our entity store + // The contact is at index 5 (after connection role, 2 type codes, and 2 accounts) + Guid contactId = entityStore[5].Id; + + // This query retrieves all connections this contact is part of + var query = new QueryExpression + { + EntityName = "connection", + ColumnSet = new ColumnSet("connectionid"), + Criteria = new FilterExpression + { + FilterOperator = LogicalOperator.And, + Conditions = + { + // You can safely query against only record1id or + // record2id - Dataverse will find all connections this + // entity is a part of either way. + new ConditionExpression + { + AttributeName = "record1id", + Operator = ConditionOperator.Equal, + Values = { contactId } + } + } + } + }; + + EntityCollection results = service.RetrieveMultiple(query); + + // Here you could do a variety of tasks with the + // connections retrieved, such as listing the connected entities, + // finding reciprocal connections, etc. + + Console.WriteLine("Retrieved {0} connection instances for the contact.", results.Entities.Count); + + foreach (var connection in results.Entities) + { + Console.WriteLine(" Connection ID: {0}", connection.Id); + } + + Console.WriteLine("==============================="); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + // Delete in reverse order to handle dependencies + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryByRecord/QueryByRecord.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByRecord/QueryByRecord.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByRecord/QueryByRecord.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryByRecord/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByRecord/README.md new file mode 100644 index 00000000..e7e6a8a3 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryByRecord/README.md @@ -0,0 +1,102 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates querying connections for a specific record" +--- + +# QueryByRecord + +Demonstrates querying connections for a specific record + +## What this sample does + +This sample shows how to: +- Query connection records using QueryExpression to find all connections a specific entity record is part of +- Create connection roles and associated object type codes +- Create connections between different entity records +- Use QueryExpression with FilterExpression to filter by record ID + +Connection records link two records together in Dataverse. This sample demonstrates how to query connections to find all related records, regardless of whether the target record is in the record1id or record2id position. + +## How this sample works + +### Setup + +The setup process: +1. Creates a connection role with "Business" category +2. Creates connection role object type codes for both Account and Contact entities +3. Creates two account records ("Example Account 1" and "Example Account 2") +4. Creates a contact record ("Example Contact") +5. Creates a connection between Account 1 and the Contact +6. Creates a connection between the Contact and Account 2 + +### Run + +The main demonstration: +1. Retrieves the contact ID from the entity store +2. Creates a QueryExpression to query the "connection" entity +3. Adds a filter condition on "record1id" to find connections where the contact is record1 +4. Executes RetrieveMultiple to retrieve matching connection records +5. Displays the count and IDs of all connections found + +**Important**: Dataverse automatically creates reciprocal connection records, so querying against just record1id will find all connections the entity is part of, whether it's in the record1 or record2 position. + +### Cleanup + +The cleanup process deletes all created records in reverse order to handle dependencies: +- Connection records +- Contact record +- Account records +- Connection role object type codes +- Connection role + +## Demonstrates + +This sample demonstrates: +- **QueryExpression**: Building queries with criteria and column sets +- **FilterExpression**: Adding filter conditions to queries +- **ConditionExpression**: Specifying attribute-based filter criteria +- **Connection Entity**: Working with connection records to link entities +- **ConnectionRole**: Creating and using connection roles +- **EntityReference**: Using late-bound entity references +- **RetrieveMultiple**: Executing queries to retrieve multiple records + +## Sample Output + +``` +Connected to Dataverse. + +Setting up sample data... +Created connection role: Example Connection Role +Created a related Connection Role Object Type Code record for Account. +Created a related Connection Role Object Type Code record for Contact. +Created Example Account 1. +Created Example Account 2. +Created contact: Example Contact. +Created a connection between account 1 and the contact. +Created a connection between the contact and account 2. +Setup complete. + +Query Connections By Record +=============================== +Retrieved 2 connection instances for the contact. + Connection ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + Connection ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +=============================== +Cleaning up... +Deleting 8 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Connection entities](https://learn.microsoft.com/power-apps/developer/data-platform/connection-entities) +[Build queries with QueryExpression](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/build-queries-with-queryexpression) +[Query data using the SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/entity-operations-query-data) +[Create connections](https://learn.microsoft.com/power-apps/developer/data-platform/configure-connection-roles) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryHoursMultipleUsers/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/QueryHoursMultipleUsers/Program.cs new file mode 100644 index 00000000..8d739daa --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryHoursMultipleUsers/Program.cs @@ -0,0 +1,213 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates querying the working hours of multiple users + /// + /// + /// This sample shows how to retrieve the working hours of multiple users + /// by using the QueryMultipleSchedulesRequest message. + /// + /// IMPORTANT: This sample requires a user manually created in your environment: + /// First Name: Kevin + /// Last Name: Cook + /// Security Role: Sales Manager + /// UserName: kcook@yourorg.onmicrosoft.com + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + // Store user IDs used in the sample + private static Guid _currentUserId; + private static Guid _otherUserId; + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Setting up sample data..."); + + // Get the current user's information + var userRequest = new WhoAmIRequest(); + var userResponse = (WhoAmIResponse)service.Execute(userRequest); + _currentUserId = userResponse.UserId; + Console.WriteLine($"Current User ID: {_currentUserId}"); + + // Try to retrieve the manually created user (Kevin Cook, Sales Manager) + _otherUserId = RetrieveUserByName(service, "Kevin", "Cook"); + + if (_otherUserId == Guid.Empty) + { + throw new Exception( + "Required user not found. Please manually create a user in your environment:\n" + + "First Name: Kevin\n" + + "Last Name: Cook\n" + + "Security Role: Sales Manager\n" + + "UserName: kcook@yourorg.onmicrosoft.com"); + } + + Console.WriteLine($"Found user Kevin Cook with ID: {_otherUserId}"); + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Querying working hours of multiple users..."); + Console.WriteLine(); + + // Create the request to query multiple schedules + var scheduleRequest = new QueryMultipleSchedulesRequest + { + // Specify the resource IDs (users) to query + ResourceIds = new Guid[] { _currentUserId, _otherUserId }, + + // Set the time range for the query (now to 7 days from now) + Start = DateTime.Now, + End = DateTime.Today.AddDays(7), + + // Specify we want available time blocks + TimeCodes = new TimeCode[] { TimeCode.Available } + }; + + // Execute the request + var scheduleResponse = (QueryMultipleSchedulesResponse)service.Execute(scheduleRequest); + + // Verify if some data is returned for the availability of the users + if (scheduleResponse.TimeInfos.Length > 0) + { + Console.WriteLine($"Successfully queried the working hours of {scheduleRequest.ResourceIds.Length} users."); + Console.WriteLine($"Retrieved {scheduleResponse.TimeInfos.Length} time slot(s) with availability information."); + } + else + { + Console.WriteLine("No available time slots found for the users in the next 7 days."); + Console.WriteLine("This may indicate that the users have no working hours configured."); + } + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + + // Note: This sample does not create any records that need to be deleted. + // It only queries existing user working hours information. + + Console.WriteLine("No records to delete."); + } + + #endregion + + #region Helper Methods + + /// + /// Retrieves a user by first and last name + /// + /// The service client + /// The user's first name + /// The user's last name + /// The user's ID, or Guid.Empty if not found + private static Guid RetrieveUserByName(ServiceClient service, string firstName, string lastName) + { + // Query for the user by first and last name + var userQuery = new QueryExpression("systemuser") + { + ColumnSet = new ColumnSet("systemuserid", "firstname", "lastname", "domainname"), + Criteria = new FilterExpression(LogicalOperator.And) + }; + userQuery.Criteria.AddCondition("firstname", ConditionOperator.Equal, firstName); + userQuery.Criteria.AddCondition("lastname", ConditionOperator.Equal, lastName); + userQuery.Criteria.AddCondition("isdisabled", ConditionOperator.Equal, false); + + var results = service.RetrieveMultiple(userQuery); + + if (results.Entities.Count > 0) + { + return results.Entities[0].Id; + } + + return Guid.Empty; + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + + // Display additional details for common issues + if (ex.InnerException != null) + { + Console.WriteLine("Inner Exception: {0}", ex.InnerException.Message); + } + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryHoursMultipleUsers/QueryHoursMultipleUsers.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/QueryHoursMultipleUsers/QueryHoursMultipleUsers.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryHoursMultipleUsers/QueryHoursMultipleUsers.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryHoursMultipleUsers/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/QueryHoursMultipleUsers/README.md new file mode 100644 index 00000000..39a33026 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryHoursMultipleUsers/README.md @@ -0,0 +1,57 @@ +# Query the working hours of multiple users + +This sample shows how to retrieve the working hours of multiple users by using the [QueryMultipleSchedulesRequest](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.querymultipleschedulesrequest) message. + +This sample requires an additional user that is not present in your system. Create the required user manually **as is** shown below in **Office 365** before you run the sample. + +**First Name**: Kevin +**Last Name**: Cook +**Security Role**: Sales Manager +**UserName**: kcook@yourorg.onmicrosoft.com + +## How to run this sample + +See [How to run samples](https://github.com/microsoft/PowerApps-Samples/blob/master/dataverse/README.md) for information about how to run this sample. + +## What this sample does + +The `QueryMultipleSchedulesRequest` message is intended to be used in a scenario where it contains data that is needed to search multiple resources for available time blocks that match the specified parameters. + +## How this sample works + +In order to simulate the scenario described in [What this sample does](#what-this-sample-does), the sample will do the following: + +### Setup + +1. Retrieves the current user's information using `WhoAmIRequest`. +2. Queries for the manually created user (Kevin Cook) by first and last name. +3. Validates that the required user exists before proceeding. + +### Demonstrate + +1. Creates a `QueryMultipleSchedulesRequest` with: + - Resource IDs for both the current user and Kevin Cook + - Time range from now to 7 days in the future + - Time code set to `Available` to retrieve available time blocks +2. Executes the request to retrieve working hours information. +3. Displays the returned time information, including: + - Number of time info records + - Start and end times for each available block + - Resource (user) information + +### Clean up + +This sample does not create any records that need to be deleted. It only queries existing user working hours information. + +## Key Concepts + +- **QueryMultipleSchedulesRequest**: Used to query available time blocks for multiple users simultaneously +- **TimeCode.Available**: Specifies that we want to retrieve available time slots +- **Working Hours**: The query returns information about when users are scheduled to be available for work +- **Resource Scheduling**: This functionality is useful for scheduling meetings, appointments, or resource allocation + +## See Also + +[Query schedules](https://learn.microsoft.com/dynamics365/customerengagement/on-premises/developer/schedule-collections-appointments) +[QueryMultipleSchedulesRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.querymultipleschedulesrequest) +[Service calendar and scheduling](https://learn.microsoft.com/dynamics365/customer-service/basics-service-service-scheduling) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryWorkingHours/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/QueryWorkingHours/Program.cs new file mode 100644 index 00000000..640cd4af --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryWorkingHours/Program.cs @@ -0,0 +1,166 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates querying working hours schedules for users + /// + /// + /// This sample shows how to use QueryScheduleRequest to retrieve + /// the working hours and availability of a user in Dataverse. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + #region Sample Methods + + private static void Setup(ServiceClient service) + { + // No setup required for this sample + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Querying working hours for current user..."); + Console.WriteLine(); + + // Get the current user's information + var userRequest = new WhoAmIRequest(); + var userResponse = (WhoAmIResponse)service.Execute(userRequest); + + Console.WriteLine($"Current User ID: {userResponse.UserId}"); + Console.WriteLine(); + + // Retrieve the working hours of the current user + var scheduleRequest = new QueryScheduleRequest + { + ResourceId = userResponse.UserId, + Start = DateTime.Now, + End = DateTime.Today.AddDays(7), + TimeCodes = new TimeCode[] { TimeCode.Available } + }; + + var scheduleResponse = (QueryScheduleResponse)service.Execute(scheduleRequest); + + // Display the results + if (scheduleResponse.TimeInfos.Length > 0) + { + Console.WriteLine("Successfully queried the working hours of the current user."); + Console.WriteLine($"Found {scheduleResponse.TimeInfos.Length} time slot(s) with availability."); + Console.WriteLine(); + + // Display first few time slots as examples + int displayCount = Math.Min(5, scheduleResponse.TimeInfos.Length); + Console.WriteLine($"Displaying first {displayCount} available time slot(s):"); + Console.WriteLine(); + + for (int i = 0; i < displayCount; i++) + { + var timeInfo = scheduleResponse.TimeInfos[i]; + Console.WriteLine($"Time Slot {i + 1}:"); + Console.WriteLine($" Start: {timeInfo.Start}"); + Console.WriteLine($" End: {timeInfo.End}"); + Console.WriteLine($" Time Code: {timeInfo.TimeCode}"); + Console.WriteLine($" Sub Code: {timeInfo.SubCode}"); + Console.WriteLine(); + } + + if (scheduleResponse.TimeInfos.Length > displayCount) + { + Console.WriteLine($"... and {scheduleResponse.TimeInfos.Length - displayCount} more time slot(s)"); + Console.WriteLine(); + } + } + else + { + Console.WriteLine("No available time slots found for the current user in the next 7 days."); + Console.WriteLine("This may indicate that the user has no working hours configured."); + Console.WriteLine(); + } + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + // No records created in this sample, nothing to clean up + Console.WriteLine("No records to delete."); + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + if (ex.InnerException != null) + { + Console.WriteLine("Inner Exception: {0}", ex.InnerException.Message); + } + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryWorkingHours/QueryWorkingHours.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/QueryWorkingHours/QueryWorkingHours.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryWorkingHours/QueryWorkingHours.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/QueryWorkingHours/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/QueryWorkingHours/README.md new file mode 100644 index 00000000..b66ddf9d --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/QueryWorkingHours/README.md @@ -0,0 +1,119 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates querying working hours schedules for users in Dataverse" +--- + +# QueryWorkingHours + +Demonstrates querying working hours schedules for users in Dataverse + +## What this sample does + +This sample shows how to: +- Use WhoAmIRequest to get the current user's information +- Use QueryScheduleRequest to retrieve working hours and availability +- Query available time slots for a specific time period +- Process and display TimeInfo results from schedule queries + +QueryScheduleRequest enables applications to retrieve calendar and schedule information, which is useful for appointment scheduling, resource allocation, and availability checking. + +## How this sample works + +### Setup + +No setup is required for this sample. It queries the working hours of the currently authenticated user. + +### Run + +The main demonstration: +1. Executes WhoAmIRequest to get the current user's ID +2. Creates a QueryScheduleRequest with: + - ResourceId set to the current user's ID + - Start time set to now + - End time set to 7 days from now + - TimeCodes set to Available +3. Executes the QueryScheduleRequest +4. Displays available time slots, including: + - Start and end times + - Time codes (Available, Busy, etc.) + - Sub codes for additional detail +5. Shows the first 5 available time slots as examples + +### Cleanup + +No cleanup is required. This sample does not create any records. + +## Demonstrates + +This sample demonstrates: +- **WhoAmIRequest**: Getting the current authenticated user's information +- **QueryScheduleRequest**: Querying calendar and schedule data +- **TimeCode enumeration**: Specifying which types of time slots to retrieve +- **TimeInfo processing**: Working with schedule query results +- **Calendar/Schedule queries**: Retrieving working hours and availability information + +## Sample Output + +``` +Connected to Dataverse. + +Setup complete. + +Querying working hours for current user... + +Current User ID: 12345678-1234-1234-1234-123456789012 + +Successfully queried the working hours of the current user. +Found 35 time slot(s) with availability. + +Displaying first 5 available time slot(s): + +Time Slot 1: + Start: 2/6/2026 9:00:00 AM + End: 2/6/2026 5:00:00 PM + Time Code: Available + Sub Code: Unspecified + +Time Slot 2: + Start: 2/7/2026 9:00:00 AM + End: 2/7/2026 5:00:00 PM + Time Code: Available + Sub Code: Unspecified + +Time Slot 3: + Start: 2/10/2026 9:00:00 AM + End: 2/10/2026 5:00:00 PM + Time Code: Available + Sub Code: Unspecified + +Time Slot 4: + Start: 2/11/2026 9:00:00 AM + End: 2/11/2026 5:00:00 PM + Time Code: Available + Sub Code: Unspecified + +Time Slot 5: + Start: 2/12/2026 9:00:00 AM + End: 2/12/2026 5:00:00 PM + Time Code: Available + Sub Code: Unspecified + +... and 30 more time slot(s) + +Cleaning up... +No records to delete. + +Press any key to exit. +``` + +## See also + +[QueryScheduleRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.queryschedulerequest) +[Schedule and appointment entities](https://learn.microsoft.com/power-apps/developer/data-platform/schedule-appointment-entities) +[Service Scheduling entities](https://learn.microsoft.com/power-apps/developer/data-platform/schedule-collections-appointments-resources-services) +[WhoAmIRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.whoamirequest) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/README.md new file mode 100644 index 00000000..af0314ca --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/README.md @@ -0,0 +1,58 @@ +# Query + +Samples demonstrating various query methods in Dataverse including QueryExpression, FetchXML, and LINQ. + +These samples show how to retrieve data using different query APIs, implement paging, use aggregation functions, work with saved queries, and query specialized data like intersect tables and working hours. + +More information: [Query data using the SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/entity-operations-query-data) + +## Samples + +|Sample folder|Description|Build target| +|---|---|---| +|Convertqueriesfetchqueryexpressions|Convert queries between FetchXML and QueryExpression|.NET 6| +|QueriesUsingLINQ|Query data using LINQ|.NET 6| +|RetrieveMultipleByQueryExpression|Retrieve multiple records using QueryExpression|.NET 6| +|RetrieveMultipleQueryByAttribute|Retrieve multiple records using QueryByAttribute|.NET 6| +|RetrieveRecordsFromIntersectTable|Query many-to-many relationship intersect tables|.NET 6| +|UseAggregationInFetchXML|Use aggregate functions in FetchXML queries|.NET 6| +|UseFetchXMLWithPaging|Implement paging with FetchXML|.NET 6| +|UseQueryExpressionwithPaging|Implement paging with QueryExpression|.NET 6| +|ValidateandExecuteSavedQuery|Work with saved queries (views)|.NET 6| +|QueryByReciprocalRole|Query connection records by reciprocal role|.NET 6| +|QueryByRecord|Query connections for specific records|.NET 6| +|QueryHoursMultipleUsers|Query working hours for multiple users|.NET 6| +|QueryWorkingHours|Query working hours schedules|.NET 6| +|ExportDataUsingFetchXmlToAnnotation|Export query results to annotations|.NET 6| + +## Prerequisites + +- Visual Studio 2022 or later +- .NET 6.0 SDK or later +- Access to a Dataverse environment + +## How to run samples + +1. Clone the PowerApps-Samples repository +2. Navigate to `dataverse/orgsvc/CSharp-NETCore/Query/` +3. Open the desired sample folder +4. Edit the `appsettings.json` file (located in the Query folder) with your environment connection details: + ```json + { + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } + } + ``` +5. Build and run the sample: + ```bash + cd SampleFolder + dotnet run + ``` + +## See also + +[Query data using the SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/entity-operations-query-data) +[Use FetchXML to construct a query](https://learn.microsoft.com/power-apps/developer/data-platform/use-fetchxml-construct-query) +[Build queries with QueryExpression](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/build-queries-with-queryexpression) +[Query data using LINQ](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/query-data-using-linq) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleByQueryExpression/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleByQueryExpression/Program.cs new file mode 100644 index 00000000..1e52c9df --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleByQueryExpression/Program.cs @@ -0,0 +1,188 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates querying data using QueryExpression with linked entities + /// + /// + /// This sample shows how to use QueryExpression with LinkEntity to retrieve + /// data from related entities using entity aliases and aliased values. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating sample data..."); + + // Create a contact + var contact = new Entity("contact") + { + ["firstname"] = "ContactFirstName", + ["lastname"] = "ContactLastName" + }; + Guid contactId = service.Create(contact); + entityStore.Add(new EntityReference("contact", contactId)); + + // Create multiple accounts with the same primary contact + var account1 = new Entity("account") + { + ["name"] = "Test Account1", + ["primarycontactid"] = new EntityReference("contact", contactId) + }; + Guid accountId1 = service.Create(account1); + entityStore.Add(new EntityReference("account", accountId1)); + + var account2 = new Entity("account") + { + ["name"] = "Test Account2", + ["primarycontactid"] = new EntityReference("contact", contactId) + }; + Guid accountId2 = service.Create(account2); + entityStore.Add(new EntityReference("account", accountId2)); + + var account3 = new Entity("account") + { + ["name"] = "Test Account3", + ["primarycontactid"] = new EntityReference("contact", contactId) + }; + Guid accountId3 = service.Create(account3); + entityStore.Add(new EntityReference("account", accountId3)); + + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Entering: RetrieveMultipleWithRelatedEntityColumns"); + Console.WriteLine(); + + // Create a query expression with link entity + var qe = new QueryExpression + { + EntityName = "account", + ColumnSet = new ColumnSet("name") + }; + + // Add link to contact entity + var linkEntity = new LinkEntity( + "account", + "contact", + "primarycontactid", + "contactid", + JoinOperator.Inner + ); + linkEntity.Columns.AddColumns("firstname", "lastname"); + linkEntity.EntityAlias = "primarycontact"; + qe.LinkEntities.Add(linkEntity); + + // Execute query + EntityCollection ec = service.RetrieveMultiple(qe); + + Console.WriteLine("Retrieved {0} entities", ec.Entities.Count); + Console.WriteLine(); + + foreach (Entity act in ec.Entities) + { + Console.WriteLine("Account name: {0}", act["name"]); + Console.WriteLine("Primary contact first name: {0}", + act.GetAttributeValue("primarycontact.firstname").Value); + Console.WriteLine("Primary contact last name: {0}", + act.GetAttributeValue("primarycontact.lastname").Value); + Console.WriteLine(); + } + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleByQueryExpression/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleByQueryExpression/README.md new file mode 100644 index 00000000..8bae1d40 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleByQueryExpression/README.md @@ -0,0 +1,91 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates querying data using QueryExpression with linked entities" +--- + +# RetrieveMultipleByQueryExpression + +Demonstrates querying data using QueryExpression with linked entities + +## What this sample does + +This sample shows how to: +- Use QueryExpression to build complex queries +- Add LinkEntity to join related entities +- Use entity aliases to identify columns from related entities +- Retrieve and access aliased values from linked entities + +QueryExpression provides a powerful object model for building complex queries with joins, filters, and sorting. + +## How this sample works + +### Setup + +The setup process: +1. Creates a contact record (ContactFirstName ContactLastName) +2. Creates three account records, each with the contact as primary contact + +### Run + +The main demonstration: +1. Creates a QueryExpression for the "account" entity +2. Adds a LinkEntity to join with the "contact" entity via primarycontactid +3. Specifies "primarycontact" as the entity alias for the linked entity +4. Retrieves columns from both account and linked contact +5. Executes RetrieveMultiple to get all matching records +6. Displays account names and primary contact information using AliasedValue + +### Cleanup + +The cleanup process deletes all created accounts and contacts. + +## Demonstrates + +This sample demonstrates: +- **QueryExpression**: Building complex queries using the object model +- **LinkEntity**: Joining related entities in queries +- **Entity aliases**: Identifying columns from linked entities +- **AliasedValue**: Accessing columns from linked entities in results +- **JoinOperator**: Using inner joins in queries + +## Sample Output + +``` +Connected to Dataverse. + +Creating sample data... +Setup complete. + +Entering: RetrieveMultipleWithRelatedEntityColumns + +Retrieved 3 entities + +Account name: Test Account1 +Primary contact first name: ContactFirstName +Primary contact last name: ContactLastName + +Account name: Test Account2 +Primary contact first name: ContactFirstName +Primary contact last name: ContactLastName + +Account name: Test Account3 +Primary contact first name: ContactFirstName +Primary contact last name: ContactLastName + +Cleaning up... +Deleting 4 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Build queries with QueryExpression](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/build-queries-with-queryexpression) +[Join tables using QueryExpression](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/join-tables-using-queryexpression) +[Query data using the SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/entity-operations-query-data) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleByQueryExpression/RetrieveMultipleByQueryExpression.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleByQueryExpression/RetrieveMultipleByQueryExpression.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleByQueryExpression/RetrieveMultipleByQueryExpression.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleQueryByAttribute/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleQueryByAttribute/Program.cs new file mode 100644 index 00000000..08936f61 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleQueryByAttribute/Program.cs @@ -0,0 +1,165 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates querying data using QueryByAttribute + /// + /// + /// This sample shows how to use QueryByAttribute to query records based on attribute values. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating sample accounts..."); + + var account1 = new Entity("account") + { + ["name"] = "A. Datum Corporation", + ["address1_stateorprovince"] = "Colorado", + ["address1_telephone1"] = "(206)555-5555", + ["emailaddress1"] = "info@datum.com" + }; + Guid account1Id = service.Create(account1); + entityStore.Add(new EntityReference("account", account1Id)); + + var account2 = new Entity("account") + { + ["name"] = "Adventure Works Cycle", + ["address1_stateorprovince"] = "Washington", + ["address1_city"] = "Redmond", + ["address1_telephone1"] = "(206)555-5555", + ["emailaddress1"] = "contactus@adventureworkscycle.com" + }; + Guid account2Id = service.Create(account2); + entityStore.Add(new EntityReference("account", account2Id)); + + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Query Using QueryByAttribute"); + Console.WriteLine("==============================="); + + // Create query using QueryByAttribute + var querybyattribute = new QueryByAttribute("account"); + querybyattribute.ColumnSet = new ColumnSet("name", "address1_city", "emailaddress1"); + + // Attribute to query + querybyattribute.Attributes.AddRange("address1_city"); + + // Value of queried attribute to return + querybyattribute.Values.AddRange("Redmond"); + + // Execute query + EntityCollection retrieved = service.RetrieveMultiple(querybyattribute); + + // Iterate through returned collection + foreach (var account in retrieved.Entities) + { + Console.WriteLine("Name: " + account["name"]); + + if (account.Contains("address1_city")) + Console.WriteLine("Address: " + account["address1_city"]); + + if (account.Contains("emailaddress1")) + Console.WriteLine("E-mail: " + account["emailaddress1"]); + } + Console.WriteLine("==============================="); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleQueryByAttribute/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleQueryByAttribute/README.md new file mode 100644 index 00000000..a85033f7 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleQueryByAttribute/README.md @@ -0,0 +1,79 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates querying data using QueryByAttribute" +--- + +# RetrieveMultipleQueryByAttribute + +Demonstrates querying data using QueryByAttribute + +## What this sample does + +This sample shows how to: +- Use QueryByAttribute to query records based on attribute values +- Specify columns to return using ColumnSet +- Filter records by attribute name and value pairs +- Iterate through query results + +QueryByAttribute provides a simple way to query records when you need to filter by specific attribute values without building complex QueryExpression criteria. + +## How this sample works + +### Setup + +The setup process: +1. Creates account "A. Datum Corporation" with address in Colorado +2. Creates account "Adventure Works Cycle" with address in Redmond, Washington + +### Run + +The main demonstration: +1. Creates a QueryByAttribute for the "account" entity +2. Specifies columns to return: name, address1_city, emailaddress1 +3. Adds filter criteria: address1_city = "Redmond" +4. Executes RetrieveMultiple to get matching records +5. Displays account information for all matching records + +### Cleanup + +The cleanup process deletes all created accounts. + +## Demonstrates + +This sample demonstrates: +- **QueryByAttribute**: Simple attribute-based query construction +- **ColumnSet**: Specifying which columns to retrieve +- **RetrieveMultiple**: Executing queries to retrieve multiple records +- **EntityCollection**: Working with query result collections + +## Sample Output + +``` +Connected to Dataverse. + +Creating sample accounts... +Setup complete. + +Query Using QueryByAttribute +=============================== +Name: Adventure Works Cycle +Address: Redmond +E-mail: contactus@adventureworkscycle.com +=============================== +Cleaning up... +Deleting 2 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Query data using QueryByAttribute](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/use-querybyattribute-class) +[Build queries with QueryExpression](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/build-queries-with-queryexpression) +[Query data using the SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/entity-operations-query-data) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleQueryByAttribute/RetrieveMultipleQueryByAttribute.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleQueryByAttribute/RetrieveMultipleQueryByAttribute.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveMultipleQueryByAttribute/RetrieveMultipleQueryByAttribute.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveRecordsFromIntersectTable/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveRecordsFromIntersectTable/Program.cs new file mode 100644 index 00000000..4536b4d6 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveRecordsFromIntersectTable/Program.cs @@ -0,0 +1,347 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Query; +using System.Text; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates retrieving records from an intersect table (many-to-many relationship) + /// + /// + /// This sample shows three different approaches to querying intersect tables: + /// 1. QueryExpression with LinkEntity + /// 2. FetchXML with link-entity and intersect="true" + /// 3. Direct query of the intersect table + /// + /// The sample creates a custom role and associates it with the current user, + /// then demonstrates how to retrieve the association records from the + /// systemuserroles intersect table. + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + private static Guid userId; + private static Guid roleId; + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating sample data..."); + + // Retrieve the default business unit needed to create the role + var queryDefaultBusinessUnit = new QueryExpression + { + EntityName = "businessunit", + ColumnSet = new ColumnSet("businessunitid"), + Criteria = new FilterExpression() + }; + + // Find the root business unit (parent is null) + queryDefaultBusinessUnit.Criteria.AddCondition( + "parentbusinessunitid", + ConditionOperator.Null); + + EntityCollection businessUnits = service.RetrieveMultiple(queryDefaultBusinessUnit); + + if (businessUnits.Entities.Count == 0) + { + throw new Exception("No default business unit found."); + } + + Entity defaultBusinessUnit = businessUnits.Entities[0]; + Guid businessUnitId = defaultBusinessUnit.Id; + + // Get the GUID of the current user + var whoRequest = new WhoAmIRequest(); + var whoResponse = (WhoAmIResponse)service.Execute(whoRequest); + userId = whoResponse.UserId; + Console.WriteLine($"Current User ID: {userId}"); + + // Create a custom role + var role = new Entity("role") + { + ["name"] = "ABC Management Role", + ["businessunitid"] = new EntityReference("businessunit", businessUnitId) + }; + + roleId = service.Create(role); + entityStore.Add(new EntityReference("role", roleId)); + Console.WriteLine($"Created Role: {roleId}"); + + // Associate the user with the role using the systemuserroles_association relationship + var associateRequest = new AssociateRequest + { + Target = new EntityReference("systemuser", userId), + RelatedEntities = new EntityReferenceCollection + { + new EntityReference("role", roleId) + }, + Relationship = new Relationship("systemuserroles_association") + }; + + service.Execute(associateRequest); + Console.WriteLine($"Associated User {userId} with Role {roleId}"); + Console.WriteLine(); + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("=== Retrieving Records from Intersect Table ==="); + Console.WriteLine(); + + // Approach 1: QueryExpression with LinkEntity + Console.WriteLine("Approach 1: QueryExpression with LinkEntity"); + Console.WriteLine("--------------------------------------------"); + RetrieveWithQueryExpression(service); + Console.WriteLine(); + + // Approach 2: FetchXML + Console.WriteLine("Approach 2: FetchXML with intersect link-entity"); + Console.WriteLine("------------------------------------------------"); + RetrieveWithFetchXML(service); + Console.WriteLine(); + + // Approach 3: Direct query of intersect table + Console.WriteLine("Approach 3: Direct query of intersect table"); + Console.WriteLine("--------------------------------------------"); + RetrieveIntersectTableDirectly(service); + Console.WriteLine(); + } + + private static void RetrieveWithQueryExpression(ServiceClient service) + { + // Create QueryExpression that links from role to systemuserroles intersect table + var query = new QueryExpression + { + EntityName = "role", + ColumnSet = new ColumnSet("name") + }; + + // Add link to the systemuserroles intersect table + var linkEntity = new LinkEntity + { + LinkFromEntityName = "role", + LinkFromAttributeName = "roleid", + LinkToEntityName = "systemuserroles", + LinkToAttributeName = "roleid", + LinkCriteria = new FilterExpression + { + FilterOperator = LogicalOperator.And, + Conditions = + { + new ConditionExpression + { + AttributeName = "systemuserid", + Operator = ConditionOperator.Equal, + Values = { userId } + } + } + } + }; + query.LinkEntities.Add(linkEntity); + + // Execute the query + EntityCollection results = service.RetrieveMultiple(query); + + // Display results + Console.WriteLine($"QueryExpression retrieved {results.Entities.Count} role(s):"); + foreach (Entity role in results.Entities) + { + Console.WriteLine($" - Role Name: {role.GetAttributeValue("name")}"); + } + } + + private static void RetrieveWithFetchXML(ServiceClient service) + { + // Build FetchXML query with intersect link + var fetchXml = new StringBuilder(); + fetchXml.Append(""); + fetchXml.Append(" "); + fetchXml.Append(" "); + fetchXml.Append(" "); + fetchXml.Append(" "); + fetchXml.Append($" "); + fetchXml.Append(" "); + fetchXml.Append(" "); + fetchXml.Append(" "); + fetchXml.Append(""); + + // Execute the FetchXML query + var fetchRequest = new RetrieveMultipleRequest + { + Query = new FetchExpression(fetchXml.ToString()) + }; + var fetchResponse = (RetrieveMultipleResponse)service.Execute(fetchRequest); + EntityCollection results = fetchResponse.EntityCollection; + + // Display results + Console.WriteLine($"FetchXML retrieved {results.Entities.Count} role(s):"); + foreach (Entity role in results.Entities) + { + Console.WriteLine($" - Role Name: {role.GetAttributeValue("name")}"); + } + } + + private static void RetrieveIntersectTableDirectly(ServiceClient service) + { + // Query the systemuserroles intersect table directly + var query = new QueryExpression + { + EntityName = "systemuserroles", + ColumnSet = new ColumnSet("systemuserid", "roleid"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression + { + AttributeName = "systemuserid", + Operator = ConditionOperator.Equal, + Values = { userId } + }, + new ConditionExpression + { + AttributeName = "roleid", + Operator = ConditionOperator.Equal, + Values = { roleId } + } + } + } + }; + + EntityCollection results = service.RetrieveMultiple(query); + + // Display results + Console.WriteLine($"Direct query retrieved {results.Entities.Count} association(s):"); + foreach (Entity association in results.Entities) + { + Guid systemUserId = association.GetAttributeValue("systemuserid"); + Guid associatedRoleId = association.GetAttributeValue("roleid"); + Console.WriteLine($" - User ID: {systemUserId}, Role ID: {associatedRoleId}"); + } + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + // Disassociate the user from the role before deleting + try + { + var disassociateRequest = new DisassociateRequest + { + Target = new EntityReference("systemuser", userId), + RelatedEntities = new EntityReferenceCollection + { + new EntityReference("role", roleId) + }, + Relationship = new Relationship("systemuserroles_association") + }; + service.Execute(disassociateRequest); + Console.WriteLine("Disassociated user from role."); + } + catch (Exception ex) + { + Console.WriteLine($"Error during disassociation: {ex.Message}"); + } + + // Delete created records + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + for (int i = entityStore.Count - 1; i >= 0; i--) + { + try + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + catch (Exception ex) + { + Console.WriteLine($"Error deleting {entityStore[i].LogicalName} {entityStore[i].Id}: {ex.Message}"); + } + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + Console.WriteLine(); + Console.WriteLine("Stack Trace:"); + Console.WriteLine(ex.StackTrace); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveRecordsFromIntersectTable/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveRecordsFromIntersectTable/README.md new file mode 100644 index 00000000..0086308b --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveRecordsFromIntersectTable/README.md @@ -0,0 +1,141 @@ +# Retrieve records from an intersect table + +This sample demonstrates how to retrieve records from an intersect table in Microsoft Dataverse. Intersect tables are used to represent many-to-many relationships between entities. + +## How to run this sample + +1. Clone or download the [PowerApps-Samples](https://github.com/microsoft/PowerApps-Samples) repository. + +2. Navigate to the sample directory: + ``` + cd dataverse/orgsvc/CSharp-NETCore/Query/RetrieveRecordsFromIntersectTable + ``` + +3. Update the connection string in `../appsettings.json` with your Dataverse environment details: + ```json + { + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } + } + ``` + +4. Build and run the sample: + ``` + dotnet run + ``` + +## What this sample does + +This sample demonstrates three different approaches for retrieving records from an intersect table: + +1. **QueryExpression with LinkEntity** - Uses a QueryExpression to link from the primary entity (role) through the intersect table (systemuserroles) with filter criteria. + +2. **FetchXML with intersect link-entity** - Uses FetchXML with the `intersect="true"` attribute to query through the many-to-many relationship. + +3. **Direct query of intersect table** - Directly queries the systemuserroles intersect table to retrieve association records. + +## How this sample works + +### Setup + +1. Retrieves the default business unit needed to create the role. +2. Gets the GUID of the current user using `WhoAmIRequest`. +3. Creates a custom role named "ABC Management Role". +4. Associates the current user with the role using the `systemuserroles_association` relationship. + +### Demonstrate + +The sample demonstrates three different query approaches: + +#### 1. QueryExpression with LinkEntity +Creates a QueryExpression that links from the role entity to the systemuserroles intersect table, filtering by the current user's ID. This approach retrieves role records that are associated with the user. + +```csharp +var query = new QueryExpression +{ + EntityName = "role", + ColumnSet = new ColumnSet("name") +}; + +var linkEntity = new LinkEntity +{ + LinkFromEntityName = "role", + LinkFromAttributeName = "roleid", + LinkToEntityName = "systemuserroles", + LinkToAttributeName = "roleid", + LinkCriteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("systemuserid", ConditionOperator.Equal, userId) + } + } +}; +query.LinkEntities.Add(linkEntity); +``` + +#### 2. FetchXML with intersect attribute +Uses FetchXML with `intersect="true"` to query through the many-to-many relationship. This is a more declarative approach. + +```xml + + + + + + + + + + +``` + +#### 3. Direct query of intersect table +Directly queries the systemuserroles intersect table to retrieve the association records themselves, showing the raw relationship data. + +```csharp +var query = new QueryExpression +{ + EntityName = "systemuserroles", + ColumnSet = new ColumnSet("systemuserid", "roleid"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("systemuserid", ConditionOperator.Equal, userId), + new ConditionExpression("roleid", ConditionOperator.Equal, roleId) + } + } +}; +``` + +### Clean up + +1. Disassociates the user from the role using `DisassociateRequest`. +2. Deletes the custom role that was created. + +## Key concepts + +### Intersect Tables +- Intersect tables represent many-to-many relationships in Dataverse +- They contain only the IDs of the related entities (e.g., systemuserid and roleid) +- The table name is typically a combination of both entity names (e.g., systemuserroles) + +### Querying Intersect Tables +- **LinkEntity approach**: Best when you need data from the related entities +- **FetchXML with intersect**: More declarative, easier to construct dynamically +- **Direct query**: Useful when you need the raw association data or metadata + +### Association/Disassociation +- Use `AssociateRequest` to create relationships +- Use `DisassociateRequest` to remove relationships +- The `Relationship` object specifies the schema name (e.g., "systemuserroles_association") + +## Related documentation + +- [QueryExpression class](https://learn.microsoft.com/dotnet/api/microsoft.xrm.sdk.query.queryexpression) +- [FetchXML reference](https://learn.microsoft.com/power-apps/developer/data-platform/fetchxml/overview) +- [Many-to-many relationships](https://learn.microsoft.com/power-apps/developer/data-platform/create-retrieve-entity-relationships#many-to-many-relationships) +- [AssociateRequest class](https://learn.microsoft.com/dotnet/api/microsoft.xrm.sdk.messages.associaterequest) +- [DisassociateRequest class](https://learn.microsoft.com/dotnet/api/microsoft.xrm.sdk.messages.disassociaterequest) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveRecordsFromIntersectTable/RetrieveRecordsFromIntersectTable.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveRecordsFromIntersectTable/RetrieveRecordsFromIntersectTable.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/RetrieveRecordsFromIntersectTable/RetrieveRecordsFromIntersectTable.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/UseAggregationInFetchXML/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/UseAggregationInFetchXML/Program.cs new file mode 100644 index 00000000..b17e4f42 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/UseAggregationInFetchXML/Program.cs @@ -0,0 +1,651 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates using aggregation in FetchXML queries + /// + /// + /// This sample shows how to use aggregate functions (sum, avg, count, min, max) + /// in FetchXML queries, including grouping and date grouping features. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating sample account and opportunities..."); + + var OpportunityStatusCodes = new + { + Won = 3 + }; + + // Create sample account + var setupAccount = new Entity("account") + { + ["name"] = "Example Account" + }; + + Guid accountId = service.Create(setupAccount); + entityStore.Add(new EntityReference("account", accountId)); + + Console.WriteLine("Created {0}.", setupAccount["name"]); + + EntityReference setupCustomer = new EntityReference("account", accountId); + + // Create 3 sample opportunities with different estimated values + var setupOpportunities = new[] + { + new Entity("opportunity") + { + ["name"] = "Sample Opp 1", + ["estimatedvalue"] = new Money(120000.00m), + ["customerid"] = setupCustomer + }, + new Entity("opportunity") + { + ["name"] = "Sample Opp With Duplicate Name", + ["estimatedvalue"] = new Money(240000.00m), + ["customerid"] = setupCustomer + }, + new Entity("opportunity") + { + ["name"] = "Sample Opp With Duplicate Name", + ["estimatedvalue"] = new Money(360000.00m), + ["customerid"] = setupCustomer + } + }; + + // Create opportunities and track their IDs + foreach (var opp in setupOpportunities) + { + Guid oppId = service.Create(opp); + entityStore.Add(new EntityReference("opportunity", oppId)); + + // Win the opportunity + var winRequest = new WinOpportunityRequest + { + OpportunityClose = new Entity("opportunityclose") + { + ["opportunityid"] = new EntityReference("opportunity", oppId), + // Mark these entities as won in 2009 to have testable results + ["actualend"] = new DateTime(2009, 11, 1, 12, 0, 0) + }, + Status = new OptionSetValue(OpportunityStatusCodes.Won) + }; + service.Execute(winRequest); + } + + Console.WriteLine("Created {0} sample opportunity records and updated as won for this sample.", setupOpportunities.Length); + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + // ***************************************************************************************************************** + // FetchXML estimatedvalue_avg Aggregate 1 + // ***************************************************************************************************************** + // Fetch the average of estimatedvalue for all opportunities. This is the equivalent of + // SELECT AVG(estimatedvalue) AS estimatedvalue_avg ... in SQL. + Console.WriteLine("==============================="); + string estimatedvalue_avg = @" + + + + + "; + + EntityCollection estimatedvalue_avg_result = service.RetrieveMultiple(new FetchExpression(estimatedvalue_avg)); + + foreach (var c in estimatedvalue_avg_result.Entities) + { + decimal aggregate1 = ((Money)((AliasedValue)c["estimatedvalue_avg"]).Value).Value; + Console.WriteLine("Average estimated value: " + aggregate1); + } + Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML opportunity_count Aggregate 2 + // ***************************************************************************************************************** + // Fetch the count of all opportunities. This is the equivalent of + // SELECT COUNT(*) AS opportunity_count ... in SQL. + string opportunity_count = @" + + + + + "; + + EntityCollection opportunity_count_result = service.RetrieveMultiple(new FetchExpression(opportunity_count)); + + foreach (var c in opportunity_count_result.Entities) + { + Int32 aggregate2 = (Int32)((AliasedValue)c["opportunity_count"]).Value; + Console.WriteLine("Count of all opportunities: " + aggregate2); + } + Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML opportunity_colcount Aggregate 3 + // ***************************************************************************************************************** + // Fetch the count of all opportunities. This is the equivalent of + // SELECT COUNT(name) AS opportunity_count ... in SQL. + string opportunity_colcount = @" + + + + + "; + + EntityCollection opportunity_colcount_result = service.RetrieveMultiple(new FetchExpression(opportunity_colcount)); + + foreach (var c in opportunity_colcount_result.Entities) + { + Int32 aggregate3 = (Int32)((AliasedValue)c["opportunity_colcount"]).Value; + Console.WriteLine("Column count of all opportunities: " + aggregate3); + } + Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML opportunity_distcount Aggregate 4 + // ***************************************************************************************************************** + // Fetch the count of distinct names for opportunities. This is the equivalent of + // SELECT COUNT(DISTINCT name) AS opportunity_count ... in SQL. + string opportunity_distcount = @" + + + + + "; + + EntityCollection opportunity_distcount_result = service.RetrieveMultiple(new FetchExpression(opportunity_distcount)); + + foreach (var c in opportunity_distcount_result.Entities) + { + Int32 aggregate4 = (Int32)((AliasedValue)c["opportunity_distcount"]).Value; + Console.WriteLine("Distinct name count of all opportunities: " + aggregate4); + } + Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML estimatedvalue_max Aggregate 5 + // ***************************************************************************************************************** + // Fetch the maximum estimatedvalue of all opportunities. This is the equivalent of + // SELECT MAX(estimatedvalue) AS estimatedvalue_max ... in SQL. + string estimatedvalue_max = @" + + + + + "; + + EntityCollection estimatedvalue_max_result = service.RetrieveMultiple(new FetchExpression(estimatedvalue_max)); + + foreach (var c in estimatedvalue_max_result.Entities) + { + decimal aggregate5 = ((Money)((AliasedValue)c["estimatedvalue_max"]).Value).Value; + Console.WriteLine("Max estimated value of all opportunities: " + aggregate5); + } + Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML estimatedvalue_min Aggregate 6 + // ***************************************************************************************************************** + // Fetch the minimum estimatedvalue of all opportunities. This is the equivalent of + // SELECT MIN(estimatedvalue) AS estimatedvalue_min ... in SQL. + string estimatedvalue_min = @" + + + + + "; + + EntityCollection estimatedvalue_min_result = service.RetrieveMultiple(new FetchExpression(estimatedvalue_min)); + + foreach (var c in estimatedvalue_min_result.Entities) + { + decimal aggregate6 = ((Money)((AliasedValue)c["estimatedvalue_min"]).Value).Value; + Console.WriteLine("Minimum estimated value of all opportunities: " + aggregate6); + } + Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML estimatedvalue_sum Aggregate 7 + // ***************************************************************************************************************** + // Fetch the sum of estimatedvalue for all opportunities. This is the equivalent of + // SELECT SUM(estimatedvalue) AS estimatedvalue_sum ... in SQL. + string estimatedvalue_sum = @" + + + + + "; + + EntityCollection estimatedvalue_sum_result = service.RetrieveMultiple(new FetchExpression(estimatedvalue_sum)); + + foreach (var c in estimatedvalue_sum_result.Entities) + { + decimal aggregate7 = ((Money)((AliasedValue)c["estimatedvalue_sum"]).Value).Value; + Console.WriteLine("Sum of estimated value of all opportunities: " + aggregate7); + } + Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML estimatedvalue_avg, estimatedvalue_sum Aggregate 8 + // ***************************************************************************************************************** + // Fetch multiple aggregate values within a single query. + string estimatedvalue_avg2 = @" + + + + + + + "; + + EntityCollection estimatedvalue_avg2_result = service.RetrieveMultiple(new FetchExpression(estimatedvalue_avg2)); + + foreach (var c in estimatedvalue_avg2_result.Entities) + { + Int32 aggregate8a = (Int32)((AliasedValue)c["opportunity_count"]).Value; + Console.WriteLine("Count of all opportunities: " + aggregate8a); + decimal aggregate8b = ((Money)((AliasedValue)c["estimatedvalue_sum"]).Value).Value; + Console.WriteLine("Sum of estimated value of all opportunities: " + aggregate8b); + decimal aggregate8c = ((Money)((AliasedValue)c["estimatedvalue_avg"]).Value).Value; + Console.WriteLine("Average of estimated value of all opportunities: " + aggregate8c); + } + System.Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML groupby1 Aggregate 9 + // ***************************************************************************************************************** + // Fetch a list of users with a count of all the opportunities they own using groupby. + string groupby1 = @" + + + + + + "; + + EntityCollection groupby1_result = service.RetrieveMultiple(new FetchExpression(groupby1)); + + foreach (var c in groupby1_result.Entities) + { + Int32 aggregate9a = (Int32)((AliasedValue)c["opportunity_count"]).Value; + Console.WriteLine("Count of all opportunities: " + aggregate9a + "\n"); + string aggregate9b = ((EntityReference)((AliasedValue)c["ownerid"]).Value).Name; + Console.WriteLine("Owner: " + aggregate9b); + string aggregate9c = (string)((AliasedValue)c["ownerid_owneridyominame"]).Value; + Console.WriteLine("Owner: " + aggregate9c); + string aggregate9d = (string)((AliasedValue)c["ownerid_owneridyominame"]).Value; + Console.WriteLine("Owner: " + aggregate9d); + } + System.Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML groupby2 Aggregate 10 + // ***************************************************************************************************************** + // Fetch the number of opportunities each manager's direct reports + // own using a groupby within a link-entity. + string groupby2 = @" + + + + + + + + "; + + EntityCollection groupby2_result = service.RetrieveMultiple(new FetchExpression(groupby2)); + + foreach (var c in groupby2_result.Entities) + { + int? aggregate10a = (int?)((AliasedValue)c["opportunity_count"]).Value; + Console.WriteLine("Count of all opportunities: " + aggregate10a + "\n"); + } + Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML byyear Aggregate 11 + // ***************************************************************************************************************** + // Fetch aggregate information about the opportunities that have + // been won by year. + string byyear = @" + + + + + + + + + + + "; + + EntityCollection byyear_result = service.RetrieveMultiple(new FetchExpression(byyear)); + + foreach (var c in byyear_result.Entities) + { + Int32 aggregate11 = (Int32)((AliasedValue)c["year"]).Value; + Console.WriteLine("Year: " + aggregate11); + Int32 aggregate11a = (Int32)((AliasedValue)c["opportunity_count"]).Value; + Console.WriteLine("Count of all opportunities: " + aggregate11a); + decimal aggregate11b = ((Money)((AliasedValue)c["estimatedvalue_sum"]).Value).Value; + Console.WriteLine("Sum of estimated value of all opportunities: " + aggregate11b); + decimal aggregate11c = ((Money)((AliasedValue)c["estimatedvalue_avg"]).Value).Value; + Console.WriteLine("Average of estimated value of all opportunities: " + aggregate11c); + Console.WriteLine("----------------------------------------------"); + } + System.Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML byquarter Aggregate 12 + // ***************************************************************************************************************** + // Fetch aggregate information about the opportunities that have + // been won by quarter.(returns 1-4) + string byquarter = @" + + + + + + + + + + + "; + + EntityCollection byquarter_result = service.RetrieveMultiple(new FetchExpression(byquarter)); + + foreach (var c in byquarter_result.Entities) + { + Int32 aggregate12 = (Int32)((AliasedValue)c["quarter"]).Value; + Console.WriteLine("Quarter: " + aggregate12); + Int32 aggregate12a = (Int32)((AliasedValue)c["opportunity_count"]).Value; + Console.WriteLine("Count of all opportunities: " + aggregate12a); + decimal aggregate12b = ((Money)((AliasedValue)c["estimatedvalue_sum"]).Value).Value; + Console.WriteLine("Sum of estimated value of all opportunities: " + aggregate12b); + decimal aggregate12c = ((Money)((AliasedValue)c["estimatedvalue_avg"]).Value).Value; + Console.WriteLine("Average of estimated value of all opportunities: " + aggregate12c); + Console.WriteLine("----------------------------------------------"); + } + Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML bymonth Aggregate 13 + // ***************************************************************************************************************** + // Fetch aggregate information about the opportunities that have + // been won by month. (returns 1-12) + string bymonth = @" + + + + + + + + + + + "; + + EntityCollection bymonth_result = service.RetrieveMultiple(new FetchExpression(bymonth)); + + foreach (var c in bymonth_result.Entities) + { + Int32 aggregate13 = (Int32)((AliasedValue)c["month"]).Value; + Console.WriteLine("Month: " + aggregate13); + Int32 aggregate13a = (Int32)((AliasedValue)c["opportunity_count"]).Value; + Console.WriteLine("Count of all opportunities: " + aggregate13a); + decimal aggregate13b = ((Money)((AliasedValue)c["estimatedvalue_sum"]).Value).Value; + Console.WriteLine("Sum of estimated value of all opportunities: " + aggregate13b); + decimal aggregate13c = ((Money)((AliasedValue)c["estimatedvalue_avg"]).Value).Value; + Console.WriteLine("Average of estimated value of all opportunities: " + aggregate13c); + Console.WriteLine("----------------------------------------------"); + } + System.Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML byweek Aggregate 14 + // ***************************************************************************************************************** + // Fetch aggregate information about the opportunities that have + // been won by week. (Returns 1-52) + string byweek = @" + + + + + + + + + + + "; + + EntityCollection byweek_result = service.RetrieveMultiple(new FetchExpression(byweek)); + + foreach (var c in byweek_result.Entities) + { + Int32 aggregate14 = (Int32)((AliasedValue)c["week"]).Value; + Console.WriteLine("Week: " + aggregate14); + Int32 aggregate14a = (Int32)((AliasedValue)c["opportunity_count"]).Value; + Console.WriteLine("Count of all opportunities: " + aggregate14a); + decimal aggregate14b = ((Money)((AliasedValue)c["estimatedvalue_sum"]).Value).Value; + Console.WriteLine("Sum of estimated value of all opportunities: " + aggregate14b); + decimal aggregate14c = ((Money)((AliasedValue)c["estimatedvalue_avg"]).Value).Value; + Console.WriteLine("Average of estimated value of all opportunities: " + aggregate14c); + Console.WriteLine("----------------------------------------------"); + } + System.Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML byday Aggregate 15 + // ***************************************************************************************************************** + // Fetch aggregate information about the opportunities that have + // been won by day. (Returns 1-31) + string byday = @" + + + + + + + + + + + "; + + EntityCollection byday_result = service.RetrieveMultiple(new FetchExpression(byday)); + + foreach (var c in byday_result.Entities) + { + Int32 aggregate15 = (Int32)((AliasedValue)c["day"]).Value; + Console.WriteLine("Day: " + aggregate15); + Int32 aggregate15a = (Int32)((AliasedValue)c["opportunity_count"]).Value; + Console.WriteLine("Count of all opportunities: " + aggregate15a); + decimal aggregate15b = ((Money)((AliasedValue)c["estimatedvalue_sum"]).Value).Value; + Console.WriteLine("Sum of estimated value of all opportunities: " + aggregate15b); + decimal aggregate15c = ((Money)((AliasedValue)c["estimatedvalue_avg"]).Value).Value; + Console.WriteLine("Average of estimated value of all opportunities: " + aggregate15c); + Console.WriteLine("----------------------------------------------"); + } + System.Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML byyrqtr Aggregate 16 + // ***************************************************************************************************************** + // Fetch aggregate information about the opportunities that have + // been won by year and quarter. + string byyrqtr = @" + + + + + + + + + + + + "; + + EntityCollection byyrqtr_result = service.RetrieveMultiple(new FetchExpression(byyrqtr)); + + foreach (var c in byyrqtr_result.Entities) + { + Int32 aggregate16d = (Int32)((AliasedValue)c["year"]).Value; + Console.WriteLine("Year: " + aggregate16d); + Int32 aggregate16 = (Int32)((AliasedValue)c["quarter"]).Value; + Console.WriteLine("Quarter: " + aggregate16); + Int32 aggregate16a = (Int32)((AliasedValue)c["opportunity_count"]).Value; + Console.WriteLine("Count of all opportunities: " + aggregate16a); + decimal aggregate16b = ((Money)((AliasedValue)c["estimatedvalue_sum"]).Value).Value; + Console.WriteLine("Sum of estimated value of all opportunities: " + aggregate16b); + decimal aggregate16c = ((Money)((AliasedValue)c["estimatedvalue_avg"]).Value).Value; + Console.WriteLine("Average of estimated value of all opportunities: " + aggregate16c); + Console.WriteLine("----------------------------------------------"); + } + Console.WriteLine("==============================="); + + // ***************************************************************************************************************** + // FetchXML byyrqtr2 Aggregate 17 + // ***************************************************************************************************************** + // Specify the result order for the previous sample. Order by year, then quarter. + string byyrqtr2 = @" + + + + + + + + + + + + + + "; + + EntityCollection byyrqtr2_result = service.RetrieveMultiple(new FetchExpression(byyrqtr2)); + + foreach (var c in byyrqtr2_result.Entities) + { + Int32 aggregate17 = (Int32)((AliasedValue)c["quarter"]).Value; + Console.WriteLine("Quarter: " + aggregate17); + Int32 aggregate17d = (Int32)((AliasedValue)c["year"]).Value; + Console.WriteLine("Year: " + aggregate17d); + Int32 aggregate17a = (Int32)((AliasedValue)c["opportunity_count"]).Value; + Console.WriteLine("Count of all opportunities: " + aggregate17a); + decimal aggregate17b = ((Money)((AliasedValue)c["estimatedvalue_sum"]).Value).Value; + Console.WriteLine("Sum of estimated value of all opportunities: " + aggregate17b); + decimal aggregate17c = ((Money)((AliasedValue)c["estimatedvalue_avg"]).Value).Value; + Console.WriteLine("Average of estimated value of all opportunities: " + aggregate17c); + Console.WriteLine("----------------------------------------------"); + } + Console.WriteLine("==============================="); + + Console.WriteLine("Retrieved aggregate record data."); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Entity records have been deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/UseAggregationInFetchXML/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/UseAggregationInFetchXML/README.md new file mode 100644 index 00000000..8170a4c6 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/UseAggregationInFetchXML/README.md @@ -0,0 +1,104 @@ +# Use Aggregation in FetchXML + +This sample demonstrates how to use aggregate functions in FetchXML queries to perform calculations and grouping operations on Dataverse data. + +## What This Sample Does + +This sample creates sample opportunity records and then demonstrates 17 different aggregate query patterns using FetchXML, including: + +- **Basic Aggregate Functions**: AVG, COUNT, MIN, MAX, SUM +- **Count Variations**: COUNT(*), COUNT(column), COUNT(DISTINCT column) +- **Multiple Aggregates**: Combining multiple aggregate functions in a single query +- **Grouping**: GROUP BY with ownerid and linked entities +- **Date Grouping**: Grouping by year, quarter, month, week, and day +- **Ordering**: Controlling the sort order of aggregate results + +## How This Sample Works + +### Setup + +The sample creates: +1. One account record +2. Three opportunity records with different estimated values ($120,000, $240,000, $360,000) +3. Marks all opportunities as "Won" with a close date of November 1, 2009 + +### Demonstrate + +The sample executes 17 different FetchXML queries demonstrating various aggregation scenarios: + +1. **AVG** - Average estimated value of all opportunities +2. **COUNT(*)** - Total count of all opportunities +3. **COUNT(column)** - Count of opportunities with non-null name values +4. **COUNT(DISTINCT column)** - Count of distinct opportunity names +5. **MAX** - Maximum estimated value +6. **MIN** - Minimum estimated value +7. **SUM** - Sum of all estimated values +8. **Multiple Aggregates** - Count, sum, and average in one query +9. **GROUP BY ownerid** - Opportunities grouped by owner +10. **GROUP BY with link-entity** - Opportunities grouped by manager +11. **Date Grouping by Year** - Won opportunities grouped by year +12. **Date Grouping by Quarter** - Won opportunities grouped by quarter (1-4) +13. **Date Grouping by Month** - Won opportunities grouped by month (1-12) +14. **Date Grouping by Week** - Won opportunities grouped by week (1-52) +15. **Date Grouping by Day** - Won opportunities grouped by day (1-31) +16. **Multiple Date Groupings** - Grouped by both year and quarter +17. **Ordered Results** - Same as #16 but with explicit ordering + +### Cleanup + +The sample deletes all created records (opportunities and account). + +## Key Concepts + +### Aggregate Attributes + +FetchXML supports these aggregate functions via the `aggregate` attribute: +- `count` - Count all records +- `countcolumn` - Count non-null values in a column +- `sum` - Sum numeric values +- `avg` - Average of numeric values +- `min` - Minimum value +- `max` - Maximum value + +### Aliased Values + +Aggregate results are returned as `AliasedValue` objects. To access the value: + +```csharp +var result = (int)((AliasedValue)entity["alias_name"]).Value; +``` + +### Date Grouping + +FetchXML supports date grouping with the `dategrouping` attribute: +- `year` - Group by year +- `quarter` - Group by quarter (1-4) +- `month` - Group by month (1-12) +- `week` - Group by week (1-52) +- `day` - Group by day of month (1-31) + +### Distinct Counts + +Use `distinct='true'` with `countcolumn` to count unique values: + +```xml + +``` + +## Running the Sample + +1. Update the connection string in `appsettings.json` at the Query folder level +2. Build the project: `dotnet build` +3. Run the sample: `dotnet run` + +The sample will: +- Create test data (1 account, 3 opportunities) +- Execute all 17 aggregate queries and display results +- Prompt to delete created records +- Clean up all created data + +## More Information + +- [Use FetchXML aggregation](https://docs.microsoft.com/power-apps/developer/data-platform/use-fetchxml-aggregation) +- [FetchXML reference](https://docs.microsoft.com/power-apps/developer/data-platform/fetchxml-reference) +- [Build queries with FetchXML](https://docs.microsoft.com/power-apps/developer/data-platform/org-service/build-queries-fetchxml) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/UseAggregationInFetchXML/UseAggregationInFetchXML.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/UseAggregationInFetchXML/UseAggregationInFetchXML.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/UseAggregationInFetchXML/UseAggregationInFetchXML.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/UseFetchXMLWithPaging/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/UseFetchXMLWithPaging/Program.cs new file mode 100644 index 00000000..17a65aec --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/UseFetchXMLWithPaging/Program.cs @@ -0,0 +1,241 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Query; +using System.Text; +using System.Xml; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + class Program + { + private static readonly List entityStore = new(); + private static Guid parentAccountId; + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating sample account records..."); + + // Create parent account + var parentAccount = new Entity("account") + { + ["name"] = "Root Test Account", + ["emailaddress1"] = "root@root.com" + }; + parentAccountId = service.Create(parentAccount); + entityStore.Add(new EntityReference("account", parentAccountId)); + + // Create 10 child accounts + for (int i = 1; i <= 10; i++) + { + var childAccount = new Entity("account") + { + ["name"] = $"Child Test Account {i}", + ["emailaddress1"] = $"child{i}@root.com", + ["emailaddress2"] = "same@root.com", + ["parentaccountid"] = new EntityReference("account", parentAccountId) + }; + Guid childId = service.Create(childAccount); + entityStore.Add(new EntityReference("account", childId)); + } + + Console.WriteLine("Created 1 parent and 10 child accounts."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + // Define the fetch attributes. + // Set the number of records per page to retrieve. + int fetchCount = 3; + // Initialize the page number. + int pageNumber = 1; + // Initialize the number of records. + int recordCount = 0; + // Specify the current paging cookie. For retrieving the first page, + // pagingCookie should be null. + string? pagingCookie = null; + + // Create the FetchXml string for retrieving all child accounts to a parent account. + // This fetch query is using 1 placeholder to specify the parent account id + // for filtering out required accounts. Filter query is optional. + // Fetch query also includes optional order criteria that, in this case, is used + // to order the results in ascending order on the name data column. + string fetchXml = string.Format(@" + + + + + + + + + ", + parentAccountId); + + Console.WriteLine("Retrieving data in pages\n"); + Console.WriteLine("#\tAccount Name\t\t\tEmail Address"); + + while (true) + { + // Build fetchXml string with the placeholders. + string xml = CreateXml(fetchXml, pagingCookie, pageNumber, fetchCount); + + // Execute the fetch query and get the xml result. + var fetchRequest = new RetrieveMultipleRequest + { + Query = new FetchExpression(xml) + }; + + EntityCollection returnCollection = ((RetrieveMultipleResponse)service.Execute(fetchRequest)).EntityCollection; + + foreach (var c in returnCollection.Entities) + { + string name = c.Contains("name") ? c["name"].ToString() ?? "" : ""; + string email = c.Contains("emailaddress1") ? c["emailaddress1"].ToString() ?? "" : ""; + Console.WriteLine("{0}.\t{1}\t\t{2}", ++recordCount, name, email); + } + + // Check for morerecords, if it returns true. + if (returnCollection.MoreRecords) + { + Console.WriteLine("\n****************\nPage number {0}\n****************", pageNumber); + Console.WriteLine("#\tAccount Name\t\t\tEmail Address"); + + // Increment the page number to retrieve the next page. + pageNumber++; + + // Set the paging cookie to the paging cookie returned from current results. + pagingCookie = returnCollection.PagingCookie; + } + else + { + // If no more records in the result nodes, exit the loop. + break; + } + } + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("\nCleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Records deleted."); + } + } + + /// + /// Creates an XML string with paging information added to the FetchXML query. + /// + /// The base FetchXML query string + /// The paging cookie from the previous page (null for first page) + /// The page number to retrieve + /// The number of records per page + /// The FetchXML string with paging attributes added + private static string CreateXml(string xml, string? cookie, int page, int count) + { + StringReader stringReader = new StringReader(xml); + XmlTextReader reader = new XmlTextReader(stringReader); + + // Load document + XmlDocument doc = new XmlDocument(); + doc.Load(reader); + + XmlAttributeCollection attrs = doc.DocumentElement!.Attributes; + + if (cookie != null) + { + XmlAttribute pagingAttr = doc.CreateAttribute("paging-cookie"); + pagingAttr.Value = cookie; + attrs.Append(pagingAttr); + } + + XmlAttribute pageAttr = doc.CreateAttribute("page"); + pageAttr.Value = page.ToString(); + attrs.Append(pageAttr); + + XmlAttribute countAttr = doc.CreateAttribute("count"); + countAttr.Value = count.ToString(); + attrs.Append(countAttr); + + StringBuilder sb = new StringBuilder(1024); + StringWriter stringWriter = new StringWriter(sb); + + XmlTextWriter writer = new XmlTextWriter(stringWriter); + doc.WriteTo(writer); + writer.Close(); + + return sb.ToString(); + } + + #endregion + + #region Application Setup + + IConfiguration Configuration { get; } + + Program() + { + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/UseFetchXMLWithPaging/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/UseFetchXMLWithPaging/README.md new file mode 100644 index 00000000..ea923432 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/UseFetchXMLWithPaging/README.md @@ -0,0 +1,111 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates paging with FetchXML using page and paging-cookie attributes" +--- + +# UseFetchXMLWithPaging + +Demonstrates paging with FetchXML using page and paging-cookie attributes to retrieve records in pages + +## What this sample does + +This sample shows how to: +- Use FetchXML with paging attributes (page, count, paging-cookie) +- Execute FetchXML queries using RetrieveMultipleRequest +- Use paging cookies to navigate through result pages +- Handle the MoreRecords flag to determine when to stop paging +- Display results from multiple pages + +Paging is essential when working with large datasets to improve performance and avoid timeouts. + +## How this sample works + +### Setup + +The setup process: +1. Creates 1 parent account ("Root Test Account") +2. Creates 10 child accounts linked to the parent account + +### Run + +The main demonstration: +1. Creates a FetchXML query to retrieve child accounts +2. Defines paging parameters (fetchCount=3, pageNumber=1, pagingCookie=null) +3. Loops through pages: + - Calls CreateXml() to inject paging attributes into FetchXML + - Executes RetrieveMultipleRequest with FetchExpression + - Displays records with page separators + - Checks MoreRecords flag + - Updates pagingCookie from EntityCollection.PagingCookie + - Increments pageNumber +4. Continues until all records are retrieved (MoreRecords = false) + +### Cleanup + +The cleanup process deletes all created accounts (parent and children) in reverse order. + +## Demonstrates + +This sample demonstrates: +- **FetchXML**: Building XML-based queries with filters and ordering +- **Paging Attributes**: Using page, count, and paging-cookie attributes +- **XML Manipulation**: Dynamically adding attributes to FetchXML +- **RetrieveMultipleRequest**: Executing FetchXML with request/response pattern +- **PagingCookie**: Using cookies to maintain paging state +- **MoreRecords**: Determining if more pages exist +- **EntityCollection**: Working with paged result sets + +## Sample Output + +``` +Connected to Dataverse. + +Creating sample account records... +Created 1 parent and 10 child accounts. + +Retrieving data in pages + +# Account Name Email Address +1. Child Test Account 1 child1@root.com +2. Child Test Account 10 child10@root.com +3. Child Test Account 2 child2@root.com + +**************** +Page number 1 +**************** +# Account Name Email Address +4. Child Test Account 3 child3@root.com +5. Child Test Account 4 child4@root.com +6. Child Test Account 5 child5@root.com + +**************** +Page number 2 +**************** +# Account Name Email Address +7. Child Test Account 6 child6@root.com +8. Child Test Account 7 child7@root.com +9. Child Test Account 8 child8@root.com + +**************** +Page number 3 +**************** +# Account Name Email Address +10. Child Test Account 9 child9@root.com + +Cleaning up... +Deleting 11 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Page large result sets with FetchXML](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/page-large-result-sets-with-fetchxml) +[Build queries with FetchXML](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/use-fetchxml-construct-query) +[FetchXML reference](https://learn.microsoft.com/power-apps/developer/data-platform/fetchxml/reference/index) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/UseFetchXMLWithPaging/UseFetchXMLWithPaging.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/UseFetchXMLWithPaging/UseFetchXMLWithPaging.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/UseFetchXMLWithPaging/UseFetchXMLWithPaging.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/UseQueryExpressionwithPaging/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/UseQueryExpressionwithPaging/Program.cs new file mode 100644 index 00000000..58df6424 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/UseQueryExpressionwithPaging/Program.cs @@ -0,0 +1,180 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + class Program + { + private static readonly List entityStore = new(); + private static Guid parentAccountId; + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating sample account records..."); + + var parentAccount = new Entity("account") + { + ["name"] = "Root Test Account", + ["emailaddress1"] = "root@root.com" + }; + parentAccountId = service.Create(parentAccount); + entityStore.Add(new EntityReference("account", parentAccountId)); + + for (int i = 1; i <= 10; i++) + { + var childAccount = new Entity("account") + { + ["name"] = $"Child Test Account {i}", + ["emailaddress1"] = $"child{i}@root.com", + ["parentaccountid"] = new EntityReference("account", parentAccountId) + }; + Guid childId = service.Create(childAccount); + entityStore.Add(new EntityReference("account", childId)); + } + + Console.WriteLine("Created 1 parent and 10 child accounts."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + int queryCount = 3; + int pageNumber = 1; + int recordCount = 0; + + var pagecondition = new ConditionExpression + { + AttributeName = "parentaccountid", + Operator = ConditionOperator.Equal + }; + pagecondition.Values.Add(parentAccountId); + + var order = new OrderExpression + { + AttributeName = "name", + OrderType = OrderType.Ascending + }; + + var pagequery = new QueryExpression + { + EntityName = "account", + ColumnSet = new ColumnSet("name", "emailaddress1") + }; + pagequery.Criteria.AddCondition(pagecondition); + pagequery.Orders.Add(order); + + pagequery.PageInfo = new PagingInfo + { + Count = queryCount, + PageNumber = pageNumber, + PagingCookie = null + }; + + Console.WriteLine("Retrieving sample account records in pages...\n"); + Console.WriteLine("#\tAccount Name\t\t\tEmail Address"); + + while (true) + { + EntityCollection results = service.RetrieveMultiple(pagequery); + if (results.Entities != null) + { + foreach (Entity acct in results.Entities) + { + string name = acct.Contains("name") ? acct["name"].ToString() : ""; + string email = acct.Contains("emailaddress1") ? acct["emailaddress1"].ToString() : ""; + Console.WriteLine("{0}.\t{1}\t{2}", ++recordCount, name, email); + } + } + + if (results.MoreRecords) + { + Console.WriteLine("\n****************\nPage number {0}\n****************", pagequery.PageInfo.PageNumber); + Console.WriteLine("#\tAccount Name\t\t\tEmail Address"); + + pagequery.PageInfo.PageNumber++; + pagequery.PageInfo.PagingCookie = results.PagingCookie; + } + else + { + break; + } + } + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("\nCleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + IConfiguration Configuration { get; } + + Program() + { + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/UseQueryExpressionwithPaging/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/UseQueryExpressionwithPaging/README.md new file mode 100644 index 00000000..15d1d75e --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/UseQueryExpressionwithPaging/README.md @@ -0,0 +1,106 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates paging with QueryExpression using PagingInfo" +--- + +# UseQueryExpressionwithPaging + +Demonstrates paging with QueryExpression using PagingInfo and paging cookies + +## What this sample does + +This sample shows how to: +- Use PagingInfo with QueryExpression to retrieve records in pages +- Use paging cookies to navigate through result pages +- Handle the MoreRecords flag to determine when to stop paging +- Display results from multiple pages + +Paging is essential when working with large datasets to improve performance and avoid timeouts. + +## How this sample works + +### Setup + +The setup process: +1. Creates 1 parent account ("Root Test Account") +2. Creates 10 child accounts linked to the parent account + +### Run + +The main demonstration: +1. Creates a QueryExpression to retrieve child accounts +2. Sets PageInfo with Count=3 (page size) +3. Loops through pages: + - Retrieves records for current page + - Displays records with page separators + - Checks MoreRecords flag + - Updates PagingCookie for next page + - Increments PageNumber +4. Continues until all records are retrieved + +### Cleanup + +The cleanup process deletes all created accounts (parent and children). + +## Demonstrates + +This sample demonstrates: +- **QueryExpression**: Building queries with filters and ordering +- **PagingInfo**: Configuring page size and page number +- **PagingCookie**: Using cookies to navigate through pages +- **MoreRecords**: Determining if more pages exist +- **EntityCollection**: Working with paged result sets + +## Sample Output + +``` +Connected to Dataverse. + +Creating sample account records... +Created 1 parent and 10 child accounts. + +Retrieving sample account records in pages... + +# Account Name Email Address +1. Child Test Account 1 child1@root.com +2. Child Test Account 10 child10@root.com +3. Child Test Account 2 child2@root.com + +**************** +Page number 1 +**************** +# Account Name Email Address +4. Child Test Account 3 child3@root.com +5. Child Test Account 4 child4@root.com +6. Child Test Account 5 child5@root.com + +**************** +Page number 2 +**************** +# Account Name Email Address +7. Child Test Account 6 child6@root.com +8. Child Test Account 7 child7@root.com +9. Child Test Account 8 child8@root.com + +**************** +Page number 3 +**************** +# Account Name Email Address +10. Child Test Account 9 child9@root.com + +Cleaning up... +Deleting 11 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Page large result sets with QueryExpression](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/page-large-result-sets-with-queryexpression) +[Build queries with QueryExpression](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/build-queries-with-queryexpression) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/UseQueryExpressionwithPaging/UseQueryExpressionwithPaging.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/UseQueryExpressionwithPaging/UseQueryExpressionwithPaging.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/UseQueryExpressionwithPaging/UseQueryExpressionwithPaging.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/UseQueryExpressionwithPaging/temp.txt b/dataverse/orgsvc/CSharp-NETCore/Query/UseQueryExpressionwithPaging/temp.txt new file mode 100644 index 00000000..e69de29b diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/ValidateandExecuteSavedQuery/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Query/ValidateandExecuteSavedQuery/Program.cs new file mode 100644 index 00000000..d76ddb06 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/ValidateandExecuteSavedQuery/Program.cs @@ -0,0 +1,315 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using System.Text; +using System.Xml; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates validating and executing saved queries (views) + /// + /// + /// This sample shows how to: + /// 1. Create a saved query (system view) and user query (personal view) + /// 2. Validate the saved query using ValidateSavedQueryRequest + /// 3. Execute the saved query using ExecuteByIdSavedQueryRequest + /// 4. Execute a user query using ExecuteByIdUserQueryRequest + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating sample accounts..."); + + var account1 = new Entity("account") + { + ["name"] = "Coho Vineyard" + }; + Guid account1Id = service.Create(account1); + entityStore.Add(new EntityReference("account", account1Id)); + Console.WriteLine(" Created Account: {0}", account1["name"]); + + var account2 = new Entity("account") + { + ["name"] = "Coho Winery" + }; + Guid account2Id = service.Create(account2); + entityStore.Add(new EntityReference("account", account2Id)); + Console.WriteLine(" Created Account: {0}", account2["name"]); + + var account3 = new Entity("account") + { + ["name"] = "Coho Vineyard & Winery" + }; + Guid account3Id = service.Create(account3); + entityStore.Add(new EntityReference("account", account3Id)); + Console.WriteLine(" Created Account: {0}", account3["name"]); + + Console.WriteLine(); + Console.WriteLine("Creating a Saved Query that retrieves all Account names..."); + + var savedQuery = new Entity("savedquery") + { + ["name"] = "Fetch all Account ids", + ["returnedtypecode"] = "account", + ["fetchxml"] = @" + + + + + ", + ["querytype"] = 0 + }; + Guid savedQueryId = service.Create(savedQuery); + entityStore.Add(new EntityReference("savedquery", savedQueryId)); + + Console.WriteLine(); + Console.WriteLine("Creating a User Query that retrieves Account 'Coho Winery'..."); + + var userQuery = new Entity("userquery") + { + ["name"] = "Fetch Coho Winery", + ["returnedtypecode"] = "account", + ["fetchxml"] = @" + + + + + + + + ", + ["querytype"] = 0 + }; + Guid userQueryId = service.Create(userQuery); + entityStore.Add(new EntityReference("userquery", userQueryId)); + + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + // Get the saved query and user query from entityStore + var savedQueryRef = entityStore.First(e => e.LogicalName == "savedquery"); + var userQueryRef = entityStore.First(e => e.LogicalName == "userquery"); + + // Retrieve the saved query to get its FetchXml + var savedQuery = service.Retrieve("savedquery", savedQueryRef.Id, + new Microsoft.Xrm.Sdk.Query.ColumnSet("fetchxml", "querytype")); + + Console.WriteLine("Validating Saved Query"); + Console.WriteLine("======================"); + + // Create the validate request + var validateRequest = new ValidateSavedQueryRequest() + { + FetchXml = savedQuery.GetAttributeValue("fetchxml"), + QueryType = savedQuery.GetAttributeValue("querytype") + }; + + try + { + // Execute the validate request (will throw if invalid) + var validateResponse = (ValidateSavedQueryResponse)service.Execute(validateRequest); + Console.WriteLine(" Saved Query validated successfully"); + } + catch (Exception ex) + { + Console.WriteLine(" Invalid Saved Query: {0}", ex.Message); + throw; + } + + Console.WriteLine(); + Console.WriteLine("Executing Saved Query"); + Console.WriteLine("====================="); + + // Create the execute saved query request + var executeSavedQueryRequest = new ExecuteByIdSavedQueryRequest() + { + EntityId = savedQueryRef.Id + }; + + // Execute the saved query + var executeSavedQueryResponse = + (ExecuteByIdSavedQueryResponse)service.Execute(executeSavedQueryRequest); + + // Check results + if (string.IsNullOrEmpty(executeSavedQueryResponse.String)) + { + throw new Exception("Saved Query did not return any results"); + } + + PrintResults(executeSavedQueryResponse.String); + + Console.WriteLine(); + Console.WriteLine("Executing User Query"); + Console.WriteLine("===================="); + + // Create the execute user query request + var executeUserQueryRequest = new ExecuteByIdUserQueryRequest() + { + EntityId = userQueryRef + }; + + // Execute the user query + var executeUserQueryResponse = + (ExecuteByIdUserQueryResponse)service.Execute(executeUserQueryRequest); + + // Check results + if (string.IsNullOrEmpty(executeUserQueryResponse.String)) + { + throw new Exception("User Query did not return any results"); + } + + PrintResults(executeUserQueryResponse.String); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine(); + Console.WriteLine("Cleaning up..."); + + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + + // Delete in reverse order (important for dependencies) + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + + Console.WriteLine("Records deleted."); + } + } + + /// + /// Formats and prints the XML results from query execution + /// + private static void PrintResults(string response) + { + var output = new StringBuilder(); + using (XmlReader reader = XmlReader.Create(new StringReader(response))) + { + XmlWriterSettings settings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true + }; + + using (XmlWriter writer = XmlWriter.Create(output, settings)) + { + while (reader.Read()) + { + switch (reader.NodeType) + { + case XmlNodeType.Element: + writer.WriteStartElement(reader.Name); + break; + case XmlNodeType.Text: + writer.WriteString(reader.Value); + break; + case XmlNodeType.XmlDeclaration: + case XmlNodeType.ProcessingInstruction: + writer.WriteProcessingInstruction(reader.Name, reader.Value); + break; + case XmlNodeType.Comment: + writer.WriteComment(reader.Value); + break; + case XmlNodeType.EndElement: + writer.WriteFullEndElement(); + break; + } + } + } + } + + Console.WriteLine(" Result of query:"); + Console.WriteLine(output.ToString()); + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine(); + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + + if (ex.InnerException != null) + { + Console.WriteLine("Inner Exception: {0}", ex.InnerException.Message); + } + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/ValidateandExecuteSavedQuery/README.md b/dataverse/orgsvc/CSharp-NETCore/Query/ValidateandExecuteSavedQuery/README.md new file mode 100644 index 00000000..93bf4c0a --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/ValidateandExecuteSavedQuery/README.md @@ -0,0 +1,107 @@ +# Sample: Validate and execute a saved query + +This sample shows how to validate FetchXML queries and execute both saved queries (system views) and user queries (personal views) using the Dataverse SDK. + +## How to run this sample + +See [How to run samples](https://github.com/microsoft/PowerApps-Samples/blob/master/dataverse/README.md) for information about how to run this sample. + +## What this sample does + +The `ValidateSavedQueryRequest` message is used to validate that a saved query's FetchXML is well-formed and valid. + +The `ExecuteByIdSavedQueryRequest` message is used to execute a saved query (system view) by its ID and return the results as XML. + +The `ExecuteByIdUserQueryRequest` message is used to execute a user query (personal view) by its ID and return the results as XML. + +## How this sample works + +In order to simulate the scenario described in [What this sample does](#what-this-sample-does), the sample will do the following: + +### Setup + +1. Creates 3 sample account records: + - "Coho Vineyard" + - "Coho Winery" + - "Coho Vineyard & Winery" + +2. Creates a saved query (system view) that retrieves all account names using FetchXML: + ```xml + + + + + + ``` + +3. Creates a user query (personal view) that retrieves only the "Coho Winery" account using filtered FetchXML: + ```xml + + + + + + + + + ``` + +### Demonstrate + +1. **Validate the saved query:** + - Uses `ValidateSavedQueryRequest` to validate the FetchXML syntax + - The validation will throw an exception if the FetchXML is malformed or invalid + - Confirms successful validation + +2. **Execute the saved query:** + - Uses `ExecuteByIdSavedQueryRequest` to execute the saved query by ID + - Returns results as XML string containing all account names + - Formats and displays the XML results + +3. **Execute the user query:** + - Uses `ExecuteByIdUserQueryRequest` to execute the user query by ID + - Returns results as XML string containing only "Coho Winery" account + - Formats and displays the XML results + +### Clean up + +Displays an option to delete all the data created in the sample. The deletion is optional in case you want to examine the data created by the sample. You can manually delete the data to achieve the same results. + +## Key concepts + +### Saved Query vs User Query + +- **Saved Query (savedquery)**: System-wide views visible to all users (requires appropriate privileges to create) +- **User Query (userquery)**: Personal views created by and visible only to the user who created them + +### FetchXML Validation + +The `ValidateSavedQueryRequest` message validates: +- XML syntax correctness +- Entity and attribute names exist +- Operators are appropriate for attribute types +- Overall query structure is valid + +This is useful when building query editors or when constructing FetchXML programmatically to catch errors before execution. + +### Query Execution + +Both `ExecuteByIdSavedQueryRequest` and `ExecuteByIdUserQueryRequest`: +- Execute queries by their unique ID +- Return results as XML string (not EntityCollection) +- Are useful when you need the raw XML response format + +For most scenarios, using `RetrieveMultiple` with `FetchExpression` is more common and returns structured `EntityCollection` objects. + +## Related samples + +- [Use QueryExpression with paging](../UseQueryExpressionwithPaging/) +- [Retrieve multiple by QueryExpression](../RetrieveMultipleByQueryExpression/) +- [Retrieve multiple by QueryByAttribute](../RetrieveMultipleQueryByAttribute/) + +## Learn more + +- [Use FetchXML to construct a query](https://learn.microsoft.com/power-apps/developer/data-platform/fetchxml/overview) +- [ValidateSavedQueryRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.validatesavedqueryrequest) +- [ExecuteByIdSavedQueryRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.executebyidsavedqueryrequest) +- [ExecuteByIdUserQueryRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.executebyiduserqueryrequest) diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/ValidateandExecuteSavedQuery/ValidateandExecuteSavedQuery.csproj b/dataverse/orgsvc/CSharp-NETCore/Query/ValidateandExecuteSavedQuery/ValidateandExecuteSavedQuery.csproj new file mode 100644 index 00000000..ac33882c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/ValidateandExecuteSavedQuery/ValidateandExecuteSavedQuery.csproj @@ -0,0 +1,21 @@ + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Query/appsettings.json b/dataverse/orgsvc/CSharp-NETCore/Query/appsettings.json new file mode 100644 index 00000000..037aca85 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Query/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/AddRecordToQueue/AddRecordToQueue.csproj b/dataverse/orgsvc/CSharp-NETCore/Queues/AddRecordToQueue/AddRecordToQueue.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/AddRecordToQueue/AddRecordToQueue.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/AddRecordToQueue/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Queues/AddRecordToQueue/Program.cs new file mode 100644 index 00000000..548d1819 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/AddRecordToQueue/Program.cs @@ -0,0 +1,143 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates adding records to queues and moving between queues + /// + /// + /// This sample shows how to add records to queues and move them between queues. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating queues and letter..."); + + var sourceQueue = new Entity("queue") { ["name"] = "Source Queue", ["queueviewtype"] = new OptionSetValue(1) }; + Guid sourceQueueId = service.Create(sourceQueue); + entityStore.Add(new EntityReference("queue", sourceQueueId)); + + var destQueue = new Entity("queue") { ["name"] = "Destination Queue", ["queueviewtype"] = new OptionSetValue(1) }; + Guid destQueueId = service.Create(destQueue); + entityStore.Add(new EntityReference("queue", destQueueId)); + + var letter = new Entity("letter") { ["description"] = "Example Letter" }; + Guid letterId = service.Create(letter); + entityStore.Add(new EntityReference("letter", letterId)); + + service.Execute(new AddToQueueRequest + { + DestinationQueueId = sourceQueueId, + Target = new EntityReference("letter", letterId) + }); + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Moving letter between queues..."); + + service.Execute(new AddToQueueRequest + { + SourceQueueId = entityStore[0].Id, + Target = new EntityReference("letter", entityStore[2].Id), + DestinationQueueId = entityStore[1].Id + }); + + Console.WriteLine("Letter moved to destination queue."); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + foreach (var entityRef in entityStore) + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/AddRecordToQueue/README.md b/dataverse/orgsvc/CSharp-NETCore/Queues/AddRecordToQueue/README.md new file mode 100644 index 00000000..f1824a0f --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/AddRecordToQueue/README.md @@ -0,0 +1,84 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates adding records to queues and moving between queues" +--- + +# AddRecordToQueue + +Demonstrates adding records to queues and moving records between queues + +## What this sample does + +This sample shows how to: +- Create multiple queues (source and destination) +- Create an activity record (letter) +- Add a record to a queue using AddToQueueRequest +- Move a record from one queue to another queue + +Queues are used to organize work items and activities that need attention. This sample demonstrates the routing of work items between queues. + +## How this sample works + +### Setup + +The setup process: +1. Creates a "Source Queue" (private queue) +2. Creates a "Destination Queue" (private queue) +3. Creates a letter activity record +4. Adds the letter to the Source Queue using AddToQueueRequest + +### Run + +The main demonstration: +1. Retrieves the queue and letter IDs from the entity store +2. Executes AddToQueueRequest with: + - SourceQueueId: The source queue containing the letter + - Target: EntityReference to the letter activity + - DestinationQueueId: The destination queue to move the letter to +3. The letter is moved from the source queue to the destination queue + +### Cleanup + +The cleanup process deletes all created records: +- Source queue +- Destination queue +- Letter activity + +## Demonstrates + +This sample demonstrates: +- **AddToQueueRequest**: Adding records to queues and routing between queues +- **Queue routing**: Moving work items from one queue to another +- **Activity management**: Working with activity entities (letter) in queues +- **EntityReference**: Referencing entities across operations + +## Sample Output + +``` +Connected to Dataverse. + +Creating queues and letter... + Created Source Queue + Created Destination Queue + Created letter activity + Added letter to Source Queue +Setup complete. + +Moving letter between queues... +Letter moved to destination queue. +Cleaning up... +Deleting 3 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Work with queues](https://learn.microsoft.com/power-apps/developer/data-platform/work-with-queues) +[AddToQueueRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.addtoqueuerequest) diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/AddSecurityPrincipalToQueue/AddSecurityPrincipalToQueue.csproj b/dataverse/orgsvc/CSharp-NETCore/Queues/AddSecurityPrincipalToQueue/AddSecurityPrincipalToQueue.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/AddSecurityPrincipalToQueue/AddSecurityPrincipalToQueue.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/AddSecurityPrincipalToQueue/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Queues/AddSecurityPrincipalToQueue/Program.cs new file mode 100644 index 00000000..1f793b5a --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/AddSecurityPrincipalToQueue/Program.cs @@ -0,0 +1,151 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates adding security principals to queues + /// + /// + /// This sample shows how to add a team to a queue using AddPrincipalToQueueRequest. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + private static Guid queueId; + private static Guid teamId; + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating queue and team..."); + + var queue = new Entity("queue") { ["name"] = "Example Queue" }; + queueId = service.Create(queue); + entityStore.Add(new EntityReference("queue", queueId)); + + // Get default business unit + var query = new QueryExpression("businessunit") + { + ColumnSet = new ColumnSet("businessunitid"), + Criteria = new FilterExpression() + }; + query.Criteria.AddCondition("parentbusinessunitid", ConditionOperator.Null); + var defaultBU = service.RetrieveMultiple(query).Entities[0]; + + var team = new Entity("team") + { + ["name"] = "Example Team", + ["businessunitid"] = new EntityReference("businessunit", defaultBU.Id) + }; + teamId = service.Create(team); + entityStore.Add(new EntityReference("team", teamId)); + + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Adding team to queue..."); + + var teamEntity = service.Retrieve("team", teamId, new ColumnSet("name")); + + service.Execute(new AddPrincipalToQueueRequest + { + Principal = teamEntity, + QueueId = queueId + }); + + Console.WriteLine("Team added to queue."); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/AddSecurityPrincipalToQueue/README.md b/dataverse/orgsvc/CSharp-NETCore/Queues/AddSecurityPrincipalToQueue/README.md new file mode 100644 index 00000000..006f25e6 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/AddSecurityPrincipalToQueue/README.md @@ -0,0 +1,79 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates adding security principals (teams/users) as queue members" +--- + +# AddSecurityPrincipalToQueue + +Demonstrates adding security principals (teams/users) as queue members + +## What this sample does + +This sample shows how to: +- Create a queue and a team +- Add a security principal (team) to a queue using AddPrincipalToQueueRequest +- Configure queue membership for teams + +Adding security principals to queues makes them members of the queue, allowing them to work with queue items. This differs from sharing access - queue membership provides a direct association between the principal and the queue. + +## How this sample works + +### Setup + +The setup process: +1. Creates a queue +2. Retrieves the default business unit (where parentbusinessunitid is null) +3. Creates a team associated with the default business unit + +### Run + +The main demonstration: +1. Retrieves the team entity with its name column +2. Executes AddPrincipalToQueueRequest with: + - Principal: The team entity (not just EntityReference) + - QueueId: The ID of the queue +3. The team is added as a member of the queue +4. Team members can now work with items in this queue + +### Cleanup + +The cleanup process deletes all created records: +- Team +- Queue + +## Demonstrates + +This sample demonstrates: +- **AddPrincipalToQueueRequest**: Adding security principals to queue membership +- **Queue membership**: Understanding the relationship between principals and queues +- **Entity retrieval**: Retrieving full entity records for request parameters +- **Business unit query**: Retrieving the default business unit +- **Team management**: Creating and associating teams with queues + +## Sample Output + +``` +Connected to Dataverse. + +Creating queue and team... +Setup complete. + +Adding team to queue... +Team added to queue. +Cleaning up... +Deleting 2 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Work with queues](https://learn.microsoft.com/power-apps/developer/data-platform/work-with-queues) +[AddPrincipalToQueueRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.addprincipaltoqueuerequest) +[Queue entity reference](https://learn.microsoft.com/power-apps/developer/data-platform/reference/entities/queue) diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/CleanHistoryQueue/CleanHistoryQueue.csproj b/dataverse/orgsvc/CSharp-NETCore/Queues/CleanHistoryQueue/CleanHistoryQueue.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/CleanHistoryQueue/CleanHistoryQueue.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/CleanHistoryQueue/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Queues/CleanHistoryQueue/Program.cs new file mode 100644 index 00000000..f9f9d9e2 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/CleanHistoryQueue/Program.cs @@ -0,0 +1,146 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates removing completed items from queues + /// + /// + /// This sample shows how to remove completed/inactive items from queues using RemoveFromQueueRequest. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + private static Guid queueItemId; + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating queue and phone call activity..."); + + var queue = new Entity("queue") { ["name"] = "Example Queue", ["queueviewtype"] = new OptionSetValue(1) }; + Guid queueId = service.Create(queue); + entityStore.Add(new EntityReference("queue", queueId)); + + var phoneCall = new Entity("phonecall") { ["description"] = "Example Phone Call" }; + Guid phoneCallId = service.Create(phoneCall); + entityStore.Add(new EntityReference("phonecall", phoneCallId)); + + var queueItem = new Entity("queueitem") + { + ["queueid"] = new EntityReference("queue", queueId), + ["objectid"] = new EntityReference("phonecall", phoneCallId) + }; + queueItemId = service.Create(queueItem); + + // Mark phone call as completed + service.Execute(new SetStateRequest + { + EntityMoniker = new EntityReference("phonecall", phoneCallId), + State = new OptionSetValue(1), // Completed + Status = new OptionSetValue(2) // Made + }); + + Console.WriteLine("Setup complete - phone call added to queue and marked as completed."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Removing completed phone call from queue..."); + + service.Execute(new RemoveFromQueueRequest { QueueItemId = queueItemId }); + + Console.WriteLine("Completed item removed from queue."); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + foreach (var entityRef in entityStore) + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/CleanHistoryQueue/README.md b/dataverse/orgsvc/CSharp-NETCore/Queues/CleanHistoryQueue/README.md new file mode 100644 index 00000000..68a8097f --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/CleanHistoryQueue/README.md @@ -0,0 +1,78 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates removing completed items from queue history" +--- + +# CleanHistoryQueue + +Demonstrates removing completed items from queue history + +## What this sample does + +This sample shows how to: +- Create a queue and add an activity (phone call) +- Mark an activity as completed using SetStateRequest +- Remove the completed activity from the queue using RemoveFromQueueRequest + +Queues can accumulate completed work items over time. This sample demonstrates how to clean up queue history by removing completed items, helping maintain queue organization and performance. + +## How this sample works + +### Setup + +The setup process: +1. Creates a private queue +2. Creates a phone call activity record +3. Marks the phone call as completed using SetStateRequest: + - State: Completed (1) + - Status: Made (2) +4. Creates a queue item linking the completed phone call to the queue + +### Run + +The main demonstration: +1. Executes RemoveFromQueueRequest with the queue item ID +2. The completed phone call is removed from the queue +3. The queue history is cleaned + +### Cleanup + +The cleanup process deletes all created records: +- Queue +- Phone call activity + +## Demonstrates + +This sample demonstrates: +- **RemoveFromQueueRequest**: Removing items from queues +- **SetStateRequest**: Changing activity state to completed +- **Queue maintenance**: Managing queue history and completed items +- **Activity lifecycle**: Understanding activity state transitions + +## Sample Output + +``` +Connected to Dataverse. + +Creating queue and phone call... +Setup complete. + +Removing completed item from queue... +Completed item removed from queue. +Cleaning up... +Deleting 2 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Work with queues](https://learn.microsoft.com/power-apps/developer/data-platform/work-with-queues) +[RemoveFromQueueRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.removefromqueuerequest) +[SetStateRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.setstaterequest) diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/CreateQueue/CreateQueue.csproj b/dataverse/orgsvc/CSharp-NETCore/Queues/CreateQueue/CreateQueue.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/CreateQueue/CreateQueue.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/CreateQueue/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Queues/CreateQueue/Program.cs new file mode 100644 index 00000000..cdeb8a7e --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/CreateQueue/Program.cs @@ -0,0 +1,155 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates how to create a queue with various configuration options + /// + /// + /// This sample shows how to: + /// - Create a queue entity + /// - Configure queue properties including email delivery and filtering methods + /// - Set queue view type (public/private) + /// + /// Prerequisites: + /// - System Administrator or System Customizer role + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + /// + /// Sets up sample data required for the demonstration + /// + private static void Setup(ServiceClient service) + { + // No setup required for this sample + } + + /// + /// Demonstrates creating a queue with configuration options + /// + private static void Run(ServiceClient service) + { + Console.WriteLine("Creating a queue..."); + + // Create a queue with various property values + var newQueue = new Entity("queue") + { + ["name"] = "Example Queue", + ["description"] = "This is an example queue.", + ["incomingemaildeliverymethod"] = new OptionSetValue(0), // None + ["incomingemailfilteringmethod"] = new OptionSetValue(0), // All Email Messages + ["outgoingemaildeliverymethod"] = new OptionSetValue(0), // None + ["queueviewtype"] = new OptionSetValue(1) // Private + }; + + Guid queueId = service.Create(newQueue); + entityStore.Add(new EntityReference("queue", queueId)); + + Console.WriteLine($"Created queue: {newQueue["name"]}"); + Console.WriteLine($" Queue ID: {queueId}"); + Console.WriteLine($" Description: {newQueue["description"]}"); + Console.WriteLine($" Incoming Email Delivery Method: None (0)"); + Console.WriteLine($" Incoming Email Filtering Method: All Email Messages (0)"); + Console.WriteLine($" Outgoing Email Delivery Method: None (0)"); + Console.WriteLine($" Queue View Type: Private (1)"); + Console.WriteLine(); + + Console.WriteLine("Queue creation complete."); + } + + /// + /// Cleans up sample data created during execution + /// + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + foreach (var entityRef in entityStore) + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/CreateQueue/README.md b/dataverse/orgsvc/CSharp-NETCore/Queues/CreateQueue/README.md new file mode 100644 index 00000000..de5525e6 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/CreateQueue/README.md @@ -0,0 +1,81 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates how to create a queue with various configuration options" +--- + +# CreateQueue + +Demonstrates how to create a queue with various configuration options + +## What this sample does + +This sample shows how to: +- Create a queue entity with configuration properties +- Set queue properties including email delivery methods, filtering methods, and view type +- Configure a private queue that can be used for work item routing + +Queues in Dataverse are used to organize and prioritize work items, enabling teams to manage activities, cases, and other records that require action. + +## How this sample works + +### Setup + +No setup is required for this sample. + +### Run + +The main demonstration: +1. Creates a queue entity with the following properties: + - Name: "Example Queue" + - Description: "This is an example queue." + - Incoming email delivery method: None (0) + - Incoming email filtering method: All Email Messages (0) + - Outgoing email delivery method: None (0) + - Queue view type: Private (1) + +2. Displays the created queue information including ID and configuration settings + +### Cleanup + +The cleanup process deletes the created queue record. + +## Demonstrates + +This sample demonstrates: +- **Entity creation**: Creating queue records using late-bound syntax +- **OptionSetValue**: Setting option set values for queue configuration properties +- **Queue configuration**: Understanding queue property options including: + - Email delivery methods (None, Email Router, Forward Mailbox) + - Email filtering methods (All Messages, Responses Only, From Leads/Contacts/Accounts) + - Queue view types (Public, Private) + +## Sample Output + +``` +Connected to Dataverse. + +Creating a queue... +Created queue: Example Queue (ID: a1234567-89ab-cdef-0123-456789abcdef) + Description: This is an example queue. + Incoming Email Delivery Method: None (0) + Incoming Email Filtering Method: All Email Messages (0) + Outgoing Email Delivery Method: None (0) + Queue View Type: Private (1) + +Queue creation complete. +Cleaning up... +Deleting 1 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Work with queues](https://learn.microsoft.com/power-apps/developer/data-platform/work-with-queues) +[Queue entity reference](https://learn.microsoft.com/power-apps/developer/data-platform/reference/entities/queue) diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/DeleteQueue/DeleteQueue.csproj b/dataverse/orgsvc/CSharp-NETCore/Queues/DeleteQueue/DeleteQueue.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/DeleteQueue/DeleteQueue.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/DeleteQueue/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Queues/DeleteQueue/Program.cs new file mode 100644 index 00000000..f476b387 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/DeleteQueue/Program.cs @@ -0,0 +1,136 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates creating and deleting a queue + /// + /// + /// This sample shows how to create and delete a queue entity. + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + /// + /// Sets up sample data required for the demonstration + /// + private static void Setup(ServiceClient service) + { + // No setup required + } + + /// + /// Demonstrates creating and deleting a queue + /// + private static void Run(ServiceClient service) + { + Console.WriteLine("Creating a queue..."); + + var newQueue = new Entity("queue") + { + ["name"] = "Example Queue", + ["description"] = "This is an example queue." + }; + + Guid queueId = service.Create(newQueue); + entityStore.Add(new EntityReference("queue", queueId)); + + Console.WriteLine($"Created queue: {newQueue["name"]} (ID: {queueId})"); + Console.WriteLine("Queue will be deleted during cleanup."); + } + + /// + /// Cleans up sample data created during execution + /// + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + foreach (var entityRef in entityStore) + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/DeleteQueue/README.md b/dataverse/orgsvc/CSharp-NETCore/Queues/DeleteQueue/README.md new file mode 100644 index 00000000..fe6f8f22 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/DeleteQueue/README.md @@ -0,0 +1,65 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates creating and deleting a queue" +--- + +# DeleteQueue + +Demonstrates creating and deleting a queue + +## What this sample does + +This sample shows how to: +- Create a queue record +- Delete a queue record using the Delete method + +This demonstrates the basic lifecycle of queue management, showing both creation and deletion operations. + +## How this sample works + +### Setup + +No setup is required for this sample. + +### Run + +The main demonstration: +1. Creates a queue entity with name "Example Queue" +2. Displays the created queue's ID +3. Indicates the queue will be deleted during cleanup + +### Cleanup + +The cleanup process deletes the created queue record using the standard Delete method. + +## Demonstrates + +This sample demonstrates: +- **Entity creation**: Creating queue records +- **Entity deletion**: Using service.Delete() to remove queue records +- **Queue lifecycle management**: Basic CRUD operations for queues + +## Sample Output + +``` +Connected to Dataverse. + +Creating a queue... +Created queue: Example Queue (ID: a1234567-89ab-cdef-0123-456789abcdef) +Queue will be deleted during cleanup. +Cleaning up... +Deleting 1 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Work with queues](https://learn.microsoft.com/power-apps/developer/data-platform/work-with-queues) +[Queue entity reference](https://learn.microsoft.com/power-apps/developer/data-platform/reference/entities/queue) diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/Queues.slnx b/dataverse/orgsvc/CSharp-NETCore/Queues/Queues.slnx new file mode 100644 index 00000000..32f431cf --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/Queues.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/README.md b/dataverse/orgsvc/CSharp-NETCore/Queues/README.md new file mode 100644 index 00000000..3cf412f0 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/README.md @@ -0,0 +1,60 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates queue operations including creating queues, adding records to queues, and managing queue items" +--- + +# Queues +Demonstrates queue operations including creating queues, adding records to queues, and managing queue items + +## Samples + +This folder contains the following samples: + +|Sample folder|Description|Build target| +|---|---|---| +|[CreateQueue](CreateQueue)|Demonstrates creating a queue with configuration options|.NET 6| +|[DeleteQueue](DeleteQueue)|Demonstrates creating and deleting a queue|.NET 6| +|[AddRecordToQueue](AddRecordToQueue)|Demonstrates adding records to queues and moving between queues|.NET 6| +|[ReleaseQueueItems](ReleaseQueueItems)|Demonstrates releasing queue items from workers|.NET 6| +|[SpecifyQueueItem](SpecifyQueueItem)|Demonstrates assigning queue items to specific workers|.NET 6| +|[CleanHistoryQueue](CleanHistoryQueue)|Demonstrates removing completed items from queues|.NET 6| +|[ShareQueue](ShareQueue)|Demonstrates sharing queue access with teams|.NET 6| +|[AddSecurityPrincipalToQueue](AddSecurityPrincipalToQueue)|Demonstrates adding security principals to queues|.NET 6| + +## Prerequisites + +- Microsoft Visual Studio 2022 +- Access to Dataverse with appropriate privileges for the operations demonstrated + +## How to run samples + +1. Clone or download the PowerApps-Samples repository +2. Navigate to `/dataverse/orgsvc/CSharp-NETCore/Queues/` +3. Open `Queues.sln` in Visual Studio 2022 +4. Edit the `appsettings.json` file in the category folder root with your Dataverse environment details: + - Set `Url` to your Dataverse environment URL + - Set `Username` to your user account +5. Build and run the desired sample project + +## appsettings.json + +Each sample in this category references the shared `appsettings.json` file in the category root folder. The connection string format is: + +```json +{ + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } +} +``` + +You can also set the `DATAVERSE_APPSETTINGS` environment variable to point to a custom appsettings.json file location if you prefer to keep your connection string outside the repository. + +## See also + +[SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/overview) diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/ReleaseQueueItems/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Queues/ReleaseQueueItems/Program.cs new file mode 100644 index 00000000..984dd62b --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/ReleaseQueueItems/Program.cs @@ -0,0 +1,148 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates releasing queue items from workers + /// + /// + /// This sample shows how to release a queue item from a worker using ReleaseToQueueRequest. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + private static Guid queueItemId; + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating queue, letter, and queue item..."); + + var queue = new Entity("queue") { ["name"] = "Example Queue", ["queueviewtype"] = new OptionSetValue(1) }; + Guid queueId = service.Create(queue); + entityStore.Add(new EntityReference("queue", queueId)); + + var letter = new Entity("letter") { ["description"] = "Example Letter" }; + Guid letterId = service.Create(letter); + entityStore.Add(new EntityReference("letter", letterId)); + + var queueItem = new Entity("queueitem") + { + ["queueid"] = new EntityReference("queue", queueId), + ["objectid"] = new EntityReference("letter", letterId) + }; + queueItemId = service.Create(queueItem); + entityStore.Add(new EntityReference("queueitem", queueItemId)); + + // Get current user and assign as worker + var whoAmI = (WhoAmIResponse)service.Execute(new WhoAmIRequest()); + var updateItem = new Entity("queueitem") + { + Id = queueItemId, + ["workerid"] = new EntityReference("systemuser", whoAmI.UserId) + }; + service.Update(updateItem); + + Console.WriteLine("Setup complete - queue item assigned to worker."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Releasing queue item from worker..."); + + service.Execute(new ReleaseToQueueRequest { QueueItemId = queueItemId }); + + Console.WriteLine("Queue item released from worker."); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + foreach (var entityRef in entityStore) + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/ReleaseQueueItems/README.md b/dataverse/orgsvc/CSharp-NETCore/Queues/ReleaseQueueItems/README.md new file mode 100644 index 00000000..dff41284 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/ReleaseQueueItems/README.md @@ -0,0 +1,77 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates releasing queue items from workers back to the queue" +--- + +# ReleaseQueueItems + +Demonstrates releasing queue items from workers back to the queue + +## What this sample does + +This sample shows how to: +- Create a queue and add a queue item +- Assign a queue item to a worker (user) +- Release the queue item from the worker back to the queue using ReleaseToQueueRequest + +When a queue item is assigned to a worker, they become responsible for handling it. This sample demonstrates how to release that assignment and return the item to the general queue pool, making it available for other workers to pick up. + +## How this sample works + +### Setup + +The setup process: +1. Creates a private queue +2. Creates a letter activity record +3. Creates a queue item linking the letter to the queue +4. Retrieves the current user's ID using WhoAmIRequest +5. Assigns the queue item to the current user by updating the workerid field + +### Run + +The main demonstration: +1. Executes ReleaseToQueueRequest with the queue item ID +2. The queue item is released from the worker's assignment +3. The item becomes available in the queue for other workers to pick + +### Cleanup + +The cleanup process deletes all created records: +- Queue +- Letter activity +- Queue item (automatically deleted when queue or letter is deleted) + +## Demonstrates + +This sample demonstrates: +- **ReleaseToQueueRequest**: Releasing queue items from worker assignments +- **WhoAmIRequest**: Getting the current user's identity +- **Queue item assignment**: Understanding worker assignment workflow +- **Queue item lifecycle**: Managing work item states (assigned vs. available) + +## Sample Output + +``` +Connected to Dataverse. + +Creating queue and queue item... +Setup complete. + +Releasing queue item... +Queue item released from worker. +Cleaning up... +Deleting 2 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Work with queues](https://learn.microsoft.com/power-apps/developer/data-platform/work-with-queues) +[ReleaseToQueueRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.releasetoqueuerequest) diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/ReleaseQueueItems/ReleaseQueueItems.csproj b/dataverse/orgsvc/CSharp-NETCore/Queues/ReleaseQueueItems/ReleaseQueueItems.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/ReleaseQueueItems/ReleaseQueueItems.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/ShareQueue/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Queues/ShareQueue/Program.cs new file mode 100644 index 00000000..f4361758 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/ShareQueue/Program.cs @@ -0,0 +1,153 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates sharing queue access with teams + /// + /// + /// This sample shows how to share a queue with a team using GrantAccessRequest. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + private static Guid queueId; + private static Guid teamId; + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating queue and team..."); + + var queue = new Entity("queue") { ["name"] = "Example Queue", ["queueviewtype"] = new OptionSetValue(1) }; + queueId = service.Create(queue); + entityStore.Add(new EntityReference("queue", queueId)); + + // Get default business unit + var query = new QueryExpression("businessunit") + { + ColumnSet = new ColumnSet("businessunitid"), + Criteria = new FilterExpression() + }; + query.Criteria.AddCondition("parentbusinessunitid", ConditionOperator.Null); + var defaultBU = service.RetrieveMultiple(query).Entities[0]; + + var team = new Entity("team") + { + ["name"] = "Example Team", + ["businessunitid"] = new EntityReference("businessunit", defaultBU.Id) + }; + teamId = service.Create(team); + entityStore.Add(new EntityReference("team", teamId)); + + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Sharing queue with team..."); + + service.Execute(new GrantAccessRequest + { + PrincipalAccess = new PrincipalAccess + { + Principal = new EntityReference("team", teamId), + AccessMask = AccessRights.ReadAccess | AccessRights.AppendToAccess + }, + Target = new EntityReference("queue", queueId) + }); + + Console.WriteLine("Queue access granted to team."); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + for (int i = entityStore.Count - 1; i >= 0; i--) + { + service.Delete(entityStore[i].LogicalName, entityStore[i].Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/ShareQueue/README.md b/dataverse/orgsvc/CSharp-NETCore/Queues/ShareQueue/README.md new file mode 100644 index 00000000..60771b26 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/ShareQueue/README.md @@ -0,0 +1,80 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates sharing queue access with teams using security permissions" +--- + +# ShareQueue + +Demonstrates sharing queue access with teams using security permissions + +## What this sample does + +This sample shows how to: +- Create a private queue and a team +- Share queue access with a team using GrantAccessRequest +- Configure specific access rights (Read and AppendTo) for the team + +Private queues require explicit sharing to grant access to users or teams. This sample demonstrates how to programmatically configure queue sharing and access control. + +## How this sample works + +### Setup + +The setup process: +1. Creates a private queue (queueviewtype = 1) +2. Retrieves the default business unit (where parentbusinessunitid is null) +3. Creates a team associated with the default business unit + +### Run + +The main demonstration: +1. Executes GrantAccessRequest with: + - PrincipalAccess containing: + - Principal: EntityReference to the team + - AccessMask: ReadAccess | AppendToAccess (combined rights) + - Target: EntityReference to the queue +2. The team is granted read and append-to access to the queue +3. Team members can now view and add items to the queue + +### Cleanup + +The cleanup process deletes all created records: +- Team +- Queue + +## Demonstrates + +This sample demonstrates: +- **GrantAccessRequest**: Sharing record access with security principals +- **PrincipalAccess**: Configuring access rights for principals +- **AccessRights**: Using AccessMask flags (ReadAccess, AppendToAccess) +- **Security model**: Understanding Dataverse record-level security +- **Business unit query**: Retrieving the default business unit + +## Sample Output + +``` +Connected to Dataverse. + +Creating queue and team... +Setup complete. + +Sharing queue with team... +Queue access granted to team. +Cleaning up... +Deleting 2 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Work with queues](https://learn.microsoft.com/power-apps/developer/data-platform/work-with-queues) +[GrantAccessRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.grantaccessrequest) +[Security concepts in Dataverse](https://learn.microsoft.com/power-apps/developer/data-platform/security-concepts) diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/ShareQueue/ShareQueue.csproj b/dataverse/orgsvc/CSharp-NETCore/Queues/ShareQueue/ShareQueue.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/ShareQueue/ShareQueue.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/SpecifyQueueItem/Program.cs b/dataverse/orgsvc/CSharp-NETCore/Queues/SpecifyQueueItem/Program.cs new file mode 100644 index 00000000..17e1c52f --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/SpecifyQueueItem/Program.cs @@ -0,0 +1,146 @@ +using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// Demonstrates assigning queue items to specific workers + /// + /// + /// This sample shows how to assign a queue item to a worker using PickFromQueueRequest. + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + private static Guid queueItemId; + + #region Sample Methods + + private static void Setup(ServiceClient service) + { + Console.WriteLine("Creating queue, letter, and queue item..."); + + var queue = new Entity("queue") { ["name"] = "Example Queue", ["queueviewtype"] = new OptionSetValue(1) }; + Guid queueId = service.Create(queue); + entityStore.Add(new EntityReference("queue", queueId)); + + var letter = new Entity("letter") { ["description"] = "Example Letter" }; + Guid letterId = service.Create(letter); + entityStore.Add(new EntityReference("letter", letterId)); + + var queueItem = new Entity("queueitem") + { + ["queueid"] = new EntityReference("queue", queueId), + ["objectid"] = new EntityReference("letter", letterId) + }; + queueItemId = service.Create(queueItem); + entityStore.Add(new EntityReference("queueitem", queueItemId)); + + Console.WriteLine("Setup complete."); + Console.WriteLine(); + } + + private static void Run(ServiceClient service) + { + Console.WriteLine("Assigning queue item to current user..."); + + var whoAmI = (WhoAmIResponse)service.Execute(new WhoAmIRequest()); + var currentUser = service.Retrieve("systemuser", whoAmI.UserId, new ColumnSet("fullname")); + + service.Execute(new PickFromQueueRequest + { + QueueItemId = queueItemId, + WorkerId = whoAmI.UserId + }); + + Console.WriteLine($"Queue item assigned to {currentUser["fullname"]}."); + } + + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created record(s)..."); + foreach (var entityRef in entityStore) + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + Console.WriteLine("Records deleted."); + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/SpecifyQueueItem/README.md b/dataverse/orgsvc/CSharp-NETCore/Queues/SpecifyQueueItem/README.md new file mode 100644 index 00000000..52dcf9c2 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/SpecifyQueueItem/README.md @@ -0,0 +1,79 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "Demonstrates assigning queue items to specific workers" +--- + +# SpecifyQueueItem + +Demonstrates assigning queue items to specific workers + +## What this sample does + +This sample shows how to: +- Create a queue and add a queue item +- Assign a queue item to a specific worker using PickFromQueueRequest +- Retrieve the current user's information for assignment +- Display the worker's name after successful assignment + +Queue items represent work that needs to be completed. This sample demonstrates how to assign specific queue items to designated workers who will be responsible for handling them. + +## How this sample works + +### Setup + +The setup process: +1. Creates a private queue +2. Creates a letter activity record +3. Creates a queue item linking the letter to the queue + +### Run + +The main demonstration: +1. Retrieves the current user's ID using WhoAmIRequest +2. Retrieves the current user's full name from the systemuser entity +3. Executes PickFromQueueRequest with: + - QueueItemId: The queue item to assign + - WorkerId: The ID of the user to assign it to (current user) +4. Displays the worker's name confirming the assignment + +### Cleanup + +The cleanup process deletes all created records: +- Queue +- Letter activity +- Queue item (automatically deleted when queue or letter is deleted) + +## Demonstrates + +This sample demonstrates: +- **PickFromQueueRequest**: Assigning queue items to specific workers +- **WhoAmIRequest**: Getting the current user's identity +- **Entity retrieval**: Retrieving user information for display +- **Queue item assignment**: Understanding worker assignment workflow + +## Sample Output + +``` +Connected to Dataverse. + +Creating queue and queue item... +Setup complete. + +Assigning queue item to worker... +Queue item assigned to John Doe. +Cleaning up... +Deleting 2 created record(s)... +Records deleted. + +Press any key to exit. +``` + +## See also + +[Work with queues](https://learn.microsoft.com/power-apps/developer/data-platform/work-with-queues) +[PickFromQueueRequest Class](https://learn.microsoft.com/dotnet/api/microsoft.crm.sdk.messages.pickfromqueuerequest) diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/SpecifyQueueItem/SpecifyQueueItem.csproj b/dataverse/orgsvc/CSharp-NETCore/Queues/SpecifyQueueItem/SpecifyQueueItem.csproj new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/SpecifyQueueItem/SpecifyQueueItem.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Queues/appsettings.json b/dataverse/orgsvc/CSharp-NETCore/Queues/appsettings.json new file mode 100644 index 00000000..037aca85 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Queues/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Scripts/Convert-LegacySample.ps1 b/dataverse/orgsvc/CSharp-NETCore/Scripts/Convert-LegacySample.ps1 new file mode 100644 index 00000000..7d4cdce3 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Scripts/Convert-LegacySample.ps1 @@ -0,0 +1,151 @@ +# Convert-LegacySample.ps1 +# Assists with transforming legacy code to modern patterns +# Performs automated text replacements and outputs transformed code for review + +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [string]$LegacySamplePath, + + [Parameter(Mandatory=$false)] + [string]$OutputPath = "" +) + +$ErrorActionPreference = "Stop" + +# Validate legacy sample path exists +if (-not (Test-Path $LegacySamplePath)) { + Write-Error "Legacy sample path does not exist: $LegacySamplePath" + exit 1 +} + +Write-Host "Analyzing legacy sample: $LegacySamplePath" -ForegroundColor Cyan +Write-Host "" + +# Find source files +$csFiles = Get-ChildItem -Path $LegacySamplePath -Filter "*.cs" -Recurse | Where-Object { + $_.FullName -notlike "*\obj\*" -and + $_.FullName -notlike "*\bin\*" -and + $_.Name -ne "AssemblyInfo.cs" +} + +if ($csFiles.Count -eq 0) { + Write-Error "No C# source files found in $LegacySamplePath" + exit 1 +} + +Write-Host "Found $($csFiles.Count) source file(s):" +$csFiles | ForEach-Object { Write-Host " - $($_.Name)" } +Write-Host "" + +# Function to transform code +function Transform-Code { + param( + [string]$content + ) + + # Replace CrmServiceClient with ServiceClient + $content = $content -replace '\bCrmServiceClient\b', 'ServiceClient' + + # Replace using Microsoft.Xrm.Tooling.Connector + $content = $content -replace 'using Microsoft\.Xrm\.Tooling\.Connector;', '// Removed: using Microsoft.Xrm.Tooling.Connector;' + + # Replace namespace PowerApps.Samples with PowerPlatform.Dataverse.CodeSamples + $content = $content -replace '\bnamespace PowerApps\.Samples\b', 'namespace PowerPlatform.Dataverse.CodeSamples' + + # Replace service.IsReady checks (common pattern) + $content = $content -replace 'if \(service\.IsReady\)', 'if (!service.IsReady)' + + # Comment out SampleHelpers usage + $content = $content -replace '\bSampleHelpers\.Connect\b', '// TODO: Replace with ServiceClient initialization from appsettings.json - SampleHelpers.Connect' + $content = $content -replace '\bSampleHelpers\.\w+', '// TODO: Review and update - $&' + + # Comment out SystemUserProvider usage + $content = $content -replace '\bSystemUserProvider\.\w+', '// TODO: Replace with modern pattern - $&' + + # Add TODO markers for WhoAmIRequest pattern (common) + if ($content -match 'WhoAmIRequest') { + $content = "// TODO: Review WhoAmIRequest usage for modern pattern`r`n" + $content + } + + return $content +} + +# Process each file +$transformedFiles = @() + +foreach ($file in $csFiles) { + Write-Host "Processing: $($file.Name)" -ForegroundColor Yellow + + $content = Get-Content -Path $file.FullName -Raw -Encoding UTF8 + $originalSize = $content.Length + + $transformed = Transform-Code -content $content + + $transformedFiles += @{ + OriginalPath = $file.FullName + OriginalName = $file.Name + Content = $transformed + OriginalSize = $originalSize + NewSize = $transformed.Length + Reduction = [math]::Round((($originalSize - $transformed.Length) / $originalSize) * 100, 2) + } + + Write-Host " Original size: $originalSize bytes" + Write-Host " Transformed size: $($transformed.Length) bytes" + Write-Host "" +} + +# Display summary +Write-Host "" +Write-Host "Transformation Summary" -ForegroundColor Green +Write-Host "=====================" -ForegroundColor Green +Write-Host "Files processed: $($transformedFiles.Count)" +Write-Host "" + +# Output transformed files +if ($OutputPath) { + # Create output directory if specified + if (-not (Test-Path $OutputPath)) { + New-Item -ItemType Directory -Path $OutputPath | Out-Null + } + + Write-Host "Writing transformed files to: $OutputPath" -ForegroundColor Cyan + Write-Host "" + + foreach ($file in $transformedFiles) { + $outPath = Join-Path $OutputPath $file.OriginalName + Set-Content -Path $outPath -Value $file.Content -Encoding UTF8 + Write-Host " Created: $outPath" + } + + Write-Host "" + Write-Host "Transformation complete!" -ForegroundColor Green +} else { + # Display transformed content to console + Write-Host "Transformed Code (review and manually apply changes):" -ForegroundColor Cyan + Write-Host "========================================================" -ForegroundColor Cyan + Write-Host "" + + foreach ($file in $transformedFiles) { + Write-Host "" + Write-Host "// ========================================" -ForegroundColor Magenta + Write-Host "// File: $($file.OriginalName)" -ForegroundColor Magenta + Write-Host "// ========================================" -ForegroundColor Magenta + Write-Host $file.Content + Write-Host "" + } +} + +Write-Host "" +Write-Host "Manual Review Checklist:" -ForegroundColor Yellow +Write-Host "========================" -ForegroundColor Yellow +Write-Host "- [ ] Extract core logic to Setup/Run/Cleanup methods" +Write-Host "- [ ] Replace SampleHelpers.Connect with ServiceClient + appsettings.json" +Write-Host "- [ ] Remove WPF login UI code (ExampleLoginForm)" +Write-Host "- [ ] Add entity tracking to entityStore for cleanup" +Write-Host "- [ ] Update error handling to modern try-catch-finally pattern" +Write-Host "- [ ] Convert early-bound types to late-bound if possible" +Write-Host "- [ ] Test with 'dotnet build' and 'dotnet run'" +Write-Host "" +Write-Host "Use New-ModernSample.ps1 to create the target project structure first." diff --git a/dataverse/orgsvc/CSharp-NETCore/Scripts/New-CategoryFolder.ps1 b/dataverse/orgsvc/CSharp-NETCore/Scripts/New-CategoryFolder.ps1 new file mode 100644 index 00000000..8ce80fd3 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Scripts/New-CategoryFolder.ps1 @@ -0,0 +1,139 @@ +# New-CategoryFolder.ps1 +# Creates a new category folder structure in CSharp-NETCore with standard files + +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [string]$CategoryName, + + [Parameter(Mandatory=$true)] + [string]$Description, + + [Parameter(Mandatory=$false)] + [string]$LearnMoreUrl = "" +) + +$ErrorActionPreference = "Stop" + +# Get the script's directory and navigate to CSharp-NETCore root +$scriptDir = Split-Path -Parent $PSCommandPath +$csharpNETCoreRoot = Split-Path -Parent $scriptDir + +# Create category directory +$categoryPath = Join-Path $csharpNETCoreRoot $CategoryName +if (Test-Path $categoryPath) { + Write-Warning "Category folder '$CategoryName' already exists at $categoryPath" + $response = Read-Host "Do you want to continue? (y/n)" + if ($response -ne 'y') { + Write-Host "Operation cancelled." + exit + } +} else { + Write-Host "Creating category folder: $categoryPath" + New-Item -ItemType Directory -Path $categoryPath | Out-Null +} + +# Create appsettings.json +$appsettingsPath = Join-Path $categoryPath "appsettings.json" +if (-not (Test-Path $appsettingsPath)) { + Write-Host "Creating appsettings.json..." + $appsettingsContent = @" +{ + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } +} +"@ + Set-Content -Path $appsettingsPath -Value $appsettingsContent -Encoding UTF8 +} + +# Create README.md +$readmePath = Join-Path $categoryPath "README.md" +if (-not (Test-Path $readmePath)) { + Write-Host "Creating README.md..." + + $learnMoreSection = "" + if ($LearnMoreUrl) { + $learnMoreSection = "`n`nMore information: [$CategoryName]($LearnMoreUrl)" + } + + $readmeContent = @" +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "$Description" +--- + +# $CategoryName +$Description$learnMoreSection + +## Samples + +This folder contains the following samples: + +|Sample folder|Description|Build target| +|---|---|---| +| | |.NET 6| + +## Prerequisites + +- Microsoft Visual Studio 2022 +- Access to Dataverse with appropriate privileges for the operations demonstrated + +## How to run samples + +1. Clone or download the PowerApps-Samples repository +2. Navigate to ``/dataverse/orgsvc/CSharp-NETCore/$CategoryName/`` +3. Open ``$CategoryName.sln`` in Visual Studio 2022 +4. Edit the ``appsettings.json`` file in the category folder root with your Dataverse environment details: + - Set ``Url`` to your Dataverse environment URL + - Set ``Username`` to your user account +5. Build and run the desired sample project + +## appsettings.json + +Each sample in this category references the shared ``appsettings.json`` file in the category root folder. The connection string format is: + +``````json +{ + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } +} +`````` + +You can also set the ``DATAVERSE_APPSETTINGS`` environment variable to point to a custom appsettings.json file location if you prefer to keep your connection string outside the repository. + +## See also + +[SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/overview) +"@ + Set-Content -Path $readmePath -Value $readmeContent -Encoding UTF8 +} + +# Create solution file +$slnPath = Join-Path $categoryPath "$CategoryName.sln" +if (-not (Test-Path $slnPath)) { + Write-Host "Creating solution file: $CategoryName.sln..." + Push-Location $categoryPath + try { + dotnet new sln -n $CategoryName | Out-Null + Write-Host "Solution file created successfully." + } catch { + Write-Error "Failed to create solution file: $_" + } finally { + Pop-Location + } +} + +Write-Host "" +Write-Host "Category '$CategoryName' created successfully at: $categoryPath" -ForegroundColor Green +Write-Host "" +Write-Host "Next steps:" +Write-Host "1. Update the README.md with specific sample descriptions" +Write-Host "2. Use New-ModernSample.ps1 to add samples to this category" +Write-Host "3. Update appsettings.json with your environment details" diff --git a/dataverse/orgsvc/CSharp-NETCore/Scripts/New-ModernSample.ps1 b/dataverse/orgsvc/CSharp-NETCore/Scripts/New-ModernSample.ps1 new file mode 100644 index 00000000..89d5f1cc --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Scripts/New-ModernSample.ps1 @@ -0,0 +1,300 @@ +# New-ModernSample.ps1 +# Creates a new modern sample project within a category folder + +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [string]$CategoryName, + + [Parameter(Mandatory=$true)] + [string]$SampleName, + + [Parameter(Mandatory=$true)] + [string]$Description, + + [Parameter(Mandatory=$false)] + [string]$LearnMoreUrl = "" +) + +$ErrorActionPreference = "Stop" + +# Get the script's directory and navigate to CSharp-NETCore root +$scriptDir = Split-Path -Parent $PSCommandPath +$csharpNETCoreRoot = Split-Path -Parent $scriptDir + +# Verify category exists +$categoryPath = Join-Path $csharpNETCoreRoot $CategoryName +if (-not (Test-Path $categoryPath)) { + Write-Error "Category folder '$CategoryName' does not exist. Run New-CategoryFolder.ps1 first." + exit 1 +} + +# Create sample directory +$samplePath = Join-Path $categoryPath $SampleName +if (Test-Path $samplePath) { + Write-Error "Sample folder '$SampleName' already exists in category '$CategoryName'" + exit 1 +} + +Write-Host "Creating sample folder: $samplePath" +New-Item -ItemType Directory -Path $samplePath | Out-Null + +# Create .csproj file +$csprojPath = Join-Path $samplePath "$SampleName.csproj" +Write-Host "Creating $SampleName.csproj..." +$csprojContent = @" + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + +"@ +Set-Content -Path $csprojPath -Value $csprojContent -Encoding UTF8 + +# Create Program.cs skeleton +$programPath = Join-Path $samplePath "Program.cs" +Write-Host "Creating Program.cs..." +$programContent = @" +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// $Description + /// + /// + /// TODO: Add detailed description and prerequisites + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + /// + /// Sets up sample data required for the demonstration + /// + private static void Setup(ServiceClient service) + { + Console.WriteLine("Setting up sample data..."); + // TODO: Create any entities or data needed for the Run() method + // Add created entities to entityStore for cleanup + } + + /// + /// Demonstrates the main sample functionality + /// + private static void Run(ServiceClient service) + { + Console.WriteLine("Running sample..."); + // TODO: Add primary demonstration code here + } + + /// + /// Cleans up sample data created during execution + /// + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine(`$"Deleting {entityStore.Count} created records..."); + foreach (var entityRef in entityStore) + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} +"@ +Set-Content -Path $programPath -Value $programContent -Encoding UTF8 + +# Create README.md +$readmePath = Join-Path $samplePath "README.md" +Write-Host "Creating README.md..." + +$seeAlsoSection = "" +if ($LearnMoreUrl) { + $seeAlsoSection = @" + +## See also + +[$Description]($LearnMoreUrl) +"@ +} + +$readmeContent = @" +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "$Description" +--- + +# $SampleName + +$Description + +## What this sample does + +TODO: Describe the functionality demonstrated by this sample. + +## How this sample works + +### Setup + +TODO: Describe any setup operations performed. + +### Run + +TODO: Describe the main operations performed. + +### Cleanup + +TODO: Describe cleanup operations. + +## Demonstrates + +TODO: List the key SDK classes, messages, or patterns demonstrated: +- Key class or message used +- Important pattern demonstrated + +## Sample Output + +TODO: Include example console output + +`````` +Connected to Dataverse. + +Setting up sample data... +Running sample... +[Sample output here] +Cleaning up... + +Press any key to exit. +``````$seeAlsoSection +"@ +Set-Content -Path $readmePath -Value $readmeContent -Encoding UTF8 + +# Add project to solution +$slnPath = Join-Path $categoryPath "$CategoryName.sln" +if (Test-Path $slnPath) { + Write-Host "Adding project to solution..." + Push-Location $categoryPath + try { + dotnet sln add "$SampleName\$SampleName.csproj" | Out-Null + Write-Host "Project added to solution successfully." + } catch { + Write-Warning "Failed to add project to solution: $_" + } finally { + Pop-Location + } +} else { + Write-Warning "Solution file not found. Project not added to solution." +} + +Write-Host "" +Write-Host "Sample '$SampleName' created successfully in category '$CategoryName'" -ForegroundColor Green +Write-Host "Location: $samplePath" -ForegroundColor Green +Write-Host "" +Write-Host "Next steps:" +Write-Host "1. Edit Program.cs to implement the sample logic" +Write-Host "2. Update README.md with detailed documentation" +Write-Host "3. Test with 'dotnet build' and 'dotnet run'" +Write-Host "4. Update the category README.md to include this sample in the table" diff --git a/dataverse/orgsvc/CSharp-NETCore/Scripts/Test-MigratedSamples.ps1 b/dataverse/orgsvc/CSharp-NETCore/Scripts/Test-MigratedSamples.ps1 new file mode 100644 index 00000000..c65c8a4e --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Scripts/Test-MigratedSamples.ps1 @@ -0,0 +1,203 @@ +# Test-MigratedSamples.ps1 +# Validates that all samples in a category build successfully + +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [string]$CategoryName, + + [Parameter(Mandatory=$false)] + [switch]$TestSolution, + + [Parameter(Mandatory=$false)] + [switch]$Verbose +) + +$ErrorActionPreference = "Stop" + +# Get the script's directory and navigate to CSharp-NETCore root +$scriptDir = Split-Path -Parent $PSCommandPath +$csharpNETCoreRoot = Split-Path -Parent $scriptDir + +# Verify category exists +$categoryPath = Join-Path $csharpNETCoreRoot $CategoryName +if (-not (Test-Path $categoryPath)) { + Write-Error "Category folder '$CategoryName' does not exist at $categoryPath" + exit 1 +} + +Write-Host "" +Write-Host "Testing samples in category: $CategoryName" -ForegroundColor Cyan +Write-Host "Category path: $categoryPath" +Write-Host "" + +$results = @() +$totalTests = 0 +$passedTests = 0 +$failedTests = 0 + +# Test solution file if requested +if ($TestSolution) { + $slnPath = Join-Path $categoryPath "$CategoryName.sln" + if (Test-Path $slnPath) { + Write-Host "Testing solution: $CategoryName.sln" -ForegroundColor Yellow + Write-Host "Running: dotnet build `"$slnPath`"" + Write-Host "" + + $totalTests++ + Push-Location $categoryPath + try { + if ($Verbose) { + $buildOutput = dotnet build "$slnPath" 2>&1 + Write-Host $buildOutput + } else { + $buildOutput = dotnet build "$slnPath" 2>&1 | Out-String + } + + if ($LASTEXITCODE -eq 0) { + $passedTests++ + $results += @{ + Name = "$CategoryName.sln" + Type = "Solution" + Status = "PASS" + Path = $slnPath + } + Write-Host "Solution build: PASS" -ForegroundColor Green + } else { + $failedTests++ + $results += @{ + Name = "$CategoryName.sln" + Type = "Solution" + Status = "FAIL" + Path = $slnPath + Error = $buildOutput + } + Write-Host "Solution build: FAIL" -ForegroundColor Red + if (-not $Verbose) { + Write-Host "Error output:" -ForegroundColor Red + Write-Host $buildOutput + } + } + } catch { + $failedTests++ + $results += @{ + Name = "$CategoryName.sln" + Type = "Solution" + Status = "FAIL" + Path = $slnPath + Error = $_.Exception.Message + } + Write-Host "Solution build: FAIL - $($_.Exception.Message)" -ForegroundColor Red + } finally { + Pop-Location + } + Write-Host "" + } else { + Write-Warning "Solution file not found: $CategoryName.sln" + Write-Host "" + } +} + +# Find all .csproj files in sample directories +$projects = Get-ChildItem -Path $categoryPath -Filter "*.csproj" -Recurse | Where-Object { + $_.FullName -notlike "*\bin\*" -and + $_.FullName -notlike "*\obj\*" +} + +if ($projects.Count -eq 0) { + Write-Warning "No project files found in category '$CategoryName'" + exit 0 +} + +Write-Host "Found $($projects.Count) project(s) to test" +Write-Host "" + +# Test each project +foreach ($project in $projects) { + $projectName = $project.BaseName + $projectDir = $project.DirectoryName + + Write-Host "Testing: $projectName" -ForegroundColor Yellow + Write-Host "Path: $($project.FullName)" + Write-Host "Running: dotnet build" + Write-Host "" + + $totalTests++ + Push-Location $projectDir + try { + if ($Verbose) { + $buildOutput = dotnet build 2>&1 + Write-Host $buildOutput + } else { + $buildOutput = dotnet build 2>&1 | Out-String + } + + if ($LASTEXITCODE -eq 0) { + $passedTests++ + $results += @{ + Name = $projectName + Type = "Project" + Status = "PASS" + Path = $project.FullName + } + Write-Host "Build result: PASS" -ForegroundColor Green + } else { + $failedTests++ + $results += @{ + Name = $projectName + Type = "Project" + Status = "FAIL" + Path = $project.FullName + Error = $buildOutput + } + Write-Host "Build result: FAIL" -ForegroundColor Red + if (-not $Verbose) { + Write-Host "Error output:" -ForegroundColor Red + Write-Host $buildOutput + } + } + } catch { + $failedTests++ + $results += @{ + Name = $projectName + Type = "Project" + Status = "FAIL" + Path = $project.FullName + Error = $_.Exception.Message + } + Write-Host "Build result: FAIL - $($_.Exception.Message)" -ForegroundColor Red + } finally { + Pop-Location + } + + Write-Host "" + Write-Host ("=" * 80) + Write-Host "" +} + +# Display summary +Write-Host "" +Write-Host "Test Summary for $CategoryName" -ForegroundColor Cyan +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "Total tests: $totalTests" +Write-Host "Passed: $passedTests" -ForegroundColor Green +Write-Host "Failed: $failedTests" -ForegroundColor $(if ($failedTests -gt 0) { "Red" } else { "Green" }) +Write-Host "" + +if ($passedTests -eq $totalTests) { + Write-Host "All tests passed!" -ForegroundColor Green + $exitCode = 0 +} else { + Write-Host "Some tests failed. Review the output above for details." -ForegroundColor Red + $exitCode = 1 + + Write-Host "" + Write-Host "Failed projects:" -ForegroundColor Red + foreach ($result in $results | Where-Object { $_.Status -eq "FAIL" }) { + Write-Host " - $($result.Name) ($($result.Type))" + } +} + +Write-Host "" + +exit $exitCode diff --git a/dataverse/orgsvc/CSharp-NETCore/Templates/CategoryREADME.md.template b/dataverse/orgsvc/CSharp-NETCore/Templates/CategoryREADME.md.template new file mode 100644 index 00000000..1c6e9ea2 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Templates/CategoryREADME.md.template @@ -0,0 +1,56 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "{{DESCRIPTION}}" +--- + +# {{CATEGORY_NAME}} + +{{DESCRIPTION}} + +{{LEARN_MORE_SECTION}} + +## Samples + +This folder contains the following samples: + +|Sample folder|Description|Build target| +|---|---|---| +| | |.NET 6| + +## Prerequisites + +- Microsoft Visual Studio 2022 +- Access to Dataverse with appropriate privileges for the operations demonstrated + +## How to run samples + +1. Clone or download the PowerApps-Samples repository +2. Navigate to `/dataverse/orgsvc/CSharp-NETCore/{{CATEGORY_NAME}}/` +3. Open `{{CATEGORY_NAME}}.sln` in Visual Studio 2022 +4. Edit the `appsettings.json` file in the category folder root with your Dataverse environment details: + - Set `Url` to your Dataverse environment URL + - Set `Username` to your user account +5. Build and run the desired sample project + +## appsettings.json + +Each sample in this category references the shared `appsettings.json` file in the category root folder. The connection string format is: + +```json +{ + "ConnectionStrings": { + "default": "AuthType=OAuth;Url=https://yourorg.crm.dynamics.com;Username=youruser@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=http://localhost;LoginPrompt=Auto" + } +} +``` + +You can also set the `DATAVERSE_APPSETTINGS` environment variable to point to a custom appsettings.json file location if you prefer to keep your connection string outside the repository. + +## See also + +[SDK for .NET](https://learn.microsoft.com/power-apps/developer/data-platform/org-service/overview) diff --git a/dataverse/orgsvc/CSharp-NETCore/Templates/Program.cs.template b/dataverse/orgsvc/CSharp-NETCore/Templates/Program.cs.template new file mode 100644 index 00000000..b8c9d92c --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Templates/Program.cs.template @@ -0,0 +1,126 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace PowerPlatform.Dataverse.CodeSamples +{ + /// + /// {{DESCRIPTION}} + /// + /// + /// {{DETAILED_DESCRIPTION}} + /// + /// Set the appropriate Url and Username values for your test + /// environment in the appsettings.json file before running this program. + /// + class Program + { + private static readonly List entityStore = new(); + + #region Sample Methods + + /// + /// Sets up sample data required for the demonstration + /// + private static void Setup(ServiceClient service) + { + Console.WriteLine("Setting up sample data..."); + // Create any entities or data needed for the Run() method + // Add created entities to entityStore for cleanup + } + + /// + /// Demonstrates {{SAMPLE_PURPOSE}} + /// + private static void Run(ServiceClient service) + { + Console.WriteLine("Running sample..."); + // Primary demonstration code goes here + } + + /// + /// Cleans up sample data created during execution + /// + private static void Cleanup(ServiceClient service, bool deleteCreatedRecords) + { + Console.WriteLine("Cleaning up..."); + if (deleteCreatedRecords && entityStore.Count > 0) + { + Console.WriteLine($"Deleting {entityStore.Count} created records..."); + foreach (var entityRef in entityStore) + { + service.Delete(entityRef.LogicalName, entityRef.Id); + } + } + } + + #endregion + + #region Application Setup + + /// + /// Contains the application's configuration settings. + /// + IConfiguration Configuration { get; } + + /// + /// Constructor. Loads the application configuration settings from a JSON file. + /// + Program() + { + // Get the path to the appsettings file. If the environment variable is set, + // use that file path. Otherwise, use the runtime folder's settings file. + string? path = Environment.GetEnvironmentVariable("DATAVERSE_APPSETTINGS"); + if (path == null) path = "appsettings.json"; + + // Load the app's configuration settings from the JSON file. + Configuration = new ConfigurationBuilder() + .AddJsonFile(path, optional: false, reloadOnChange: true) + .Build(); + } + + static void Main(string[] args) + { + Program app = new(); + + // Create a Dataverse service client using the default connection string. + ServiceClient serviceClient = + new(app.Configuration.GetConnectionString("default")); + + if (!serviceClient.IsReady) + { + Console.WriteLine("Failed to connect to Dataverse."); + Console.WriteLine("Error: {0}", serviceClient.LastError); + return; + } + + Console.WriteLine("Connected to Dataverse."); + Console.WriteLine(); + + bool deleteCreatedRecords = true; + + try + { + Setup(serviceClient); + Run(serviceClient); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred:"); + Console.WriteLine(ex.Message); + } + finally + { + Cleanup(serviceClient, deleteCreatedRecords); + + Console.WriteLine(); + Console.WriteLine("Press any key to exit."); + Console.ReadKey(); + serviceClient.Dispose(); + } + } + + #endregion + } +} diff --git a/dataverse/orgsvc/CSharp-NETCore/Templates/Sample.csproj.template b/dataverse/orgsvc/CSharp-NETCore/Templates/Sample.csproj.template new file mode 100644 index 00000000..d73af524 --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Templates/Sample.csproj.template @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + PowerPlatform.Dataverse.CodeSamples + enable + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/dataverse/orgsvc/CSharp-NETCore/Templates/SampleREADME.md.template b/dataverse/orgsvc/CSharp-NETCore/Templates/SampleREADME.md.template new file mode 100644 index 00000000..800d71cf --- /dev/null +++ b/dataverse/orgsvc/CSharp-NETCore/Templates/SampleREADME.md.template @@ -0,0 +1,52 @@ +--- +languages: +- csharp +products: +- power-platform +- power-apps +page_type: sample +description: "{{DESCRIPTION}}" +--- + +# {{SAMPLE_NAME}} + +{{DESCRIPTION}} + +## What this sample does + +{{WHAT_IT_DOES}} + +## How this sample works + +### Setup + +{{SETUP_DESCRIPTION}} + +### Run + +{{RUN_DESCRIPTION}} + +### Cleanup + +{{CLEANUP_DESCRIPTION}} + +## Demonstrates + +{{DEMONSTRATES_LIST}} + +## Sample Output + +``` +Connected to Dataverse. + +Setting up sample data... +Running sample... +{{SAMPLE_OUTPUT}} +Cleaning up... + +Press any key to exit. +``` + +## See also + +{{SEE_ALSO_LINKS}}