From df1446c9ad367007e3fe7aacee63f4b4f1709747 Mon Sep 17 00:00:00 2001 From: Morten Holt Date: Wed, 4 Mar 2026 08:24:36 +0100 Subject: [PATCH 1/3] Add: Optional File logging during startup, logging CustomAPI, Plugin, and Workflow detection --- src/XrmMockup365/Core.cs | 52 ++++- .../Internal/CoreInitializationData.cs | 4 +- src/XrmMockup365/Logging/FileLogger.cs | 57 +++++ src/XrmMockup365/Logging/FileLoggerFactory.cs | 56 +++++ src/XrmMockup365/Plugin/CustomApiManager.cs | 23 +- src/XrmMockup365/Plugin/PluginManager.cs | 32 ++- src/XrmMockup365/StaticMetadataCache.cs | 10 +- src/XrmMockup365/Workflow/WorkflowManager.cs | 25 ++- src/XrmMockup365/XrmMockup365.csproj | 1 + src/XrmMockup365/XrmMockupSettings.cs | 15 +- tests/XrmMockup365Test/TestLogging.cs | 203 ++++++++++++++++++ 11 files changed, 456 insertions(+), 22 deletions(-) create mode 100644 src/XrmMockup365/Logging/FileLogger.cs create mode 100644 src/XrmMockup365/Logging/FileLoggerFactory.cs create mode 100644 tests/XrmMockup365Test/TestLogging.cs diff --git a/src/XrmMockup365/Core.cs b/src/XrmMockup365/Core.cs index 8fe3d26d..3e173f68 100644 --- a/src/XrmMockup365/Core.cs +++ b/src/XrmMockup365/Core.cs @@ -1,8 +1,11 @@ using DG.Tools.XrmMockup.Database; using DG.Tools.XrmMockup.Internal; +using DG.Tools.XrmMockup.Logging; using DG.Tools.XrmMockup.Serialization; using XrmPluginCore.Enums; using Microsoft.Crm.Sdk.Messages; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Client; using Microsoft.Xrm.Sdk.Messages; @@ -11,6 +14,7 @@ using Microsoft.Xrm.Sdk.Query; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -80,7 +84,8 @@ public Core(XrmMockupSettings Settings, MetadataSkeleton metadata, List BaseCurrency = metadata.BaseOrganization.GetAttributeValue("basecurrencyid"), BaseCurrencyPrecision = metadata.BaseOrganization.GetAttributeValue("pricingdecimalprecision"), OnlineProxy = null, - EntityTypeMap = new Dictionary() + EntityTypeMap = new Dictionary(), + LoggerFactory = ResolveLoggerFactory(Settings) }; InitializeCore(initData); @@ -100,7 +105,8 @@ public Core(XrmMockupSettings Settings, StaticMetadataCache staticCache) BaseCurrency = staticCache.BaseCurrency, BaseCurrencyPrecision = staticCache.BaseCurrencyPrecision, OnlineProxy = staticCache.OnlineProxy, - EntityTypeMap = staticCache.EntityTypeMap + EntityTypeMap = staticCache.EntityTypeMap, + LoggerFactory = staticCache.LoggerFactory }; InitializeCore(initData); @@ -111,6 +117,10 @@ public Core(XrmMockupSettings Settings, StaticMetadataCache staticCache) /// private void InitializeCore(CoreInitializationData initData) { + var sw = Stopwatch.StartNew(); + var loggerFactory = initData.LoggerFactory ?? NullLoggerFactory.Instance; + var coreLogger = loggerFactory.CreateLogger(typeof(Core).FullName); + TimeOffset = new TimeSpan(); settings = initData.Settings; metadata = initData.Metadata; @@ -134,10 +144,15 @@ private void InitializeCore(CoreInitializationData initData) allPlugins.AddRange(initData.Settings.IPluginMetadata); } - pluginManager = new PluginManager(initData.Settings.BasePluginTypes, initData.Metadata.EntityMetadata, allPlugins); + var pluginLogger = loggerFactory.CreateLogger(typeof(PluginManager).FullName); + pluginManager = new PluginManager(initData.Settings.BasePluginTypes, initData.Metadata.EntityMetadata, allPlugins, pluginLogger); + + var workflowLogger = loggerFactory.CreateLogger(typeof(WorkflowManager).FullName); workflowManager = new WorkflowManager(initData.Settings.CodeActivityInstanceTypes, initData.Settings.IncludeAllWorkflows, - initData.Workflows, initData.Metadata.EntityMetadata); - customApiManager = new CustomApiManager(initData.Settings.BaseCustomApiTypes); + initData.Workflows, initData.Metadata.EntityMetadata, workflowLogger); + + var apiLogger = loggerFactory.CreateLogger(typeof(CustomApiManager).FullName); + customApiManager = new CustomApiManager(initData.Settings.BaseCustomApiTypes, apiLogger); var typesMissingRegistration = pluginManager.missingRegistrations .Intersect(customApiManager.missingRegistration) @@ -148,6 +163,14 @@ private void InitializeCore(CoreInitializationData initData) throw new Exception($"The following plugin types are missing plugin or custom api registrations: {typeNames}"); } + sw.Stop(); + coreLogger.LogInformation("XrmMockup initialized in {ElapsedMs}ms — Plugins: {PluginCount}, Workflows: sync={SyncCount} async={AsyncCount}, Custom APIs: {ApiCount}", + sw.ElapsedMilliseconds, + pluginManager.PluginRegistrations.Count, + workflowManager.SynchronousWorkflowCount, + workflowManager.AsynchronousWorkflowCount, + customApiManager.RegisteredApiCount); + systemAttributeNames = new List() { "createdon", "createdby", "modifiedon", "modifiedby" }; RequestHandlers = GetRequestHandlers(db); @@ -194,11 +217,24 @@ public static StaticMetadataCache BuildStaticMetadataCache(XrmMockupSettings set BuildEntityTypeMap(settings, entityTypeMap); } - // Note: IPluginMetadata is handled per-instance in the Core constructor + // Note: IPluginMetadata is handled per-instance in the Core constructor // to avoid modifying the shared cache - return new StaticMetadataCache(metadata, workflows, securityRoles, entityTypeMap, - baseCurrency, baseCurrencyPrecision, onlineProxy); + var loggerFactory = ResolveLoggerFactory(settings); + + return new StaticMetadataCache(metadata, workflows, securityRoles, entityTypeMap, + baseCurrency, baseCurrencyPrecision, onlineProxy, loggerFactory); + } + + private static ILoggerFactory ResolveLoggerFactory(XrmMockupSettings settings) + { + if (settings.LoggerFactory != null) + return settings.LoggerFactory; + + if (!string.IsNullOrEmpty(settings.LogFilePath)) + return new FileLoggerFactory(settings.LogFilePath); + + return NullLoggerFactory.Instance; } private static OrganizationServiceProxy BuildOnlineProxy(XrmMockupSettings settings) diff --git a/src/XrmMockup365/Internal/CoreInitializationData.cs b/src/XrmMockup365/Internal/CoreInitializationData.cs index 4be6b99e..85dc9809 100644 --- a/src/XrmMockup365/Internal/CoreInitializationData.cs +++ b/src/XrmMockup365/Internal/CoreInitializationData.cs @@ -1,4 +1,5 @@ -using Microsoft.Xrm.Sdk; +using Microsoft.Extensions.Logging; +using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Client; using System; using System.Collections.Generic; @@ -18,5 +19,6 @@ internal class CoreInitializationData public int BaseCurrencyPrecision { get; set; } public OrganizationServiceProxy OnlineProxy { get; set; } public Dictionary EntityTypeMap { get; set; } + public ILoggerFactory LoggerFactory { get; set; } } } diff --git a/src/XrmMockup365/Logging/FileLogger.cs b/src/XrmMockup365/Logging/FileLogger.cs new file mode 100644 index 00000000..fe35596c --- /dev/null +++ b/src/XrmMockup365/Logging/FileLogger.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.Logging; +using System; +using System.IO; + +namespace DG.Tools.XrmMockup.Logging +{ + internal class FileLogger : ILogger + { + private readonly string _categoryName; + private readonly StreamWriter _writer; + private readonly object _lock; + + public FileLogger(string categoryName, StreamWriter writer, object writeLock) + { + _categoryName = categoryName; + _writer = writer; + _lock = writeLock; + } + + public IDisposable BeginScope(TState state) => NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + var message = formatter(state, exception); + if (string.IsNullOrEmpty(message)) + return; + + var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{logLevel}] {_categoryName}: {message}"; + + lock (_lock) + { + _writer.WriteLine(line); + _writer.Flush(); + } + + if (exception != null) + { + lock (_lock) + { + _writer.WriteLine(exception.ToString()); + _writer.Flush(); + } + } + } + + private class NullScope : IDisposable + { + public static NullScope Instance { get; } = new NullScope(); + public void Dispose() { } + } + } +} diff --git a/src/XrmMockup365/Logging/FileLoggerFactory.cs b/src/XrmMockup365/Logging/FileLoggerFactory.cs new file mode 100644 index 00000000..0ca30a20 --- /dev/null +++ b/src/XrmMockup365/Logging/FileLoggerFactory.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Logging; +using System; +using System.IO; + +namespace DG.Tools.XrmMockup.Logging +{ + internal class FileLoggerFactory : ILoggerFactory, IDisposable + { + private readonly StreamWriter _writer; + private readonly object _writeLock = new object(); + private bool _disposed; + + public FileLoggerFactory(string filePath) + { + var directory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); + _writer = new StreamWriter(fileStream) { AutoFlush = true }; + } + + public ILogger CreateLogger(string categoryName) + { + // Shorten category name to just the class name + var shortName = categoryName; + var lastDot = categoryName.LastIndexOf('.'); + if (lastDot >= 0 && lastDot < categoryName.Length - 1) + { + shortName = categoryName.Substring(lastDot + 1); + } + + return new FileLogger(shortName, _writer, _writeLock); + } + + public void AddProvider(ILoggerProvider provider) + { + // Not needed for this simple implementation + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + lock (_writeLock) + { + _writer?.Flush(); + _writer?.Dispose(); + } + } + } + } +} diff --git a/src/XrmMockup365/Plugin/CustomApiManager.cs b/src/XrmMockup365/Plugin/CustomApiManager.cs index f5de03d2..e398fcbf 100644 --- a/src/XrmMockup365/Plugin/CustomApiManager.cs +++ b/src/XrmMockup365/Plugin/CustomApiManager.cs @@ -1,6 +1,8 @@ using DG.Tools.XrmMockup.Internal; using DG.Tools.XrmMockup.Plugin.RegistrationStrategy; using XrmPluginCore.Interfaces.CustomApi; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Xrm.Sdk; using System; using System.Collections.Concurrent; @@ -14,6 +16,7 @@ namespace DG.Tools.XrmMockup { internal class CustomApiManager { + private readonly ILogger _logger; // Static caches shared across all CustomApiManager instances private static readonly ConcurrentDictionary>> _cachedApis = new ConcurrentDictionary>>(); @@ -23,14 +26,17 @@ internal class CustomApiManager internal List missingRegistration = new List(); private Dictionary> registeredApis = new Dictionary>(); + internal int RegisteredApiCount => registeredApis.Count; + private readonly List> registrationStrategies = new List> { new Plugin.RegistrationStrategy.XrmPluginCore.CustomApiRegistrationStrategy(), new Plugin.RegistrationStrategy.DAXIF.CustomApiRegistrationStrategy() }; - public CustomApiManager(IEnumerable> baseCustomApiTypes) + public CustomApiManager(IEnumerable> baseCustomApiTypes, ILogger logger = null) { + _logger = logger ?? NullLogger.Instance; var cacheKey = GenerateApiCacheKey(baseCustomApiTypes); // Check if we have cached results @@ -38,6 +44,7 @@ public CustomApiManager(IEnumerable> baseCustomApiTypes) { // Use cached results - no reflection/instantiation needed registeredApis = new Dictionary>(_cachedApis[cacheKey]); + _logger.LogDebug("Loaded {Count} custom API registrations from cache", registeredApis.Count); return; } @@ -47,6 +54,7 @@ public CustomApiManager(IEnumerable> baseCustomApiTypes) if (_cachedApis.ContainsKey(cacheKey)) { registeredApis = new Dictionary>(_cachedApis[cacheKey]); + _logger.LogDebug("Loaded {Count} custom API registrations from cache", registeredApis.Count); return; } @@ -59,6 +67,19 @@ public CustomApiManager(IEnumerable> baseCustomApiTypes) // Cache for future instances _cachedApis[cacheKey] = new Dictionary>(registeredApis); + + foreach (var apiKey in registeredApis.Keys) + { + _logger.LogInformation(" Registered custom API: {ApiKey}", apiKey); + } + + foreach (var missing in missingRegistration) + { + _logger.LogWarning(" Custom API type missing registration: {TypeName}", missing.FullName); + } + + _logger.LogInformation("Custom API scanning complete: {Count} registrations, {MissingCount} missing", + registeredApis.Count, missingRegistration.Count); } } diff --git a/src/XrmMockup365/Plugin/PluginManager.cs b/src/XrmMockup365/Plugin/PluginManager.cs index 8344dce8..e5dec4fa 100644 --- a/src/XrmMockup365/Plugin/PluginManager.cs +++ b/src/XrmMockup365/Plugin/PluginManager.cs @@ -3,6 +3,8 @@ using DG.Tools.XrmMockup.SystemPlugins; using XrmPluginCore.Enums; using XrmPluginCore.Interfaces.Plugin; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Metadata; using System; @@ -22,6 +24,8 @@ namespace DG.Tools.XrmMockup internal class PluginManager { + private readonly ILogger _logger; + // Static caches shared across all PluginManager instances private static readonly ConcurrentDictionary> _cachedRegisteredPlugins = new ConcurrentDictionary>(); private static readonly ConcurrentDictionary> _cachedSystemPlugins = new ConcurrentDictionary>(); @@ -56,8 +60,9 @@ internal class PluginManager new Plugin.RegistrationStrategy.DAXIF.PluginRegistrationStrategy() }; - public PluginManager(IEnumerable basePluginTypes, Dictionary metadata, List plugins) + public PluginManager(IEnumerable basePluginTypes, Dictionary metadata, List plugins, ILogger logger = null) { + _logger = logger ?? NullLogger.Instance; temporaryPlugins = new Dictionary(); var pluginCacheKey = GeneratePluginCacheKey(basePluginTypes); @@ -69,6 +74,12 @@ public PluginManager(IEnumerable basePluginTypes, Dictionary s.Values.SelectMany(l => l)).Count()); + foreach (var reg in PluginRegistrations) + { + _logger.LogDebug(" Plugin: {Registration}", reg); + } } else { @@ -79,6 +90,8 @@ public PluginManager(IEnumerable basePluginTypes, Dictionary s.Values.SelectMany(l => l)).Count()); } else { @@ -86,9 +99,7 @@ public PluginManager(IEnumerable basePluginTypes, Dictionary(); registeredSystemPlugins = new Dictionary(); - // TODO: Find all concrete types that implement IPlugin, handle system plugins separately - // TODO: How do we filter CustomAPIs? - // TODO: Should basePluginTypes act as an optional filter? + _logger.LogInformation("Scanning assemblies for plugin registrations..."); RegisterPlugins(basePluginTypes, metadata, plugins, registeredPlugins); RegisterDirectPlugins(basePluginTypes, metadata, plugins, registeredPlugins); @@ -97,6 +108,19 @@ public PluginManager(IEnumerable basePluginTypes, Dictionary workflows, List securityRoles, - Dictionary entityTypeMap, EntityReference baseCurrency, int baseCurrencyPrecision, - OrganizationServiceProxy onlineProxy) + Dictionary entityTypeMap, EntityReference baseCurrency, int baseCurrencyPrecision, + OrganizationServiceProxy onlineProxy, ILoggerFactory loggerFactory = null) { Metadata = metadata; Workflows = workflows; @@ -26,6 +29,7 @@ public StaticMetadataCache(MetadataSkeleton metadata, List workflows, Li BaseCurrency = baseCurrency; BaseCurrencyPrecision = baseCurrencyPrecision; OnlineProxy = onlineProxy; + LoggerFactory = loggerFactory ?? NullLoggerFactory.Instance; } } } diff --git a/src/XrmMockup365/Workflow/WorkflowManager.cs b/src/XrmMockup365/Workflow/WorkflowManager.cs index 59033ce8..0e829321 100644 --- a/src/XrmMockup365/Workflow/WorkflowManager.cs +++ b/src/XrmMockup365/Workflow/WorkflowManager.cs @@ -7,6 +7,8 @@ using System.Collections.Concurrent; using System.Security.Cryptography; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; using System.ServiceModel; @@ -22,6 +24,8 @@ namespace DG.Tools.XrmMockup { internal class WorkflowManager { + private readonly ILogger _logger; + // Static caches shared across all WorkflowManager instances private static readonly ConcurrentDictionary _staticParsedWorkflows = new ConcurrentDictionary(); private static readonly ConcurrentDictionary _staticCodeActivityCache = new ConcurrentDictionary(); @@ -37,7 +41,13 @@ internal class WorkflowManager { private ConcurrentQueue pendingAsyncWorkflows; - public WorkflowManager(IEnumerable codeActivityInstances, bool? IncludeAllWorkflows, List mixedWorkflows, Dictionary metadata) { + internal int SynchronousWorkflowCount => synchronousWorkflows.Count; + internal int AsynchronousWorkflowCount => asynchronousWorkflows.Count; + internal int ActionsCount => actions.Count; + internal int CodeActivityCount => codeActivityTriggers.Count; + + public WorkflowManager(IEnumerable codeActivityInstances, bool? IncludeAllWorkflows, List mixedWorkflows, Dictionary metadata, ILogger logger = null) { + _logger = logger ?? NullLogger.Instance; this.metadata = metadata; this.actions = mixedWorkflows.Where(w => w.GetAttributeValue("category").Value == 3).ToList(); @@ -58,7 +68,7 @@ public WorkflowManager(IEnumerable codeActivityInstances, bool? IncludeAll foreach (var codeActivityInstance in codeActivityInstances) { foreach (var type in codeActivityInstance.Assembly.GetTypes()) { if (type.BaseType != codeActivityInstance.BaseType || type.Module.Name.StartsWith("System") || type.IsAbstract) continue; - + // Use static cache to avoid recreating CodeActivity instances var codeActivity = _staticCodeActivityCache.GetOrAdd(type.Name, typeName => { @@ -67,6 +77,7 @@ public WorkflowManager(IEnumerable codeActivityInstances, bool? IncludeAll if (!codeActivityTriggers.ContainsKey(type.Name)) { codeActivityTriggers.Add(type.Name, codeActivity); + _logger.LogDebug("Discovered CodeActivity: {TypeName}", type.Name); } } } @@ -77,6 +88,9 @@ public WorkflowManager(IEnumerable codeActivityInstances, bool? IncludeAll AddWorkflow(workflow); } } + + _logger.LogInformation("Workflows loaded: sync={SyncCount}, async={AsyncCount}, actions={ActionsCount}, code activities={CodeActivityCount}", + synchronousWorkflows.Count, asynchronousWorkflows.Count, actions.Count, codeActivityTriggers.Count); } @@ -427,18 +441,21 @@ internal WorkflowTree ParseWorkflow(Entity workflow) { _staticParsedWorkflows.TryAdd(cacheKey, parsed); return parsed; - } catch (Exception) { - Console.WriteLine($"Tried to parse workflow with name '{workflow.Attributes["name"]}' but failed"); + } catch (Exception ex) { + _logger.LogWarning(ex, "Failed to parse workflow '{WorkflowName}'", workflow.Attributes["name"]); } return null; } internal void AddWorkflow(Entity workflow) { if (workflow.LogicalName != LogicalNames.Workflow) return; + var name = workflow.GetAttributeValue("name") ?? workflow.Id.ToString(); if (workflow.GetOptionSetValue("mode") == Workflow_Mode.Background) { asynchronousWorkflows.Add(workflow); + _logger.LogDebug("Added async workflow: {WorkflowName}", name); } else { synchronousWorkflows.Add(workflow); + _logger.LogDebug("Added sync workflow: {WorkflowName}", name); } } diff --git a/src/XrmMockup365/XrmMockup365.csproj b/src/XrmMockup365/XrmMockup365.csproj index a1cddfb6..d895852d 100644 --- a/src/XrmMockup365/XrmMockup365.csproj +++ b/src/XrmMockup365/XrmMockup365.csproj @@ -73,6 +73,7 @@ + diff --git a/src/XrmMockup365/XrmMockupSettings.cs b/src/XrmMockup365/XrmMockupSettings.cs index 69f4d67e..df084654 100644 --- a/src/XrmMockup365/XrmMockupSettings.cs +++ b/src/XrmMockup365/XrmMockupSettings.cs @@ -1,4 +1,5 @@ -using Microsoft.Xrm.Sdk.Client; +using Microsoft.Extensions.Logging; +using Microsoft.Xrm.Sdk.Client; using System; using System.Collections.Generic; using Microsoft.Xrm.Sdk.Organization; @@ -98,6 +99,18 @@ public class XrmMockupSettings /// Default is true. /// public bool EnablePowerFxFields { get; set; } = true; + + /// + /// Optional file path for diagnostic logging. When set, XrmMockup writes startup + /// information (discovered plugins, workflows, custom APIs) to this file. + /// + public string LogFilePath { get; set; } + + /// + /// Optional logger factory for diagnostic logging. When set, takes precedence + /// over . Use this to integrate with your own logging infrastructure. + /// + public ILoggerFactory LoggerFactory { get; set; } } diff --git a/tests/XrmMockup365Test/TestLogging.cs b/tests/XrmMockup365Test/TestLogging.cs new file mode 100644 index 00000000..5cd6fae6 --- /dev/null +++ b/tests/XrmMockup365Test/TestLogging.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using DG.Tools.XrmMockup; +using DG.Some.Namespace; +using Microsoft.Extensions.Logging; +using TestPluginAssembly365.Plugins.LegacyDaxif; +using TestPluginAssembly365.Plugins.ServiceBased; +using XrmPluginCore; +using Xunit; + +namespace DG.XrmMockupTest +{ + public class TestLogging : IDisposable + { + private readonly List _tempFiles = new List(); + + private XrmMockupSettings CreateSettingsWithLogFile(out string logFilePath) + { + logFilePath = Path.Combine(Path.GetTempPath(), $"xrmmockup_test_{Guid.NewGuid():N}.log"); + _tempFiles.Add(logFilePath); + + return new XrmMockupSettings + { + BasePluginTypes = new Type[] { typeof(Plugin), typeof(PluginNonDaxif), typeof(LegacyPlugin), typeof(DIPlugin) }, + BaseCustomApiTypes = new[] { new Tuple("dg", typeof(Plugin)), new Tuple("dg", typeof(LegacyCustomApi)) }, + CodeActivityInstanceTypes = new Type[] { typeof(AccountWorkflowActivity) }, + EnableProxyTypes = true, + IncludeAllWorkflows = false, + ExceptionFreeRequests = new string[] { "TestWrongRequest" }, + MetadataDirectoryPath = GetMetadataPath(), + LogFilePath = logFilePath + }; + } + + [Fact] + public void TestLogFileCreated() + { + var settings = CreateSettingsWithLogFile(out var logFilePath); + var crm = XrmMockup365.GetInstance(settings); + + Assert.True(File.Exists(logFilePath), "Log file should exist after initialization"); + var content = ReadLogFile(logFilePath); + Assert.False(string.IsNullOrWhiteSpace(content), "Log file should not be empty"); + } + + [Fact] + public void TestLogFileContainsPluginInfo() + { + var settings = CreateSettingsWithLogFile(out var logFilePath); + var crm = XrmMockup365.GetInstance(settings); + + var content = ReadLogFile(logFilePath); + Assert.Contains("plugin", content, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Plugins:", content, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void TestLogFileContainsWorkflowInfo() + { + var settings = CreateSettingsWithLogFile(out var logFilePath); + var crm = XrmMockup365.GetInstance(settings); + + var content = ReadLogFile(logFilePath); + Assert.Contains("Workflow", content, StringComparison.OrdinalIgnoreCase); + Assert.Contains("code activit", content, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void TestLogFileContainsCustomApiInfo() + { + var settings = CreateSettingsWithLogFile(out var logFilePath); + var crm = XrmMockup365.GetInstance(settings); + + var content = ReadLogFile(logFilePath); + Assert.Contains("Custom API", content, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void TestNoLogFileWithoutSetting() + { + var logFilePath = Path.Combine(Path.GetTempPath(), $"xrmmockup_nolog_{Guid.NewGuid():N}.log"); + _tempFiles.Add(logFilePath); + + var settings = new XrmMockupSettings + { + BasePluginTypes = new Type[] { typeof(Plugin), typeof(PluginNonDaxif), typeof(LegacyPlugin), typeof(DIPlugin) }, + BaseCustomApiTypes = new[] { new Tuple("dg", typeof(Plugin)), new Tuple("dg", typeof(LegacyCustomApi)) }, + CodeActivityInstanceTypes = new Type[] { typeof(AccountWorkflowActivity) }, + EnableProxyTypes = true, + IncludeAllWorkflows = false, + MetadataDirectoryPath = GetMetadataPath() + }; + + var crm = XrmMockup365.GetInstance(settings); + + Assert.False(File.Exists(logFilePath), "Log file should not exist when LogFilePath is not set"); + } + + [Fact] + public void TestCustomLoggerFactoryIsUsed() + { + var factory = new InMemoryLoggerFactory(); + + var settings = new XrmMockupSettings + { + BasePluginTypes = new Type[] { typeof(Plugin), typeof(PluginNonDaxif), typeof(LegacyPlugin), typeof(DIPlugin) }, + BaseCustomApiTypes = new[] { new Tuple("dg", typeof(Plugin)), new Tuple("dg", typeof(LegacyCustomApi)) }, + CodeActivityInstanceTypes = new Type[] { typeof(AccountWorkflowActivity) }, + EnableProxyTypes = true, + IncludeAllWorkflows = false, + MetadataDirectoryPath = GetMetadataPath(), + LoggerFactory = factory + }; + + var crm = XrmMockup365.GetInstance(settings); + + Assert.True(factory.CreateLoggerCallCount > 0, "LoggerFactory.CreateLogger should have been called"); + Assert.True(factory.LogMessages.Count > 0, "Logger should have captured log messages"); + } + + public void Dispose() + { + foreach (var file in _tempFiles) + { + try { if (File.Exists(file)) File.Delete(file); } catch { } + } + } + + private static string ReadLogFile(string path) + { + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var reader = new StreamReader(fs)) + { + return reader.ReadToEnd(); + } + } + + private static string GetMetadataPath() + { + var currentDir = Directory.GetCurrentDirectory(); + var metadataPath = Path.Combine(currentDir, "Metadata"); + if (Directory.Exists(metadataPath)) return metadataPath; + + var testProjectPaths = new[] + { + Path.Combine(currentDir, "..", "..", "..", "Metadata"), + "Metadata" + }; + + foreach (var path in testProjectPaths) + { + var fullPath = Path.GetFullPath(path); + if (Directory.Exists(fullPath)) return fullPath; + } + + throw new DirectoryNotFoundException($"Could not find Metadata directory. Searched in: {currentDir}"); + } + + private class InMemoryLoggerFactory : ILoggerFactory + { + public int CreateLoggerCallCount { get; private set; } + public ConcurrentBag LogMessages { get; } = new ConcurrentBag(); + + public ILogger CreateLogger(string categoryName) + { + CreateLoggerCallCount++; + return new InMemoryLogger(categoryName, LogMessages); + } + + public void AddProvider(ILoggerProvider provider) { } + public void Dispose() { } + } + + private class InMemoryLogger : ILogger + { + private readonly string _category; + private readonly ConcurrentBag _messages; + + public InMemoryLogger(string category, ConcurrentBag messages) + { + _category = category; + _messages = messages; + } + + public IDisposable BeginScope(TState state) => NullScope.Instance; + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) return; + _messages.Add($"[{logLevel}] {_category}: {formatter(state, exception)}"); + } + + private class NullScope : IDisposable + { + public static NullScope Instance { get; } = new NullScope(); + public void Dispose() { } + } + } + } +} From c8815132dcb362cd785bbd885b3dd42c488445b0 Mon Sep 17 00:00:00 2001 From: Morten Holt Date: Wed, 4 Mar 2026 08:50:44 +0100 Subject: [PATCH 2/3] Enhanced logging: configurable log level & detailed diagnostics - Add MinLogLevel to XrmMockupSettings for file logger filtering - FileLogger now respects minimum log level and improves flushing - Add detailed diagnostic logging to plugin/custom API discovery and registration - Inject ILogger into registration strategies for better traceability - Update unit tests to verify log level filtering and debug output - Improve documentation for new logging options and behaviors --- src/XrmMockup365/Core.cs | 2 +- src/XrmMockup365/Logging/FileLogger.cs | 15 +++---- src/XrmMockup365/Logging/FileLoggerFactory.cs | 7 +++- src/XrmMockup365/Plugin/CustomApiManager.cs | 23 ++++++++--- src/XrmMockup365/Plugin/PluginManager.cs | 34 +++++++++++----- .../DAXIF/CustomApiRegistrationStrategy.cs | 16 +++++++- .../DAXIF/PluginRegistrationStrategy.cs | 21 ++++++++-- .../IRegistrationStrategy.cs | 2 +- .../MetadataRegistrationStrategy.cs | 24 +++++++++++- .../CustomApiRegistrationStrategy.cs | 20 +++++++++- .../PluginRegistrationStrategy.cs | 19 +++++++-- src/XrmMockup365/XrmMockupSettings.cs | 7 ++++ tests/XrmMockup365Test/TestLogging.cs | 39 +++++++++++++++++++ 13 files changed, 191 insertions(+), 38 deletions(-) diff --git a/src/XrmMockup365/Core.cs b/src/XrmMockup365/Core.cs index 3e173f68..e7c9dd04 100644 --- a/src/XrmMockup365/Core.cs +++ b/src/XrmMockup365/Core.cs @@ -232,7 +232,7 @@ private static ILoggerFactory ResolveLoggerFactory(XrmMockupSettings settings) return settings.LoggerFactory; if (!string.IsNullOrEmpty(settings.LogFilePath)) - return new FileLoggerFactory(settings.LogFilePath); + return new FileLoggerFactory(settings.LogFilePath, settings.MinLogLevel); return NullLoggerFactory.Instance; } diff --git a/src/XrmMockup365/Logging/FileLogger.cs b/src/XrmMockup365/Logging/FileLogger.cs index fe35596c..712aed60 100644 --- a/src/XrmMockup365/Logging/FileLogger.cs +++ b/src/XrmMockup365/Logging/FileLogger.cs @@ -9,17 +9,19 @@ internal class FileLogger : ILogger private readonly string _categoryName; private readonly StreamWriter _writer; private readonly object _lock; + private readonly LogLevel _minLogLevel; - public FileLogger(string categoryName, StreamWriter writer, object writeLock) + public FileLogger(string categoryName, StreamWriter writer, object writeLock, LogLevel minLogLevel) { _categoryName = categoryName; _writer = writer; _lock = writeLock; + _minLogLevel = minLogLevel; } public IDisposable BeginScope(TState state) => NullScope.Instance; - public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None && logLevel >= _minLogLevel; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { @@ -35,16 +37,11 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except lock (_lock) { _writer.WriteLine(line); - _writer.Flush(); - } - - if (exception != null) - { - lock (_lock) + if (exception != null) { _writer.WriteLine(exception.ToString()); - _writer.Flush(); } + _writer.Flush(); } } diff --git a/src/XrmMockup365/Logging/FileLoggerFactory.cs b/src/XrmMockup365/Logging/FileLoggerFactory.cs index 0ca30a20..e8a2c261 100644 --- a/src/XrmMockup365/Logging/FileLoggerFactory.cs +++ b/src/XrmMockup365/Logging/FileLoggerFactory.cs @@ -8,10 +8,13 @@ internal class FileLoggerFactory : ILoggerFactory, IDisposable { private readonly StreamWriter _writer; private readonly object _writeLock = new object(); + private readonly LogLevel _minLogLevel; private bool _disposed; - public FileLoggerFactory(string filePath) + public FileLoggerFactory(string filePath, LogLevel minLogLevel = LogLevel.Information) { + _minLogLevel = minLogLevel; + var directory = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { @@ -32,7 +35,7 @@ public ILogger CreateLogger(string categoryName) shortName = categoryName.Substring(lastDot + 1); } - return new FileLogger(shortName, _writer, _writeLock); + return new FileLogger(shortName, _writer, _writeLock, _minLogLevel); } public void AddProvider(ILoggerProvider provider) diff --git a/src/XrmMockup365/Plugin/CustomApiManager.cs b/src/XrmMockup365/Plugin/CustomApiManager.cs index e398fcbf..572bd390 100644 --- a/src/XrmMockup365/Plugin/CustomApiManager.cs +++ b/src/XrmMockup365/Plugin/CustomApiManager.cs @@ -28,15 +28,16 @@ internal class CustomApiManager internal int RegisteredApiCount => registeredApis.Count; - private readonly List> registrationStrategies = new List> - { - new Plugin.RegistrationStrategy.XrmPluginCore.CustomApiRegistrationStrategy(), - new Plugin.RegistrationStrategy.DAXIF.CustomApiRegistrationStrategy() - }; + private readonly List> registrationStrategies; public CustomApiManager(IEnumerable> baseCustomApiTypes, ILogger logger = null) { _logger = logger ?? NullLogger.Instance; + registrationStrategies = new List> + { + new Plugin.RegistrationStrategy.XrmPluginCore.CustomApiRegistrationStrategy(_logger), + new Plugin.RegistrationStrategy.DAXIF.CustomApiRegistrationStrategy(_logger) + }; var cacheKey = GenerateApiCacheKey(baseCustomApiTypes); // Check if we have cached results @@ -92,10 +93,14 @@ private void RegisterCustomApis(IEnumerable> customApiBaseTy var prefix = customApiMapping.Item1; var baseApiType = customApiMapping.Item2; + _logger.LogDebug("Scanning for types extending custom API base type: {BaseType} (prefix={Prefix})", baseApiType.FullName, prefix); + var customApiTypes = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(a => a.GetLoadableTypes().Where(t => !t.IsAbstract && t.IsPublic && t.BaseType != null && (t.BaseType == baseApiType || (t.BaseType.IsGenericType && t.BaseType.GetGenericTypeDefinition() == baseApiType)))) .ToList(); + _logger.LogDebug("Found {Count} concrete type(s) extending {BaseType}", customApiTypes.Count, baseApiType.FullName); + foreach (var type in customApiTypes) { RegisterApi(prefix, type); @@ -105,10 +110,13 @@ private void RegisterCustomApis(IEnumerable> customApiBaseTy private void RegisterApi(string prefix, Type pluginType) { + _logger.LogDebug("Evaluating custom API type: {TypeName} (prefix={Prefix})", pluginType.FullName, prefix); + var plugin = _apiInstanceCache.GetOrAdd(pluginType, Utility.CreatePluginInstance); if (plugin == null) { + _logger.LogWarning("Failed to create instance of custom API type: {TypeName}", pluginType.FullName); return; } @@ -118,11 +126,14 @@ private void RegisterApi(string prefix, Type pluginType) if (registration is null) { + _logger.LogDebug("{TypeName}: no custom API registration found from any strategy", pluginType.FullName); missingRegistration.Add(pluginType); } else { - registeredApis.Add($"{prefix}_{registration.UniqueName}", plugin.Execute); + var key = $"{prefix}_{registration.UniqueName}"; + _logger.LogDebug("{TypeName}: registered as '{ApiKey}'", pluginType.FullName, key); + registeredApis.Add(key, plugin.Execute); } } diff --git a/src/XrmMockup365/Plugin/PluginManager.cs b/src/XrmMockup365/Plugin/PluginManager.cs index e5dec4fa..0341d4db 100644 --- a/src/XrmMockup365/Plugin/PluginManager.cs +++ b/src/XrmMockup365/Plugin/PluginManager.cs @@ -54,15 +54,16 @@ internal class PluginManager new SetAnnotationIsDocument() }; - private readonly List> registrationStrategies = new List> - { - new Plugin.RegistrationStrategy.XrmPluginCore.PluginRegistrationStrategy(), - new Plugin.RegistrationStrategy.DAXIF.PluginRegistrationStrategy() - }; + private readonly List> registrationStrategies; public PluginManager(IEnumerable basePluginTypes, Dictionary metadata, List plugins, ILogger logger = null) { _logger = logger ?? NullLogger.Instance; + registrationStrategies = new List> + { + new Plugin.RegistrationStrategy.XrmPluginCore.PluginRegistrationStrategy(_logger), + new Plugin.RegistrationStrategy.DAXIF.PluginRegistrationStrategy(_logger) + }; temporaryPlugins = new Dictionary(); var pluginCacheKey = GeneratePluginCacheKey(basePluginTypes); @@ -147,6 +148,8 @@ private void RegisterPlugins(IEnumerable basePluginTypes, Dictionary @@ -154,7 +157,10 @@ private void RegisterPlugins(IEnumerable basePluginTypes, Dictionary basePluginTypes, Dictionary Assembly proxyTypeAssembly = pluginType.Assembly; + _logger.LogDebug("Scanning assembly {Assembly} for direct IPlugin implementations", proxyTypeAssembly.GetName().Name); + // Look for any currently loaded types in assembly that implement IPlugin var types = proxyTypeAssembly.GetLoadableTypes() - .Where(t => t.BaseType == typeof(object) && !t.IsAbstract && t.IsPublic && typeof(IPlugin).IsAssignableFrom(t)); + .Where(t => t.BaseType == typeof(object) && !t.IsAbstract && t.IsPublic && typeof(IPlugin).IsAssignableFrom(t)) + .ToList(); + + _logger.LogDebug("Found {Count} direct IPlugin type(s) in {Assembly}", types.Count, proxyTypeAssembly.GetName().Name); foreach (var type in types) { @@ -188,9 +199,12 @@ private void RegisterDirectPlugins(IEnumerable basePluginTypes, Dictionary private void RegisterPlugin(Type pluginType, Dictionary metadata, List metaPlugins, Dictionary register) { + _logger.LogDebug("Evaluating plugin type: {TypeName}", pluginType.FullName); + var plugin = _pluginInstanceCache.GetOrAdd(pluginType, Utility.CreatePluginInstance); if (plugin == null) { + _logger.LogWarning("Failed to create instance of plugin type: {TypeName}", pluginType.FullName); return; } @@ -200,17 +214,19 @@ private void RegisterPlugin(Type pluginType, Dictionary // If we didn't find any steps, try the MetadataRegistrationStrategy as a last resort if (!steps.Any()) { - steps = new MetadataRegistrationStrategy().AnalyzeType(pluginType, metaPlugins); + _logger.LogDebug("{TypeName}: no steps from primary strategies, trying MetadataRegistrationStrategy", pluginType.FullName); + steps = new MetadataRegistrationStrategy(_logger).AnalyzeType(pluginType, metaPlugins); } var triggers = steps.Select(t => new PluginTrigger(t.EventOperation, t.ExecutionStage, plugin.Execute, t, metadata)); if (!triggers.Any()) { + _logger.LogDebug("{TypeName}: no registrations found from any strategy", pluginType.FullName); missingRegistrations.Add(pluginType); return; } - + foreach (var trigger in triggers) { AddTrigger(trigger, register); diff --git a/src/XrmMockup365/Plugin/RegistrationStrategy/DAXIF/CustomApiRegistrationStrategy.cs b/src/XrmMockup365/Plugin/RegistrationStrategy/DAXIF/CustomApiRegistrationStrategy.cs index 4253ead5..df66c71b 100644 --- a/src/XrmMockup365/Plugin/RegistrationStrategy/DAXIF/CustomApiRegistrationStrategy.cs +++ b/src/XrmMockup365/Plugin/RegistrationStrategy/DAXIF/CustomApiRegistrationStrategy.cs @@ -1,7 +1,9 @@ -using DG.Tools.XrmMockup; +using DG.Tools.XrmMockup; using DG.Tools.XrmMockup.Plugin.RegistrationStrategy; using XrmPluginCore.Enums; using XrmPluginCore.Interfaces.CustomApi; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Metadata; using System; @@ -21,14 +23,24 @@ namespace DG.Tools.XrmMockup.Plugin.RegistrationStrategy.DAXIF internal class CustomApiRegistrationStrategy : IRegistrationStrategy { + private readonly ILogger _logger; + + public CustomApiRegistrationStrategy(ILogger logger = null) + { + _logger = logger ?? NullLogger.Instance; + } + public IEnumerable AnalyzeType(IPlugin plugin) { var pluginType = plugin.GetType(); if (pluginType.GetMethod("GetCustomAPIConfig") == null) { + _logger.LogDebug("[DAXIF] {TypeName}: no GetCustomAPIConfig method found, skipping", pluginType.FullName); yield break; } + _logger.LogDebug("[DAXIF] {TypeName}: found GetCustomAPIConfig method, extracting config", pluginType.FullName); + var configs = pluginType .GetMethod("GetCustomAPIConfig") .Invoke(plugin, new object[] { }) @@ -42,6 +54,8 @@ public IEnumerable AnalyzeType(IPlugin plugin) ? ownerTypeEnum : (OwnerType?)null; + _logger.LogDebug("[DAXIF] {TypeName}: registered custom API '{UniqueName}'", pluginType.FullName, configs.Item1.Item1); + yield return new CustomApiConfig { UniqueName = configs.Item1.Item1, diff --git a/src/XrmMockup365/Plugin/RegistrationStrategy/DAXIF/PluginRegistrationStrategy.cs b/src/XrmMockup365/Plugin/RegistrationStrategy/DAXIF/PluginRegistrationStrategy.cs index 3e6a4353..9c23f7c3 100644 --- a/src/XrmMockup365/Plugin/RegistrationStrategy/DAXIF/PluginRegistrationStrategy.cs +++ b/src/XrmMockup365/Plugin/RegistrationStrategy/DAXIF/PluginRegistrationStrategy.cs @@ -1,5 +1,7 @@ -using XrmPluginCore.Enums; +using XrmPluginCore.Enums; using XrmPluginCore.Interfaces.Plugin; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Xrm.Sdk; using System; using System.Collections.Generic; @@ -16,20 +18,30 @@ namespace DG.Tools.XrmMockup.Plugin.RegistrationStrategy.DAXIF internal class PluginRegistrationStrategy : IRegistrationStrategy { + private readonly ILogger _logger; + + public PluginRegistrationStrategy(ILogger logger = null) + { + _logger = logger ?? NullLogger.Instance; + } + public IEnumerable AnalyzeType(IPlugin plugin) { var pluginType = plugin.GetType(); if (pluginType.GetMethod("PluginProcessingStepConfigs") == null) { + _logger.LogDebug("[DAXIF] {TypeName}: no PluginProcessingStepConfigs method found, skipping", pluginType.FullName); return Enumerable.Empty(); } + _logger.LogDebug("[DAXIF] {TypeName}: found PluginProcessingStepConfigs method, extracting steps", pluginType.FullName); + var configs = pluginType .GetMethod("PluginProcessingStepConfigs") .Invoke(plugin, new object[] { }) as IEnumerable>>; - return configs.Select(c => + var results = configs.Select(c => { var hasImpersonatingUser = Guid.TryParse(c.Item2.Item6, out var impersonatingUserId); @@ -53,7 +65,10 @@ public IEnumerable AnalyzeType(IPlugin plugin) Attributes = i.Item4 }) }; - }); + }).ToList(); + + _logger.LogDebug("[DAXIF] {TypeName}: extracted {Count} plugin step(s)", pluginType.FullName, results.Count); + return results; } } } diff --git a/src/XrmMockup365/Plugin/RegistrationStrategy/IRegistrationStrategy.cs b/src/XrmMockup365/Plugin/RegistrationStrategy/IRegistrationStrategy.cs index 9d48e6f8..3b9ad5b3 100644 --- a/src/XrmMockup365/Plugin/RegistrationStrategy/IRegistrationStrategy.cs +++ b/src/XrmMockup365/Plugin/RegistrationStrategy/IRegistrationStrategy.cs @@ -1,4 +1,4 @@ -using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk; using System; using System.Collections.Generic; diff --git a/src/XrmMockup365/Plugin/RegistrationStrategy/MetadataRegistrationStrategy.cs b/src/XrmMockup365/Plugin/RegistrationStrategy/MetadataRegistrationStrategy.cs index 5664665d..3c57a554 100644 --- a/src/XrmMockup365/Plugin/RegistrationStrategy/MetadataRegistrationStrategy.cs +++ b/src/XrmMockup365/Plugin/RegistrationStrategy/MetadataRegistrationStrategy.cs @@ -1,5 +1,7 @@ -using XrmPluginCore.Enums; +using XrmPluginCore.Enums; using XrmPluginCore.Interfaces.Plugin; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; using System.Linq; @@ -8,6 +10,13 @@ namespace DG.Tools.XrmMockup.Plugin.RegistrationStrategy { internal class MetadataRegistrationStrategy { + private readonly ILogger _logger; + + public MetadataRegistrationStrategy(ILogger logger = null) + { + _logger = logger ?? NullLogger.Instance; + } + public IEnumerable AnalyzeType(Type pluginType, List metaPlugins) { // Retrieve registration from CRM metadata @@ -20,13 +29,21 @@ public IEnumerable AnalyzeType(Type pluginType, List x.AssemblyName == pluginType.FullName); } + var count = 0; foreach (var metaStep in metaSteps) { + count++; + _logger.LogDebug("[Metadata] {TypeName}: found metadata step '{Message}' on '{Entity}' stage={Stage}", + pluginType.FullName, metaStep.MessageName, metaStep.PrimaryEntity, metaStep.Stage); + yield return new PluginStepConfig { ExecutionStage = (ExecutionStage)metaStep.Stage, @@ -48,6 +65,11 @@ public IEnumerable AnalyzeType(Type pluginType, List() }; } + + if (count == 0) + { + _logger.LogDebug("[Metadata] {TypeName}: no matching metadata entries found", pluginType.FullName); + } } } } diff --git a/src/XrmMockup365/Plugin/RegistrationStrategy/XrmPluginCore/CustomApiRegistrationStrategy.cs b/src/XrmMockup365/Plugin/RegistrationStrategy/XrmPluginCore/CustomApiRegistrationStrategy.cs index e5bd14ff..de2d782f 100644 --- a/src/XrmMockup365/Plugin/RegistrationStrategy/XrmPluginCore/CustomApiRegistrationStrategy.cs +++ b/src/XrmMockup365/Plugin/RegistrationStrategy/XrmPluginCore/CustomApiRegistrationStrategy.cs @@ -1,5 +1,7 @@ -using XrmPluginCore; +using XrmPluginCore; using XrmPluginCore.Interfaces.CustomApi; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Xrm.Sdk; using System.Collections.Generic; @@ -7,12 +9,26 @@ namespace DG.Tools.XrmMockup.Plugin.RegistrationStrategy.XrmPluginCore { internal class CustomApiRegistrationStrategy : IRegistrationStrategy { + private readonly ILogger _logger; + + public CustomApiRegistrationStrategy(ILogger logger = null) + { + _logger = logger ?? NullLogger.Instance; + } + public IEnumerable AnalyzeType(IPlugin plugin) { if (plugin is ICustomApiDefinition customApiDefinition) { + _logger.LogDebug("[XrmPluginCore] {TypeName}: implements ICustomApiDefinition, extracting registration", + plugin.GetType().FullName); yield return customApiDefinition.GetRegistration(); } + else + { + _logger.LogDebug("[XrmPluginCore] {TypeName}: does not implement ICustomApiDefinition, skipping", + plugin.GetType().FullName); + } } } -} \ No newline at end of file +} diff --git a/src/XrmMockup365/Plugin/RegistrationStrategy/XrmPluginCore/PluginRegistrationStrategy.cs b/src/XrmMockup365/Plugin/RegistrationStrategy/XrmPluginCore/PluginRegistrationStrategy.cs index bc6896a1..c3c9d939 100644 --- a/src/XrmMockup365/Plugin/RegistrationStrategy/XrmPluginCore/PluginRegistrationStrategy.cs +++ b/src/XrmMockup365/Plugin/RegistrationStrategy/XrmPluginCore/PluginRegistrationStrategy.cs @@ -1,5 +1,7 @@ -using XrmPluginCore; +using XrmPluginCore; using XrmPluginCore.Interfaces.Plugin; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Xrm.Sdk; using System.Collections.Generic; using System.Linq; @@ -8,14 +10,25 @@ namespace DG.Tools.XrmMockup.Plugin.RegistrationStrategy.XrmPluginCore { internal class PluginRegistrationStrategy : IRegistrationStrategy { + private readonly ILogger _logger; + + public PluginRegistrationStrategy(ILogger logger = null) + { + _logger = logger ?? NullLogger.Instance; + } + public IEnumerable AnalyzeType(IPlugin plugin) { if (plugin is IPluginDefinition pluginDefinition) { - return pluginDefinition.GetRegistrations(); + var registrations = pluginDefinition.GetRegistrations().ToList(); + _logger.LogDebug("[XrmPluginCore] {TypeName}: implements IPluginDefinition, found {Count} registration(s)", + plugin.GetType().FullName, registrations.Count); + return registrations; } + _logger.LogDebug("[XrmPluginCore] {TypeName}: does not implement IPluginDefinition, skipping", plugin.GetType().FullName); return Enumerable.Empty(); } } -} \ No newline at end of file +} diff --git a/src/XrmMockup365/XrmMockupSettings.cs b/src/XrmMockup365/XrmMockupSettings.cs index df084654..4ff1572a 100644 --- a/src/XrmMockup365/XrmMockupSettings.cs +++ b/src/XrmMockup365/XrmMockupSettings.cs @@ -106,6 +106,13 @@ public class XrmMockupSettings /// public string LogFilePath { get; set; } + /// + /// Minimum log level for the built-in file logger. Default is . + /// Only applies when is set. Ignored when is provided, + /// as the custom factory controls its own filtering. + /// + public LogLevel MinLogLevel { get; set; } = LogLevel.Information; + /// /// Optional logger factory for diagnostic logging. When set, takes precedence /// over . Use this to integrate with your own logging infrastructure. diff --git a/tests/XrmMockup365Test/TestLogging.cs b/tests/XrmMockup365Test/TestLogging.cs index 5cd6fae6..159da35e 100644 --- a/tests/XrmMockup365Test/TestLogging.cs +++ b/tests/XrmMockup365Test/TestLogging.cs @@ -120,6 +120,45 @@ public void TestCustomLoggerFactoryIsUsed() Assert.True(factory.LogMessages.Count > 0, "Logger should have captured log messages"); } + [Fact] + public void TestMinLogLevelFiltersDebugMessages() + { + // Default MinLogLevel is Information, so Debug messages should be filtered out + var settings = CreateSettingsWithLogFile(out var logFilePath); + var crm = XrmMockup365.GetInstance(settings); + + var content = ReadLogFile(logFilePath); + Assert.DoesNotContain("[Debug]", content); + Assert.Contains("[Information]", content); + } + + [Fact] + public void TestDebugLogLevelIncludesDetailedMessages() + { + var logFilePath = Path.Combine(Path.GetTempPath(), $"xrmmockup_test_{Guid.NewGuid():N}.log"); + _tempFiles.Add(logFilePath); + + var settings = new XrmMockupSettings + { + BasePluginTypes = new Type[] { typeof(Plugin), typeof(PluginNonDaxif), typeof(LegacyPlugin), typeof(DIPlugin) }, + BaseCustomApiTypes = new[] { new Tuple("dg", typeof(Plugin)), new Tuple("dg", typeof(LegacyCustomApi)) }, + CodeActivityInstanceTypes = new Type[] { typeof(AccountWorkflowActivity) }, + EnableProxyTypes = true, + IncludeAllWorkflows = false, + MetadataDirectoryPath = GetMetadataPath(), + LogFilePath = logFilePath, + MinLogLevel = LogLevel.Debug + }; + + var crm = XrmMockup365.GetInstance(settings); + + var content = ReadLogFile(logFilePath); + // Debug level should produce [Debug] entries (cache-hit or cache-miss both log at Debug) + Assert.Contains("[Debug]", content); + // Should contain per-plugin registration details + Assert.Contains("Plugin", content); + } + public void Dispose() { foreach (var file in _tempFiles) From dbc6f5a7cfe9ad49efc0f26cf2927cdc9b586830 Mon Sep 17 00:00:00 2001 From: Morten Holt Date: Wed, 4 Mar 2026 09:06:02 +0100 Subject: [PATCH 3/3] CHORE: Update RELEASE_NOTES to create preview release --- RELEASE_NOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index be0f22ed..6dd66d77 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,6 @@ +### 1.17.12-preview.1 - 04 March 2026 +* Add: Added ability to log startup sequence and Plugin/API detection to a file + ### 1.17.11 - 02 March 2026 * Fix: Added validation of emailaddress on all to references in SendEmailRequest (@JonasGLund99) #313