Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
48 changes: 42 additions & 6 deletions src/XrmMockup365/Core.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
using DG.Tools.XrmMockup.Database;
using DG.Tools.XrmMockup.Internal;
using DG.Tools.XrmMockup.Logging;
using DG.Tools.XrmMockup.Serialization;
using DG.Tools.XrmMockup.Online;
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.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Xrm.Sdk.Organization;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -80,7 +84,8 @@ public Core(XrmMockupSettings Settings, MetadataSkeleton metadata, List<Entity>
BaseCurrency = metadata.BaseOrganization.GetAttributeValue<EntityReference>("basecurrencyid"),
BaseCurrencyPrecision = metadata.BaseOrganization.GetAttributeValue<int>("pricingdecimalprecision"),
OnlineDataService = null,
EntityTypeMap = new Dictionary<string, Type>()
EntityTypeMap = new Dictionary<string, Type>(),
LoggerFactory = ResolveLoggerFactory(Settings)
};

InitializeCore(initData);
Expand All @@ -100,7 +105,8 @@ public Core(XrmMockupSettings Settings, StaticMetadataCache staticCache)
BaseCurrency = staticCache.BaseCurrency,
BaseCurrencyPrecision = staticCache.BaseCurrencyPrecision,
OnlineDataService = staticCache.OnlineDataService,
EntityTypeMap = staticCache.EntityTypeMap
EntityTypeMap = staticCache.EntityTypeMap,
LoggerFactory = staticCache.LoggerFactory
};

InitializeCore(initData);
Expand All @@ -111,6 +117,10 @@ public Core(XrmMockupSettings Settings, StaticMetadataCache staticCache)
/// </summary>
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;
Expand All @@ -133,10 +143,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)
Expand All @@ -147,6 +162,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<string>() { "createdon", "createdby", "modifiedon", "modifiedby" };

RequestHandlers = GetRequestHandlers(db);
Expand Down Expand Up @@ -196,8 +219,21 @@ public static StaticMetadataCache BuildStaticMetadataCache(XrmMockupSettings set
// Note: IPluginMetadata is handled per-instance in the Core constructor
// to avoid modifying the shared cache

var loggerFactory = ResolveLoggerFactory(settings);

return new StaticMetadataCache(metadata, workflows, securityRoles, entityTypeMap,
baseCurrency, baseCurrencyPrecision, onlineDataService);
baseCurrency, baseCurrencyPrecision, onlineDataService, loggerFactory);
}

private static ILoggerFactory ResolveLoggerFactory(XrmMockupSettings settings)
{
if (settings.LoggerFactory != null)
return settings.LoggerFactory;

if (!string.IsNullOrEmpty(settings.LogFilePath))
return new FileLoggerFactory(settings.LogFilePath, settings.MinLogLevel);

return NullLoggerFactory.Instance;
}

private static IOnlineDataService BuildOnlineDataService(XrmMockupSettings settings)
Expand Down
2 changes: 2 additions & 0 deletions src/XrmMockup365/Internal/CoreInitializationData.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Generic;
Expand All @@ -18,5 +19,6 @@ internal class CoreInitializationData
public int BaseCurrencyPrecision { get; set; }
public IOnlineDataService OnlineDataService { get; set; }
public Dictionary<string, Type> EntityTypeMap { get; set; }
public ILoggerFactory LoggerFactory { get; set; }
}
}
54 changes: 54 additions & 0 deletions src/XrmMockup365/Logging/FileLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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;
private readonly LogLevel _minLogLevel;

public FileLogger(string categoryName, StreamWriter writer, object writeLock, LogLevel minLogLevel)
{
_categoryName = categoryName;
_writer = writer;
_lock = writeLock;
_minLogLevel = minLogLevel;
}

public IDisposable BeginScope<TState>(TState state) => NullScope.Instance;

public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None && logLevel >= _minLogLevel;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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);
if (exception != null)
{
_writer.WriteLine(exception.ToString());
}
_writer.Flush();
}
}

private class NullScope : IDisposable
{
public static NullScope Instance { get; } = new NullScope();
public void Dispose() { }
}
}
}
59 changes: 59 additions & 0 deletions src/XrmMockup365/Logging/FileLoggerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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 readonly LogLevel _minLogLevel;
private bool _disposed;

public FileLoggerFactory(string filePath, LogLevel minLogLevel = LogLevel.Information)
{
_minLogLevel = minLogLevel;

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, _minLogLevel);
}

public void AddProvider(ILoggerProvider provider)
{
// Not needed for this simple implementation
}

public void Dispose()
{
if (!_disposed)
{
_disposed = true;
lock (_writeLock)
{
_writer?.Flush();
_writer?.Dispose();
}
}
}
}
}
46 changes: 39 additions & 7 deletions src/XrmMockup365/Plugin/CustomApiManager.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<string, Dictionary<string, Action<MockupServiceProviderAndFactory>>> _cachedApis = new ConcurrentDictionary<string, Dictionary<string, Action<MockupServiceProviderAndFactory>>>();
Expand All @@ -23,21 +26,26 @@ internal class CustomApiManager
internal List<Type> missingRegistration = new List<Type>();
private Dictionary<string, Action<MockupServiceProviderAndFactory>> registeredApis = new Dictionary<string, Action<MockupServiceProviderAndFactory>>();

private readonly List<IRegistrationStrategy<ICustomApiConfig>> registrationStrategies = new List<IRegistrationStrategy<ICustomApiConfig>>
{
new Plugin.RegistrationStrategy.XrmPluginCore.CustomApiRegistrationStrategy(),
new Plugin.RegistrationStrategy.DAXIF.CustomApiRegistrationStrategy()
};
internal int RegisteredApiCount => registeredApis.Count;

private readonly List<IRegistrationStrategy<ICustomApiConfig>> registrationStrategies;

public CustomApiManager(IEnumerable<Tuple<string, Type>> baseCustomApiTypes)
public CustomApiManager(IEnumerable<Tuple<string, Type>> baseCustomApiTypes, ILogger logger = null)
{
_logger = logger ?? NullLogger.Instance;
registrationStrategies = new List<IRegistrationStrategy<ICustomApiConfig>>
{
new Plugin.RegistrationStrategy.XrmPluginCore.CustomApiRegistrationStrategy(_logger),
new Plugin.RegistrationStrategy.DAXIF.CustomApiRegistrationStrategy(_logger)
};
var cacheKey = GenerateApiCacheKey(baseCustomApiTypes);

// Check if we have cached results
if (_cachedApis.ContainsKey(cacheKey))
{
// Use cached results - no reflection/instantiation needed
registeredApis = new Dictionary<string, Action<MockupServiceProviderAndFactory>>(_cachedApis[cacheKey]);
_logger.LogDebug("Loaded {Count} custom API registrations from cache", registeredApis.Count);
return;
}

Expand All @@ -47,6 +55,7 @@ public CustomApiManager(IEnumerable<Tuple<string, Type>> baseCustomApiTypes)
if (_cachedApis.ContainsKey(cacheKey))
{
registeredApis = new Dictionary<string, Action<MockupServiceProviderAndFactory>>(_cachedApis[cacheKey]);
_logger.LogDebug("Loaded {Count} custom API registrations from cache", registeredApis.Count);
return;
}

Expand All @@ -59,6 +68,19 @@ public CustomApiManager(IEnumerable<Tuple<string, Type>> baseCustomApiTypes)

// Cache for future instances
_cachedApis[cacheKey] = new Dictionary<string, Action<MockupServiceProviderAndFactory>>(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);
}
}

Expand All @@ -71,10 +93,14 @@ private void RegisterCustomApis(IEnumerable<Tuple<string, Type>> 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);
Expand All @@ -84,10 +110,13 @@ private void RegisterCustomApis(IEnumerable<Tuple<string, Type>> 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;
}

Expand All @@ -97,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);
}
}

Expand Down
Loading
Loading