Google Anlatics

Saturday, December 28, 2024

MS CRM Plugin Development: Real Talk from a Senior Dev

Hey there, fellow CRM developers! 

What The Heck Is A Plugin (And Why Should You Care)?

Think of plugins as your secret weapon in the CRM world. They're essentially .NET libraries that let you inject your own code into CRM's event pipeline. While Microsoft's marketing team might call them "extensibility points," I like to think of them as your chance to say "Hey CRM, let me handle this my way!"

The Real-World Use Cases 💡

Let me share some actual scenarios where plugins saved my bacon:

  • A client needed to validate complex pricing rules before saving opportunities (something Power Automate would choke on)
  • We had to sync data with an ancient ERP system that only spoke SOAP
  • Had to implement real-time fraud detection on lead creation
  • Needed to enforce business rules that would make a flowchart cry

Plugin Architecture: The Stuff That Matters 🏗️

Here's what you actually need to know:

public class YourAwesomePlugin : IPlugin { public void Execute(IServiceProvider serviceProvider) { // This is where the magic happens var context = (IPluginExecutionContext)serviceProvider .GetService(typeof(IPluginExecutionContext)); // Pro tip: Always log what you're doing var tracer = (ITracingService)serviceProvider .GetService(typeof(ITracingService)); tracer.Trace("Starting the awesome stuff..."); // Your business logic goes here } }

When & Where to Use Plugins 🎯

  • Pre-validation: When you need to stop bad data before it ruins your day
  • Pre-operation: For modifying data before it hits the database
  • Post-operation: When you need to trigger additional actions after the main operation
  • Async: For anything that might take longer than a coffee break

Quick Tip: Event Pipeline Explained Without the Jargon

Think of it like a security checkpoint:

  1. Pre-validation: The metal detector
  2. Pre-operation: The passport check
  3. Post-operation: The duty-free shopping
  4. Async: The stuff that happens after you're already on the plane

🎯 Why Should You Care About Plugins?

Let's be real - while Power Automate is great for simple stuff, plugins are where the real magic happens. They're like your Swiss Army knife for all things CRM customization. I've used them for everything from complex transaction validations to integrating with legacy systems that make COBOL look modern.

🔍 Plugin Basics (The Stuff Everyone Pretends They Already Know)

First, let's get our heads straight about what a plugin actually is - it's a .NET class library that hooks into CRM's event pipeline. Think of it as your bouncer at the club, deciding what data gets in and what happens to it.

Here's something they don't tell you in the docs: choosing between synchronous and asynchronous execution isn't just about speed - it's about not getting angry calls from users when their screen freezes. Trust me, I learned this the hard way.

💡 Real-World Example: The Lead Assignment Nightmare

Let me share a war story. We had a client who needed leads automatically assigned based on complex criteria (region, product interest, lead score, and current sales rep workload). Here's the battle-tested solution:


public class SmartLeadAssignment : IPlugin { public void Execute(IServiceProvider serviceProvider) { // Get the context and service objects var context = (IPluginExecutionContext)serviceProvider .GetService(typeof(IPluginExecutionContext)); var service = ((IOrganizationServiceFactory)serviceProvider .GetService(typeof(IOrganizationServiceFactory))) .CreateOrganizationService(context.UserId); var tracingService = (ITracingService)serviceProvider .GetService(typeof(ITracingService)); try { if (context.MessageName != "Create" || context.PrimaryEntityName != "lead") return; var lead = (Entity)context.InputParameters["Target"]; // Pro tip: Always check if the field exists! if (!lead.Contains("new_region") || !lead.Contains("new_productinterest")) { tracingService.Trace("Required fields missing"); return; } var assignee = GetOptimalSalesRep(service, lead); if (assignee != Guid.Empty) { lead["ownerid"] = new EntityReference("systemuser", assignee); } } catch (Exception ex) { // Always log the full exception - future you will thank present you tracingService.Trace($"Error: {ex.ToString()}"); throw new InvalidPluginExecutionException( "Error assigning lead. Check trace logs for details.", ex); } } }

🎓 Pro Tips That Saved My Bacon

  1. Always Use Early Returns

    if (!context.InputParameters.Contains("Target")) return;
    This prevents your plugin from running unnecessarily and saves precious CPU cycles.
  2. Transaction Context Is Your Friend

    if (!context.IsInTransaction) throw new InvalidPluginExecutionException();
    This has saved me from duplicate processing more times than I can count.
  3. Trace Everything (But Smart)

    tracingService.Trace($"Processing {lead.Id} for region {region}");
    Future you will buy present you a beer for this.

🚀 Deployment Like a Pro

Forget manually uploading DLLs (it's 2024, folks!). Here's my Azure DevOps pipeline that's saved hours of my life:

🔍 Debugging Like a Detective

When things go wrong (and they will), here's your survival kit:

  1. Plugin Profiler is your best friend
  2. Set up local debugging with the Plugin Registration Tool
  3. Use conditional breakpoints for specific scenarios

⚠️ Common Pitfalls (Learn From My Pain)

  • Never, ever make HTTP calls in sync plugins
  • Always check for null before accessing entity attributes
  • Don't trust the cache - always verify your data
  • Remember: plugins have a 2-minute timeout (yes, even async ones)

🎯 Advanced Techniques

For the real ninjas out there:

  • Use IOrganizationService sparingly (it's expensive)
  • Implement caching for frequently accessed data
  • Consider using Early-Bound entities for better performance
  • Use QueryExpression instead of FetchXML for complex queries

Want to see how I handle multi-threading in async plugins? Drop a comment below, and I'll share my battle-tested patterns!

🐛 Live Debugging Like a Pro

Here's my tried-and-true debugging workflow:

  1. Remote Debugging Setup

// Add this to your plugin constructor if (!Debugger.IsAttached && Debugger.Launch()) { DebuggerBreak(); }
  1. Structured Logging Pattern

public void Execute(IServiceProvider serviceProvider) { var tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); var context = new PluginTraceContext(tracer); context.Log($"Starting plugin execution for {context.MessageName}"); try { // Your logic here context.LogObject("Input parameters", context.InputParameters); } catch (Exception ex) { context.LogException(ex); throw; } }
🚨 Real Production Issues I've Faced (And How to Fix Them)
  1. The Infinite Loop Nightmare

    // Check if the plugin triggered itself if (context.Depth > 1) return;
  2. Memory Leaks in Async Plugins

    using var serviceFactory = (IOrganizationServiceFactory)serviceProvider .GetService(typeof(IOrganizationServiceFactory)); using var service = serviceFactory.CreateOrganizationService(null);
  3. Deadlocks in Transaction Processing

    // Always use optimistic concurrency entity.RowVersion = currentRowVersion;
📊 Production Monitoring That Actually Works

// Custom telemetry wrapper public class PluginTelemetry : IDisposable { private readonly Stopwatch _timer; private readonly ITracingService _tracer; public PluginTelemetry(ITracingService tracer) { _tracer = tracer; _timer = Stopwatch.StartNew(); } public void Dispose() { _timer.Stop(); _tracer.Trace($"Execution time: {_timer.ElapsedMilliseconds}ms"); } }
🌐 Extending with Web APIs (The Smart Way)

When plugins just won't cut it:


[RoutePrefix("api/v1/crm")] public class CrmExtensionController : ApiController { [HttpPost] [Route("bulkupdate")] public async Task<IHttpActionResult> BulkUpdateRecords([FromBody] BulkUpdateRequest request) { using var client = new CrmServiceClient(ConfigurationManager .ConnectionStrings["CRM"].ConnectionString); // Process in batches of 1000 foreach (var batch in request.Records.Chunk(1000)) { await ProcessBatchAsync(client, batch); } return Ok(); } }
💡 The Async Plugin Survival Guide

When to use async:

  • External API calls
  • Batch processing
  • Email notifications
  • File operations

Pro tip: Always implement retry logic:


private async Task ExecuteWithRetryAsync(Func<Task> operation) { var policy = Policy .Handle<Exception>() .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); await policy.ExecuteAsync(operation); }


Production-Ready Base Plugin Class

Here's my battle-tested base class that's saved countless hours:


public abstract class PluginBase : IPlugin { private readonly string _unsecureConfig; private readonly string _secureConfig; protected PluginBase(string unsecureConfig = null, string secureConfig = null) { _unsecureConfig = unsecureConfig; _secureConfig = secureConfig; } public void Execute(IServiceProvider serviceProvider) { var context = (IPluginExecutionContext)serviceProvider .GetService(typeof(IPluginExecutionContext)); var factory = (IOrganizationServiceFactory)serviceProvider .GetService(typeof(IOrganizationServiceFactory)); var tracer = (ITracingService)serviceProvider .GetService(typeof(ITracingService)); var service = factory.CreateOrganizationService(context.UserId); try { // Performance tracking using var performance = new PerformanceTracker(tracer); ExecutePluginLogic(new PluginContext { Context = context, Service = service, TracingService = tracer, UnsecureConfig = _unsecureConfig, SecureConfig = _secureConfig }); } catch (Exception ex) { tracer.Trace($"❌ Error: {ex}"); throw new InvalidPluginExecutionException( $"Plugin failed: {GetType().Name}", ex); } } protected abstract void ExecutePluginLogic(PluginContext context); } public class PluginContext { public IPluginExecutionContext Context { get; set; } public IOrganizationService Service { get; set; } public ITracingService TracingService { get; set; } public string UnsecureConfig { get; set; } public string SecureConfig { get; set; } }

🔐 Securing Your Plugin Configuration

Never hardcode sensitive stuff! Here's how to handle configs properly:


public class LeadScoringPlugin : PluginBase { public LeadScoringPlugin(string unsecureConfig, string secureConfig) : base(unsecureConfig, secureConfig) { } protected override void ExecutePluginLogic(PluginContext context) { var config = JsonConvert.DeserializeObject<ScoringConfig>( context.SecureConfig); context.TracingService.Trace($"Using API key: ***{ config.ApiKey.Substring(config.ApiKey.Length - 4)}"); } }

🚀 Spkl Plugin Registration - The Right Way

  1. First, your spkl.json:

{ "plugins": [ { "solution": "YourSolution", "assemblypath": "bin\\Debug\\YourPlugin.dll", "classRegex": ".*Plugin$", "excludePluginSteps": true } ] }
  1. deployment-config.json for different environments:

{ "dev": { "server": "https://dev.crm.dynamics.com", "solution": "YourSolution_1_0_0_1", "secureConfiguration": { "LeadScoringPlugin": { "ApiKey": "dev_api_key_here" } } }, "prod": { // Production config } }

🎯 Pro Tips for Spkl

  1. Version Control:

# Never commit secrets! deployment-config.json # Keep template deployment-config.template.json
  1. Automated Registration:

# Register plugins spkl plugins [path] # Update specific plugin spkl plugin [path] -name YourPlugin
  1. Debug Settings:

<!-- .spkl.json --> <setting name="debug" value="true" />

🔍 Real-World Example: Putting It All Together


public class CustomerValidationPlugin : PluginBase { private readonly ValidationSettings _settings; public CustomerValidationPlugin(string unsecureConfig, string secureConfig) : base(unsecureConfig, secureConfig) { _settings = JsonConvert.DeserializeObject<ValidationSettings>( unsecureConfig); } protected override void ExecutePluginLogic(PluginContext pluginContext) { var entity = (Entity)pluginContext.Context.InputParameters["Target"]; // Use config settings if (_settings.EnableStrictValidation) { // Validation logic here } pluginContext.TracingService.Trace( $"Validation completed with rules: {_settings.RuleSet}"); } }


🔑 Critical Things Every D365 Plugin Dev Should Know

  1. Thread Safety & Context

// Always use thread-safe collections private static readonly ConcurrentDictionary<string, object> Cache = new ConcurrentDictionary<string, object>(); // Check execution context if (context.Depth > 1) return; // Prevent recursive loops
  1. Performance Optimization

// Batch your requests var multipleRequest = new ExecuteMultipleRequest { Settings = new ExecuteMultipleSettings { ContinueOnError = false, ReturnResponses = true }, Requests = requests.ToArray() };
  1. Error Handling Best Practices

try { // Your logic } catch (FaultException<OrganizationServiceFault> ex) { // Handle CRM-specific errors tracer.Trace($"Error Code: {ex.Detail.ErrorCode}"); } catch (TimeoutException ex) { // Handle timeouts gracefully throw new InvalidPluginExecutionException( "Operation timed out. Please try again.", ex); }

Common Gotchas to Avoid

  1. Plugin Isolation
    • Don't share static state between plugin instances
    • Avoid file system operations
    • Never store sensitive data in static variables
  2. Transaction Management

// Check if you're in a transaction if (!context.IsInTransaction) { throw new InvalidPluginExecutionException( "This operation must be part of a transaction."); }
  1. Cache Usage

// Implement proper caching private static readonly MemoryCache Cache = new MemoryCache( new MemoryCacheOptions { SizeLimit = 1024 }); // Use cache with expiration Cache.Set(key, value, TimeSpan.FromMinutes(5));

Advanced Techniques

  1. Custom Actions Integration

public class CustomActionPlugin : PluginBase { protected override void ExecutePluginLogic(PluginContext context) { if (context.Context.MessageName != "my_customaction") return; var parameters = context.Context.InputParameters; // Process custom action } }
  1. Bulk Operation Handling

// Handle bulk operations efficiently private async Task ProcessBulkAsync( IOrganizationService service, List<Entity> entities) { var tasks = entities .Select(entity => Task.Run(() => ProcessSingle(service, entity))); await Task.WhenAll(tasks); }

Performance Metrics to Monitor

  1. Plugin execution time
  2. Database calls per operation
  3. Memory usage patterns
  4. Exception rates

public class PerformanceMetrics { private readonly Stopwatch _timer = new Stopwatch(); private int _dbCalls = 0; public void TrackDatabaseCall() { Interlocked.Increment(ref _dbCalls); } }

Final Words of Wisdom

  • Always test with large datasets
  • Plan for failure scenarios
  • Keep security at the forefront
  • Document your assumptions
  • Use code reviews religiously

🔚 Wrapping Up

Remember: a good plugin is like a good referee - it does its job without anyone noticing. Keep it simple, keep it fast, and always, always test in a sandbox first.


#MSDynamics #CRMDevelopment #Plugins #DotNet #RealTalk

Sri Lanka .NET 
                Forum Member