Google Anlatics
Thursday, August 11, 2016
Saturday, July 30, 2016
New datacenter regions for CRM Online
Datacenter regions
|
Service area
|
Service available
|
Information
|
Canada
|
Canada
|
June 2016
|
|
India
|
India
|
May 2016
|
|
Japan
|
Japan
|
March 2015
|
|
Oceania
|
Australia, New
Zealand, Fiji
|
Q1 CY2015
|
|
Friday, July 22, 2016
Deprecation announcements with Dynamics CRM Online 2016 Update 1 and Microsoft Dynamics CRM 2016 Service Pack 1
Thursday, July 14, 2016
Voice of Customer for Dynamics CRM 2016
Refer below link for more information.
http://www.inogic.com/blog/2016/07/voice-of-customer-for-dynamics-crm-2016/
Thursday, July 7, 2016
Dynamics CRM 2016 Could Mean the End of Survey Monkey
CRM Online - Debugging Your Plug-ins with the Plug-in Trace Log
Developer's Guide to Debugging CRM Online Plugins
A practical approach to plugin troubleshooting
The Challenge with CRM Online Debugging
Let's face it - debugging plugins in CRM Online can be frustrating. Unlike on-premises environments where you have full debugging capabilities, CRM Online is more restrictive. But don't worry! The Plugin Trace Log is your best friend here, and I'll show you how to make the most of it.
Setting Up Plugin Trace Log - The Right Way
First things first - let's turn on logging:
- Head to Settings → Administration → System Settings
- Find the Customization tab
- Look for Plugin and Custom Workflow Activity Tracing
- Choose your logging level:
- All: Captures everything (great for development)
- Exception: Only logs when things go wrong (better for production)
💡 Pro Tip: During development, always set it to "All" - you'll thank yourself later when trying to track down subtle bugs.
Making Your Plugins More Debug-Friendly
Here's a template that I've found super helpful for writing plugins that are easier to debug:
public class MyPlugin : IPlugin { private readonly string _pluginName; public MyPlugin() { _pluginName = GetType().Name; } public void Execute(IServiceProvider serviceProvider) { ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); tracer.Trace($"[{_pluginName}] Starting execution"); tracer.Trace($"[{_pluginName}] Message: {context.MessageName}"); tracer.Trace($"[{_pluginName}] Stage: {context.Stage}"); try { // Log input parameters if (context.InputParameters.Contains("Target")) { Entity target = (Entity)context.InputParameters["Target"]; tracer.Trace($"[{_pluginName}] Target Entity: {target.LogicalName}"); tracer.Trace($"[{_pluginName}] Attributes: {string.Join(", ", target.Attributes.Keys)}"); } // Your plugin logic here DoYourWork(tracer, context); tracer.Trace($"[{_pluginName}] Execution completed successfully"); } catch (Exception ex) { tracer.Trace($"[{_pluginName}] Error: {ex.Message}"); tracer.Trace($"[{_pluginName}] Stack Trace: {ex.StackTrace}"); throw new InvalidPluginExecutionException( $"Error in {_pluginName}: {ex.Message}", ex); } } private void DoYourWork(ITracingService tracer, IPluginExecutionContext context) { // Your actual plugin logic goes here tracer.Trace($"[{_pluginName}] Doing work..."); } }
Debugging Best Practices
1. Strategic Logging
- Add timestamps to important operations:
tracer.Trace($"[{DateTime.Now:HH:mm:ss.fff}] Starting database query");
// Your query here
tracer.Trace($"[{DateTime.Now:HH:mm:ss.fff}] Query completed");
tracer.Trace($"[{DateTime.Now:HH:mm:ss.fff}] Starting database query");
// Your query here
tracer.Trace($"[{DateTime.Now:HH:mm:ss.fff}] Query completed");
2. Context is King
Always log:
- Plugin stage and message
- Entity name and ID
- Modified attributes
- User context
- Business unit context
3. Performance Tracking
var stopwatch = new Stopwatch();
stopwatch.Start();
// Your code here
stopwatch.Stop();
tracer.Trace($"Operation took: {stopwatch.ElapsedMilliseconds}ms");
private void LogEntityDetails(Entity entity, ITracingService tracer) { foreach (var attribute in entity.Attributes) { tracer.Trace($"Attribute: {attribute.Key}, Value: {attribute.Value}"); } }
Finding and Reading Logs
- Go to Advanced Settings → Settings → Plugin Trace Log
- Use filters effectively:
- Filter by date range for recent issues
- Search by plugin name for specific components
- Look for error messages in the message field
Troubleshooting Common Issues
1. Plugin Never Executes
Check:
- Registration step configuration
- Filtering attributes
- Security roles
- Custom workflow activity permissions
2. Performance Problems
Look for:
- Multiple database calls
- Large data retrievals
- Nested loops
- External service calls
3. Timeout Issues
Remember:
- Plugins have a 2-minute timeout
- Async plugins have longer timeouts
- Consider breaking up long-running operations
Pro Tips
- Use Correlation IDs: Add them to your logs to track related operations:
var correlationId = Guid.NewGuid(); tracer.Trace($"[{correlationId}] Starting operation");
- Environment Variables: Use them for configuration to avoid hardcoding:
var configValue = context.OrganizationService.GetEnvironmentVariable("MyConfigKey"); tracer.Trace($"Using config value: {configValue}");
- Log Cleanup: Implement a cleanup strategy:
// Example cleanup query SELECT * FROM PluginTraceLog WHERE CreatedOn < DATEADD(day, -7, GETDATE())
Real-World Example: Troubleshooting a Plugin
Let's say you have a plugin that's supposed to update related records but sometimes fails. Here's how to debug it:
- Add detailed logging:
tracer.Trace($"Found {relatedRecords.Count} related records"); foreach (var record in relatedRecords) { tracer.Trace($"Processing record: {record.Id}"); // Process record tracer.Trace($"Record {record.Id} processed successfully"); }
- Check the logs for:
- Number of related records
- Processing time per record
- Any errors or exceptions
- Data inconsistencies
Remember: The key to successful debugging is having the right information at the right time. Don't be afraid to log extensively during development - you can always scale it back for production.
Debugging with the Plugin Registration Tool
The Plugin Registration Tool (PRT) is a powerful companion for plugin development and debugging. Here's how to make the most of it:
Setting Up the Debug Environment
- Download and Install:
- Get the latest Plugin Registration Tool from the NuGet package: 'Microsoft.CrmSdk.XrmTooling.PluginRegistrationTool'
- Or download it from the Power Platform Tools
- Connect to Your Environment:
Office 365 Authentication: - Choose Online type - Enter your CRM URL - Use your Microsoft 365 credentials
Debug Mode Setup
- Enable Debug Mode for Your Plugin:
// Add this attribute to your plugin class [CrmPluginRegistration(MessageNameEnum.Create, "account", StageEnum.PreOperation, ExecutionModeEnum.Synchronous, "", "", 1, IsolationModeEnum.Sandbox, Id = "YOUR_PLUGIN_ID", Description = "Description", Debug = true)] // This enables debug mode
- Configure Debug Instance:
- Right-click your plugin in PRT
- Select "Update"
- Check "Enable Debugging"
- Set your local debug path
Using Debug Breakpoints
- Set Up Visual Studio:
public void Execute(IServiceProvider serviceProvider) { // Add breakpoint here System.Diagnostics.Debugger.Break(); ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); // Rest of your code }
- Debug Profile Configuration:
<!-- In Visual Studio: Debug Properties --> Enable native code debugging: Yes Enable Just My Code: No
Step-by-Step Debugging Process
- Prepare Your Environment:
a. Open Visual Studio as Administrator b. Load your plugin solution c. Start the Plugin Registration Tool d. Connect to your CRM environment
- Start Debugging Session:
a. In PRT: - Select your plugin - Click "Start Debug" b. In Visual Studio: - Debug → Attach to Process - Select CrmAsyncService process
- Test Different Scenarios:
// Add conditional breakpoints for specific scenarios if (context.MessageName == "create" && context.PrimaryEntityName == "account") { System.Diagnostics.Debugger.Break(); // Test creation scenario }
Advanced PRT Features
- Profile Viewer:
- View execution pipeline - Monitor performance metrics - Track dependency chains
- Image Registration:
// Register pre/post images for debugging [ImageType(ImageTypeEnum.PreImage)] public const string PreImageAlias = "preImg"; // Access in code if (context.PreEntityImages.Contains(PreImageAlias)) { Entity preImage = context.PreEntityImages[PreImageAlias]; tracer.Trace($"Pre-image data: {preImage.Attributes.Count} attributes"); }
- Testing Tool Integration:
a. Create test cases in PRT b. Save test configurations c. Replay test scenarios
Common Debug Scenarios with PRT
- Data Transformation Issues:
// Debug data changes private void DebugEntityChanges(Entity target, Entity preImage, ITracingService tracer) { foreach (var attribute in target.Attributes) { var oldValue = preImage.Contains(attribute.Key) ? preImage[attribute.Key] : "No previous value"; tracer.Trace($"Attribute {attribute.Key}: Old={oldValue}, New={attribute.Value}"); } }
- Plugin Execution Order:
// Track execution sequence tracer.Trace($"Pipeline Stage: {context.Stage}"); tracer.Trace($"Depth: {context.Depth}"); tracer.Trace($"Mode: {context.Mode}");
- Performance Analysis:
// Use PRT's profiler var profiler = serviceProvider.GetService(typeof(IProfiler)) as IProfiler; using (profiler.BeginOperation("CustomOperation")) { // Your code here }
Troubleshooting PRT Issues
- Connection Problems:
- Check authentication settings
- Verify network connectivity
- Ensure proper security roles
- Debug Mode Not Working:
Common fixes: - Run VS as Administrator - Check firewall settings - Verify debug symbols are loaded
- Performance Issues:
Tips: - Limit debug sessions length - Clear debug logs regularly - Use conditional debugging
Best Practices for PRT Debugging
- Organize Your Debug Environment:
Project Structure: /Plugins /Debug - debug.config - test-cases.json
- Version Control Integration:
- Store debug configurations - Track plugin changes - Maintain test scenarios
- Documentation:
Keep records of: - Debug configurations - Common issues and solutions - Environment setup steps
Remember: The Plugin Registration Tool is most effective when used alongside the Plugin Trace Log. Use both tools in combination for comprehensive debugging capabilities.
Writing Debug-Friendly and Maintainable Plugin Code
Base Plugin Class Structure
First, let's create a robust base plugin class that handles common operations:
public abstract class PluginBase : IPlugin { private readonly string _pluginName; protected readonly string[] SecureConfigKeys; protected PluginBase(string[] secureConfigKeys = null) { _pluginName = GetType().Name; SecureConfigKeys = secureConfigKeys ?? Array.Empty<string>(); } public void Execute(IServiceProvider serviceProvider) { var context = InitializeContext(serviceProvider, out var tracer, out var service); try { LogExecutionContext(context, tracer); ValidatePlugin(context); ExecutePluginLogic(context, tracer, service); tracer.Trace($"[{_pluginName}] Execution completed successfully"); } catch (Exception ex) { HandleException(ex, tracer); throw; } } protected abstract void ExecutePluginLogic( IPluginExecutionContext context, ITracingService tracer, IOrganizationService service); private IPluginExecutionContext InitializeContext( IServiceProvider serviceProvider, out ITracingService tracer, out IOrganizationService service) { tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); service = factory.CreateOrganizationService(context.UserId); return context; } private void LogExecutionContext(IPluginExecutionContext context, ITracingService tracer) { tracer.Trace($"[{_pluginName}] Starting execution"); tracer.Trace($"[{_pluginName}] Message: {context.MessageName}"); tracer.Trace($"[{_pluginName}] Stage: {context.Stage}"); tracer.Trace($"[{_pluginName}] Mode: {context.Mode}"); tracer.Trace($"[{_pluginName}] Depth: {context.Depth}"); LogEntityImages(context, tracer); } private void LogEntityImages(IPluginExecutionContext context, ITracingService tracer) { if (context.PreEntityImages.Count > 0) { foreach (var image in context.PreEntityImages) { LogEntityImage("PreImage", image.Value, tracer); } } if (context.PostEntityImages.Count > 0) { foreach (var image in context.PostEntityImages) { LogEntityImage("PostImage", image.Value, tracer); } } } private void LogEntityImage(string imageType, Entity image, ITracingService tracer) { tracer.Trace($"[{_pluginName}] {imageType} - Entity: {image.LogicalName}, Id: {image.Id}"); foreach (var attribute in image.Attributes) { var value = attribute.Value?.ToString() ?? "null"; tracer.Trace($"[{_pluginName}] {imageType} - Attribute: {attribute.Key}, Value: {value}"); } } private void HandleException(Exception ex, ITracingService tracer) { tracer.Trace($"[{_pluginName}] Error: {ex.Message}"); tracer.Trace($"[{_pluginName}] Stack Trace: {ex.StackTrace}"); if (ex.InnerException != null) { tracer.Trace($"[{_pluginName}] Inner Exception: {ex.InnerException.Message}"); tracer.Trace($"[{_pluginName}] Inner Stack Trace: {ex.InnerException.StackTrace}"); } } protected void ValidatePlugin(IPluginExecutionContext context) { if (context == null) throw new InvalidPluginExecutionException("Context is null"); } }
Common Helper Classes
- Entity Extension Methods:
public static class EntityExtensions { public static T GetAttributeValue<T>(this Entity entity, string attributeName) { if (!entity.Contains(attributeName)) return default; return (T)entity[attributeName]; } public static bool HasChanged( this Entity target, Entity preImage, string attributeName) { return target.Contains(attributeName) || (preImage?.Contains(attributeName) ?? false); } public static T GetValueOrDefault<T>( this Entity target, Entity preImage, string attributeName) { if (target.Contains(attributeName)) return target.GetAttributeValue<T>(attributeName); return preImage?.GetAttributeValue<T>(attributeName) ?? default; } }
- Logging Helper:
public class PluginLogger { private readonly ITracingService _tracer; private readonly string _className; private readonly Stopwatch _stopwatch; public PluginLogger(ITracingService tracer, string className) { _tracer = tracer; _className = className; _stopwatch = new Stopwatch(); } public IDisposable TraceMethod(string methodName) { return new MethodTracer(_tracer, _className, methodName); } public void LogPerformance(string operation) { _stopwatch.Stop(); _tracer.Trace($"[{_className}] {operation} took {_stopwatch.ElapsedMilliseconds}ms"); _stopwatch.Reset(); _stopwatch.Start(); } private class MethodTracer : IDisposable { private readonly ITracingService _tracer; private readonly string _className; private readonly string _methodName; private readonly Stopwatch _stopwatch; public MethodTracer(ITracingService tracer, string className, string methodName) { _tracer = tracer; _className = className; _methodName = methodName; _stopwatch = Stopwatch.StartNew(); _tracer.Trace($"[{_className}] Entering {_methodName}"); } public void Dispose() { _stopwatch.Stop(); _tracer.Trace($"[{_className}] Exiting {_methodName} - Duration: {_stopwatch.ElapsedMilliseconds}ms"); } } }
Example Implementation
Here's how to use these base classes in your actual plugin:
public class AccountUpdatePlugin : PluginBase { public AccountUpdatePlugin() : base(new[] { "configKey1", "configKey2" }) { } protected override void ExecutePluginLogic( IPluginExecutionContext context, ITracingService tracer, IOrganizationService service) { var logger = new PluginLogger(tracer, nameof(AccountUpdatePlugin)); using (logger.TraceMethod(nameof(ExecutePluginLogic))) { var target = context.GetTargetEntity<Entity>(); var preImage = context.GetPreImage(); // Example of using extension methods if (target.HasChanged(preImage, "telephone1")) { var phone = target.GetValueOrDefault<string>(preImage, "telephone1"); tracer.Trace($"Phone number changed to: {phone}"); using (logger.TraceMethod("UpdateRelatedContacts")) { UpdateRelatedContacts(service, target.Id, phone); } } } } private void UpdateRelatedContacts( IOrganizationService service, Guid accountId, string phone) { // Implementation here } }
Best Practices for Plugin Development
- Use Strong Typing:
public static class PluginContextExtensions { public static T GetTargetEntity<T>(this IPluginExecutionContext context) where T : Entity { if (!context.InputParameters.Contains("Target")) return null; return (T)context.InputParameters["Target"]; } public static Entity GetPreImage(this IPluginExecutionContext context) { return context.PreEntityImages.Count > 0 ? context.PreEntityImages.Values.First() : null; } }
- Configuration Management:
public class PluginSettings { private readonly IOrganizationService _service; public PluginSettings(IOrganizationService service) { _service = service; } public T GetSetting<T>(string key, T defaultValue = default) { try { var query = new QueryExpression("setting"); // Implementation to retrieve setting return (T)result; // Convert result to type T } catch (Exception) { return defaultValue; } } }
- Error Handling Pattern:
public static class ErrorHandler { public static void ExecuteWithRetry( Action action, int maxRetries = 3, int delayMilliseconds = 1000) { var attempts = 0; while (true) { try { attempts++; action(); break; } catch (Exception ex) { if (attempts >= maxRetries) throw; Thread.Sleep(delayMilliseconds * attempts); } } } }
Clean Code Guidelines
- Method Organization:
public class WellOrganizedPlugin : PluginBase { // Constants at the top private const string EntityName = "account"; // Private fields next private readonly PluginLogger _logger; // Constructor public WellOrganizedPlugin() : base() { // Initialize fields } // Main plugin logic protected override void ExecutePluginLogic(/*parameters*/) { // High-level workflow ValidateRequest(); ProcessData(); UpdateRecords(); } // Private methods grouped by functionality private void ValidateRequest() { } private void ProcessData() { } private void UpdateRecords() { } }
- Code Documentation:
/// <summary> /// Updates account information and related records /// </summary> /// <remarks> /// Registered on: /// - Entity: account /// - Message: update /// - Stage: pre-operation /// </remarks> [CrmPluginRegistration(/*registration details*/)] public class WellDocumentedPlugin : PluginBase { // Implementation }
This structure provides:
- Consistent error handling
- Comprehensive logging
- Performance tracking
- Clean, maintainable code
- Reusable components
- Strong typing
- Easy debugging
Advanced Real-World Implementation: Invoice Processing System
System Overview
Let's build a robust invoice processing system that handles:
- Invoice creation/updates in CRM
- Payment processing through an external payment gateway
- Custom payment entity management
- Web API integration
- Error handling and retry mechanisms
- Audit logging
- Idempotency handling
Domain Models
public class InvoiceModel { public Guid Id { get; set; } public string Number { get; set; } public decimal Amount { get; set; } public DateTime DueDate { get; set; } public InvoiceStatus Status { get; set; } public Guid CustomerId { get; set; } public List<PaymentModel> Payments { get; set; } } public class PaymentModel { public Guid Id { get; set; } public Guid InvoiceId { get; set; } public decimal Amount { get; set; } public DateTime PaymentDate { get; set; } public string TransactionId { get; set; } public PaymentStatus Status { get; set; } } public enum InvoiceStatus { Draft = 1, Pending = 2, PartiallyPaid = 3, Paid = 4, Cancelled = 5, Failed = 6 } public enum PaymentStatus { Pending = 1, Processing = 2, Completed = 3, Failed = 4, Refunded = 5 }
Base Service Classes
public abstract class ServiceBase { protected readonly IOrganizationService OrgService; protected readonly ITracingService TracingService; protected readonly PluginLogger Logger; protected readonly IPluginExecutionContext Context; protected ServiceBase(PluginServiceProvider serviceProvider) { OrgService = serviceProvider.OrganizationService; TracingService = serviceProvider.TracingService; Context = serviceProvider.PluginContext; Logger = new PluginLogger(TracingService, GetType().Name); } protected Entity RetrieveWithRetry(string entityName, Guid id, ColumnSet columns) { return RetryPolicy.Execute(() => OrgService.Retrieve(entityName, id, columns)); } protected void UpdateWithRetry(Entity entity) { RetryPolicy.Execute(() => OrgService.Update(entity)); } } public class PluginServiceProvider { public IOrganizationService OrganizationService { get; } public ITracingService TracingService { get; } public IPluginExecutionContext PluginContext { get; } public PluginServiceProvider(IServiceProvider serviceProvider) { TracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); PluginContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); OrganizationService = factory.CreateOrganizationService(PluginContext.UserId); } }
Retry Policy Implementation
public static class RetryPolicy { private static readonly int[] RetryDelays = { 1, 2, 3 }; // Seconds public static T Execute<T>(Func<T> action) { var exceptions = new List<Exception>(); for (int i = 0; i <= RetryDelays.Length; i++) { try { return action(); } catch (Exception ex) when (IsTransient(ex)) { exceptions.Add(ex); if (i < RetryDelays.Length) { Thread.Sleep(RetryDelays[i] * 1000); } } } throw new AggregateException("All retry attempts failed", exceptions); } private static bool IsTransient(Exception ex) { return ex is TimeoutException || ex is WebException || ex.Message.Contains("deadlock") || ex.Message.Contains("timeout"); } }
Invoice Processing Plugin
public class InvoiceProcessingPlugin : PluginBase { private readonly IPaymentGateway _paymentGateway; private readonly IInvoiceService _invoiceService; private readonly IPaymentService _paymentService; public InvoiceProcessingPlugin() : base() { // In real world, use DI container _paymentGateway = new StripePaymentGateway(); _invoiceService = new InvoiceService(); _paymentService = new PaymentService(); } protected override void ExecutePluginLogic( IPluginExecutionContext context, ITracingService tracer, IOrganizationService service) { var serviceProvider = new PluginServiceProvider(context); var logger = new PluginLogger(tracer, nameof(InvoiceProcessingPlugin)); using (logger.TraceMethod(nameof(ExecutePluginLogic))) { var target = context.GetTargetEntity<Entity>(); var preImage = context.GetPreImage(); // Handle different operations based on message and stage switch (context.MessageName.ToLower()) { case "create": HandleInvoiceCreate(target, serviceProvider); break; case "update": if (IsPaymentStatusChange(target, preImage)) { HandlePaymentStatusChange(target, preImage, serviceProvider); } break; } } } private void HandleInvoiceCreate(Entity target, PluginServiceProvider serviceProvider) { var invoiceService = new InvoiceService(serviceProvider); try { // Validate invoice invoiceService.ValidateInvoice(target); // Generate invoice number var invoiceNumber = invoiceService.GenerateInvoiceNumber(); target["new_invoicenumber"] = invoiceNumber; // Set initial status target["statuscode"] = new OptionSetValue((int)InvoiceStatus.Pending); // Create payment records if needed if (target.Contains("new_requiresprepayment") && target.GetAttributeValue<bool>("new_requiresprepayment")) { CreatePrepaymentRecord(target, serviceProvider); } } catch (Exception ex) { HandleInvoiceError(target, ex, serviceProvider); throw; } } private void HandlePaymentStatusChange( Entity target, Entity preImage, PluginServiceProvider serviceProvider) { var paymentService = new PaymentService(serviceProvider); var invoiceService = new InvoiceService(serviceProvider); try { var oldStatus = preImage.GetAttributeValue<OptionSetValue>("statuscode")?.Value; var newStatus = target.GetAttributeValue<OptionSetValue>("statuscode")?.Value; if (newStatus == (int)PaymentStatus.Completed && oldStatus != (int)PaymentStatus.Completed) { // Update invoice balance var invoiceId = target.GetAttributeValue<EntityReference>("new_invoiceid").Id; await UpdateInvoiceBalance(invoiceId, target, serviceProvider); } } catch (Exception ex) { HandlePaymentError(target, ex, serviceProvider); throw; } } }
Payment Processing Service
public interface IPaymentGateway { Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request); Task<PaymentResult> RefundPaymentAsync(RefundRequest request); Task<PaymentStatus> GetPaymentStatusAsync(string transactionId); } public class PaymentService : ServiceBase { private readonly IPaymentGateway _paymentGateway; public PaymentService(PluginServiceProvider serviceProvider) : base(serviceProvider) { _paymentGateway = new StripePaymentGateway(); // Use DI in real world } public async Task ProcessPayment(Entity payment) { using (Logger.TraceMethod(nameof(ProcessPayment))) { try { var request = CreatePaymentRequest(payment); var result = await _paymentGateway.ProcessPaymentAsync(request); UpdatePaymentWithResult(payment, result); } catch (Exception ex) { Logger.LogError($"Payment processing failed: {ex.Message}"); HandlePaymentError(payment, ex); throw; } } } private PaymentRequest CreatePaymentRequest(Entity payment) { // Map CRM payment entity to payment gateway request return new PaymentRequest { Amount = payment.GetAttributeValue<Money>("new_amount").Value, Currency = "USD", CustomerId = GetCustomerId(payment), PaymentMethodId = payment.GetAttributeValue<string>("new_paymentmethodid"), Metadata = new Dictionary<string, string> { ["invoiceId"] = payment.GetAttributeValue<EntityReference>("new_invoiceid").Id.ToString(), ["crmPaymentId"] = payment.Id.ToString() } }; } private void UpdatePaymentWithResult(Entity payment, PaymentResult result) { payment["new_transactionid"] = result.TransactionId; payment["new_status"] = new OptionSetValue((int)result.Status); payment["new_lastprocesseddate"] = DateTime.UtcNow; if (!string.IsNullOrEmpty(result.ErrorMessage)) { payment["new_errormessage"] = result.ErrorMessage; } UpdateWithRetry(payment); } }
Web API Integration
[RoutePrefix("api/v1/invoices")] public class InvoiceController : ApiController { private readonly IOrganizationService _orgService; private readonly IPaymentGateway _paymentGateway; public InvoiceController() { // Initialize CRM connection and services _orgService = CrmServiceFactory.CreateOrganizationService(); _paymentGateway = new StripePaymentGateway(); } [HttpPost] [Route("{invoiceId}/payments")] public async Task<IHttpActionResult> ProcessPayment(Guid invoiceId, [FromBody] PaymentRequest request) { try { // Validate idempotency var idempotencyKey = Request.Headers.GetValues("X-Idempotency-Key").FirstOrDefault(); if (string.IsNullOrEmpty(idempotencyKey)) { return BadRequest("Idempotency key is required"); } // Check for existing payment if (await CheckExistingPayment(idempotencyKey)) { return Ok("Payment already processed"); } // Process payment var result = await ProcessPaymentTransaction(invoiceId, request, idempotencyKey); return Ok(result); } catch (Exception ex) { // Log error return InternalServerError(ex); } } private async Task<PaymentResult> ProcessPaymentTransaction( Guid invoiceId, PaymentRequest request, string idempotencyKey) { // Start distributed transaction using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { try { // Process payment with gateway var paymentResult = await _paymentGateway.ProcessPaymentAsync(request); // Create payment record in CRM var paymentId = CreatePaymentRecord(invoiceId, request, paymentResult); // Update invoice status UpdateInvoiceStatus(invoiceId, paymentResult.Status); // Commit transaction scope.Complete(); return paymentResult; } catch { // Transaction will automatically rollback throw; } } } }
Debugging Tips for Complex Scenarios
- Transaction Debugging:
public class TransactionDebugger { private readonly ITracingService _tracer; private readonly Dictionary<string, Stopwatch> _timers; public TransactionDebugger(ITracingService tracer) { _tracer = tracer; _timers = new Dictionary<string, Stopwatch>(); } public void StartOperation(string operationName) { var timer = new Stopwatch(); timer.Start(); _timers[operationName] = timer; _tracer.Trace($"Starting operation: {operationName}"); } public void EndOperation(string operationName) { if (_timers.TryGetValue(operationName, out var timer)) { timer.Stop(); _tracer.Trace($"Operation {operationName} completed in {timer.ElapsedMilliseconds}ms"); _timers.Remove(operationName); } } }
- State Tracking:
public class StateTracker { private readonly ITracingService _tracer; private readonly Dictionary<string, object> _states; public StateTracker(ITracingService tracer) { _tracer = tracer; _states = new Dictionary<string, object>(); } public void TrackState(string key, object value) { _states[key] = value; _tracer.Trace($"State change: {key} = {value}"); } public void DumpState() { _tracer.Trace("Current state:"); foreach (var kvp in _states) { _tracer.Trace($"{kvp.Key}: {kvp.Value}"); } } }
Testing Strategy
- Integration Test Setup:
public class InvoiceProcessingTests { private readonly IOrganizationService _orgService; private readonly InvoiceProcessingPlugin _plugin; private readonly Mock<IPaymentGateway> _paymentGatewayMock; [TestInitialize] public void Initialize() { // Setup test environment _orgService = TestHelpers.CreateOrganizationService(); _paymentGatewayMock = new Mock<IPaymentGateway>(); _plugin = new InvoiceProcessingPlugin(_paymentGatewayMock.Object); } [TestMethod] public async Task ProcessPayment_Success_UpdatesInvoiceAndPayment() { // Arrange var invoice = CreateTestInvoice(); var payment = CreateTestPayment(invoice.Id); _paymentGatewayMock .Setup(x => x.ProcessPaymentAsync(It.IsAny<PaymentRequest>())) .ReturnsAsync(new PaymentResult { Status = PaymentStatus.Completed }); // Act await _plugin.Execute(CreatePluginContext(payment)); // Assert var updatedInvoice = RetrieveInvoice(invoice.Id); Assert.AreEqual((int)
Comprehensive CRM Testing Guide
Setting Up Test Environment
First, let's set up our test project with necessary NuGet packages:
<!-- packages.config --> <packages> <package id="FakeXrmEasy" version="3.3.0" /> <package id="Microsoft.CrmSdk.CoreAssemblies" version="9.0.2.45" /> <package id="MSTest.TestFramework" version="2.2.8" /> <package id="Moq" version="4.16.1" /> </packages>
Base Test Class
public abstract class CrmTestBase { protected readonly XrmFakedContext Context; protected readonly IOrganizationService Service; protected readonly ITracingService TracingService; protected CrmTestBase() { Context = new XrmFakedContext(); Service = Context.GetOrganizationService(); TracingService = Context.GetFakeTracingService(); // Initialize default data InitializeTestData(); } protected virtual void InitializeTestData() { // Override in derived classes to add specific test data } protected IPluginExecutionContext CreatePluginContext( string messageName, string entityName, Entity target = null, Entity preImage = null, Entity postImage = null, int stage = 40, // Post-operation ParameterCollection inputParameters = null, ParameterCollection outputParameters = null, string preImageName = "PreImage", string postImageName = "PostImage") { var pluginContext = Context.GetDefaultPluginContext(); pluginContext.MessageName = messageName; pluginContext.PrimaryEntityName = entityName; pluginContext.Stage = stage; if (inputParameters != null) pluginContext.InputParameters = inputParameters; else pluginContext.InputParameters = new ParameterCollection(); if (target != null) pluginContext.InputParameters.Add("Target", target); if (outputParameters != null) pluginContext.OutputParameters = outputParameters; if (preImage != null) pluginContext.PreEntityImages.Add(preImageName, preImage); if (postImage != null) pluginContext.PostEntityImages.Add(postImageName, postImage); return pluginContext; } protected void AssertEntityAttributeEquals<T>( Entity entity, string attributeName, T expectedValue) { Assert.IsTrue(entity.Contains(attributeName), $"Entity does not contain attribute: {attributeName}"); Assert.AreEqual(expectedValue, entity[attributeName]); } }
Invoice Plugin Tests
[TestClass] public class InvoiceProcessingPluginTests : CrmTestBase { private InvoiceProcessingPlugin _plugin; private Mock<IPaymentGateway> _paymentGatewayMock; private readonly Guid _customerId = Guid.NewGuid(); private readonly Guid _invoiceId = Guid.NewGuid(); [TestInitialize] public void Initialize() { _paymentGatewayMock = new Mock<IPaymentGateway>(); _plugin = new InvoiceProcessingPlugin(_paymentGatewayMock.Object); // Initialize test data var customer = new Entity("account") { Id = _customerId, ["name"] = "Test Customer" }; Context.Initialize(new[] { customer }); } [TestMethod] public async Task CreateInvoice_WithValidData_CreatesInvoiceAndPayment() { // Arrange var invoice = new Entity("new_invoice") { Id = _invoiceId, ["new_amount"] = new Money(100m), ["new_customerid"] = new EntityReference("account", _customerId), ["new_requiresprepayment"] = true }; var context = CreatePluginContext( "Create", "new_invoice", target: invoice, stage: 20 // Pre-operation ); // Act await _plugin.Execute(context); // Assert var createdInvoice = Service.Retrieve( "new_invoice", _invoiceId, new ColumnSet(true)); Assert.IsNotNull(createdInvoice); Assert.IsNotNull(createdInvoice["new_invoicenumber"]); Assert.AreEqual( (int)InvoiceStatus.Pending, ((OptionSetValue)createdInvoice["statuscode"]).Value); // Verify payment record was created var payments = GetRelatedPayments(_invoiceId); Assert.AreEqual(1, payments.Count); Assert.AreEqual(100m, ((Money)payments[0]["new_amount"]).Value); } [TestMethod] public async Task ProcessPayment_Success_UpdatesInvoiceStatus() { // Arrange var invoice = CreateTestInvoice(); var payment = CreateTestPayment(invoice.Id); _paymentGatewayMock .Setup(x => x.ProcessPaymentAsync(It.IsAny<PaymentRequest>())) .ReturnsAsync(new PaymentResult { Status = PaymentStatus.Completed, TransactionId = "tx_123" }); var context = CreatePluginContext( "Update", "new_payment", target: payment, preImage: payment.Clone().ToEntity<Entity>(), stage: 40 // Post-operation ); // Act await _plugin.Execute(context); // Assert var updatedInvoice = Service.Retrieve( "new_invoice", invoice.Id, new ColumnSet(true)); Assert.AreEqual( (int)InvoiceStatus.Paid, ((OptionSetValue)updatedInvoice["statuscode"]).Value); } [TestMethod] [ExpectedException(typeof(InvalidPluginExecutionException))] public async Task CreateInvoice_InvalidAmount_ThrowsException() { // Arrange var invoice = new Entity("new_invoice") { Id = _invoiceId, ["new_amount"] = new Money(-100m), // Invalid amount ["new_customerid"] = new EntityReference("account", _customerId) }; var context = CreatePluginContext( "Create", "new_invoice", target: invoice, stage: 20 ); // Act await _plugin.Execute(context); } private Entity CreateTestInvoice() { var invoice = new Entity("new_invoice") { Id = _invoiceId, ["new_amount"] = new Money(100m), ["new_customerid"] = new EntityReference("account", _customerId), ["statuscode"] = new OptionSetValue((int)InvoiceStatus.Pending) }; Context.Initialize(new[] { invoice }); return invoice; } private Entity CreateTestPayment(Guid invoiceId) { var paymentId = Guid.NewGuid(); var payment = new Entity("new_payment") { Id = paymentId, ["new_amount"] = new Money(100m), ["new_invoiceid"] = new EntityReference("new_invoice", invoiceId), ["statuscode"] = new OptionSetValue((int)PaymentStatus.Pending) }; Context.Initialize(new[] { payment }); return payment; } private List<Entity> GetRelatedPayments(Guid invoiceId) { var query = new QueryExpression("new_payment") { ColumnSet = new ColumnSet(true), Criteria = new FilterExpression { Conditions = { new ConditionExpression( "new_invoiceid", ConditionOperator.Equal, invoiceId) } } }; return Service.RetrieveMultiple(query).Entities.ToList(); } }
Testing Complex Business Logic
[TestClass] public class InvoiceBusinessLogicTests : CrmTestBase { private InvoiceService _invoiceService; [TestInitialize] public void Initialize() { _invoiceService = new InvoiceService(Service, TracingService); } [TestMethod] public void CalculateInvoiceBalance_WithMultiplePayments_ReturnsCorrectBalance() { // Arrange var invoice = new Entity("new_invoice") { Id = Guid.NewGuid(), ["new_amount"] = new Money(1000m) }; var payments = new[] { CreatePayment(invoice.Id, 300m, PaymentStatus.Completed), CreatePayment(invoice.Id, 200m, PaymentStatus.Completed), CreatePayment(invoice.Id, 100m, PaymentStatus.Failed), CreatePayment(invoice.Id, 400m, PaymentStatus.Pending) }; Context.Initialize(new[] { invoice }.Concat(payments)); // Act var balance = _invoiceService.CalculateRemainingBalance(invoice.Id); // Assert Assert.AreEqual(500m, balance); // Only completed payments should be considered } [TestMethod] public void UpdateInvoiceStatus_BasedOnPayments_SetsCorrectStatus() { // Arrange var invoice = new Entity("new_invoice") { Id = Guid.NewGuid(), ["new_amount"] = new Money(1000m), ["statuscode"] = new OptionSetValue((int)InvoiceStatus.Pending) }; var payments = new[] { CreatePayment(invoice.Id, 300m, PaymentStatus.Completed), CreatePayment(invoice.Id, 700m, PaymentStatus.Completed) }; Context.Initialize(new[] { invoice }.Concat(payments)); // Act _invoiceService.UpdateInvoiceStatus(invoice.Id); // Assert var updatedInvoice = Service.Retrieve( "new_invoice", invoice.Id, new ColumnSet(true)); Assert.AreEqual( (int)InvoiceStatus.Paid, ((OptionSetValue)updatedInvoice["statuscode"]).Value); } private Entity CreatePayment( Guid invoiceId, decimal amount, PaymentStatus status) { return new Entity("new_payment") { Id = Guid.NewGuid(), ["new_invoiceid"] = new EntityReference("new_invoice", invoiceId), ["new_amount"] = new Money(amount), ["statuscode"] = new OptionSetValue((int)status) }; } }
Testing Async Plugins
[TestClass] public class AsyncPluginTests : CrmTestBase { [TestMethod] public async Task AsyncPlugin_WithLongRunningOperation_CompletesSuccessfully() { // Arrange var plugin = new AsyncInvoiceProcessingPlugin(); var invoice = CreateTestInvoice(); // Simulate async execution context var context = CreatePluginContext( "Update", "new_invoice", target: invoice, stage: 40, messageName: "Update" ); // Add async specific properties var asyncContext = context as IExecutionContext; asyncContext.IsExecutingOffline = true; asyncContext.IsInTransaction = false; // Act await plugin.Execute(context); // Assert var updatedInvoice = Service.Retrieve( "new_invoice", invoice.Id, new ColumnSet(true)); Assert.IsNotNull(updatedInvoice["new_asyncprocessingresult"]); } }
Testing Error Handling
[TestClass] public class ErrorHandlingTests : CrmTestBase { [TestMethod] public void RetryPolicy_HandlesTransientErrors() { // Arrange var attempts = 0; Func<string> operation = () => { attempts++; if (attempts < 3) throw new TimeoutException("Simulated timeout"); return "Success"; }; // Act var result = RetryPolicy.Execute(operation); // Assert Assert.AreEqual("Success", result); Assert.AreEqual(3, attempts); } [TestMethod] public void Plugin_HandlesAndLogsErrors() { // Arrange var plugin = new InvoiceProcessingPlugin(); var invoice = CreateTestInvoice(); _paymentGatewayMock .Setup(x => x.ProcessPaymentAsync(It.IsAny<PaymentRequest>())) .ThrowsAsync(new Exception("Payment gateway error")); var context = CreatePluginContext( "Update", "new_invoice", target: invoice ); // Act & Assert var exception = Assert.ThrowsException<InvalidPluginExecutionException>( () => plugin.Execute(context)); // Verify error was logged var errorLog = Service.RetrieveMultiple(new QueryExpression("new_errorlog")) .Entities .FirstOrDefault(); Assert.IsNotNull(errorLog); Assert.AreEqual("Payment gateway error", errorLog["new_errormessage"]); } }
Testing Best Practices
- Isolation:
[TestClass] public class TestIsolationExample : CrmTestBase { [TestInitialize] public void TestInitialize() { // Reset the CRM context for each test Context.DeleteAll(); } [TestCleanup] public void TestCleanup() { // Clean up any test data Context.DeleteAll(); } }
- Mocking External Services:
public class ExternalServiceTests : CrmTestBase { private Mock<IPaymentGateway> _paymentGateway; private Mock<IEmailService> _emailService; [TestInitialize] public void Initialize() { _paymentGateway = new Mock<IPaymentGateway>(); _emailService = new Mock<IEmailService>(); // Configure default behaviors _paymentGateway .Setup(x => x.ProcessPaymentAsync(It.IsAny<PaymentRequest>())) .ReturnsAsync(new PaymentResult { Status = PaymentStatus.Completed }); _emailService .Setup(x => x.SendEmailAsync(It.IsAny<EmailMessage>())) .ReturnsAsync(true); } }