diff --git a/src/Dataverse/Tasks/Services/MetadataTypeScanner.cs b/src/Dataverse/Tasks/Services/MetadataTypeScanner.cs
new file mode 100644
index 0000000..4209af5
--- /dev/null
+++ b/src/Dataverse/Tasks/Services/MetadataTypeScanner.cs
@@ -0,0 +1,391 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+///
+/// Reads type information from a .NET assembly using IL metadata only.
+/// No types are actually loaded into the CLR, so there are no issues with assembly binding or locking files on disk.
+///
+internal sealed class MetadataTypeScanner : IDisposable
+{
+ private readonly string _mainDllPath;
+ private readonly List _probeDirs;
+ private readonly List _disposables = new List();
+ private readonly Dictionary _readerByFile = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary _readerByAssembly = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ public MetadataTypeScanner(string mainDllPath, IEnumerable probeDirs)
+ {
+ _mainDllPath = mainDllPath;
+ _probeDirs = probeDirs
+ .Where(d => !string.IsNullOrWhiteSpace(d) && Directory.Exists(d))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+
+ public IEnumerable GetPublicTypes()
+ {
+ MetadataReader reader = OpenReaderForFile(_mainDllPath);
+ if (reader == null)
+ yield break;
+
+ foreach (var handle in reader.TypeDefinitions)
+ {
+ var typeDef = reader.GetTypeDefinition(handle);
+ if ((typeDef.Attributes & TypeAttributes.VisibilityMask) != TypeAttributes.Public)
+ continue;
+
+ yield return new ScannedType(this, reader, typeDef);
+ }
+ }
+
+
+ internal bool DerivesFromBaseType(MetadataReader reader, TypeDefinition typeDef, string baseSimpleName)
+ {
+ EntityHandle baseHandle = typeDef.BaseType;
+ int guard = 0;
+
+ while (!baseHandle.IsNil && guard++ < 200)
+ {
+ if (baseHandle.Kind == HandleKind.TypeDefinition)
+ {
+ var td = reader.GetTypeDefinition((TypeDefinitionHandle)baseHandle);
+ if (reader.GetString(td.Name) == baseSimpleName)
+ return true;
+
+ baseHandle = td.BaseType;
+ }
+ else if (baseHandle.Kind == HandleKind.TypeReference)
+ {
+ var tr = reader.GetTypeReference((TypeReferenceHandle)baseHandle);
+
+ if (reader.GetString(tr.Name) == baseSimpleName)
+ return true;
+
+ var resolved = ResolveTypeReference(reader, tr);
+ if (resolved == null)
+ return false;
+
+ reader = resolved.Value.Reader;
+ baseHandle = resolved.Value.Definition.BaseType;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ internal bool ImplementsInterface(MetadataReader reader, TypeDefinition typeDef, string interfaceNamespace, string interfaceName)
+ {
+ var visited = new HashSet(StringComparer.Ordinal);
+
+ MetadataReader currentReader = reader;
+ TypeDefinition current = typeDef;
+ int guard = 0;
+
+ while (guard++ < 200)
+ {
+ foreach (var implHandle in current.GetInterfaceImplementations())
+ {
+ EntityHandle iface = currentReader.GetInterfaceImplementation(implHandle).Interface;
+ if (InterfaceMatchesOrInherits(currentReader, iface, interfaceNamespace, interfaceName, visited))
+ return true;
+ }
+
+ EntityHandle baseHandle = current.BaseType;
+ if (baseHandle.IsNil)
+ return false;
+
+ var next = ResolveToTypeDef(currentReader, baseHandle);
+ if (next == null)
+ return false;
+
+ currentReader = next.Value.Reader;
+ current = next.Value.Definition;
+ }
+
+ return false;
+ }
+
+ private bool InterfaceMatchesOrInherits(MetadataReader reader, EntityHandle ifaceHandle, string ns, string name, HashSet visited)
+ {
+ string ifaceNs, ifaceName;
+ if (TryGetTypeName(reader, ifaceHandle, out ifaceNs, out ifaceName))
+ {
+ if (ifaceName == name && (ns == null || ifaceNs == ns))
+ return true;
+
+ if (!visited.Add(ifaceNs + "." + ifaceName))
+ return false;
+ }
+
+ // Follow interface inheritance (e.g. IMyPlugin : IPlugin).
+ var resolved = ResolveToTypeDef(reader, ifaceHandle);
+ if (resolved == null)
+ return false;
+
+ foreach (var implHandle in resolved.Value.Definition.GetInterfaceImplementations())
+ {
+ EntityHandle parent = resolved.Value.Reader.GetInterfaceImplementation(implHandle).Interface;
+ if (InterfaceMatchesOrInherits(resolved.Value.Reader, parent, ns, name, visited))
+ return true;
+ }
+
+ return false;
+ }
+
+ internal CrmRegistrationInfo ReadCrmRegistration(MetadataReader reader, TypeDefinition typeDef)
+ {
+ foreach (var caHandle in typeDef.GetCustomAttributes())
+ {
+ var ca = reader.GetCustomAttribute(caHandle);
+ if (GetAttributeTypeName(reader, ca) != "CrmPluginRegistrationAttribute")
+ continue;
+
+ try
+ {
+ CustomAttributeValue value = ca.DecodeValue(StringTypeProvider.Instance);
+
+ string name = value.FixedArguments.Length > 0 ? value.FixedArguments[0].Value as string : null;
+ string group = null;
+ foreach (var named in value.NamedArguments)
+ {
+ if (named.Name == "Group" && named.Value is string groupArg)
+ group = groupArg;
+ }
+
+ return new CrmRegistrationInfo { Name = name, Group = group };
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ private (MetadataReader Reader, TypeDefinition Definition)? ResolveToTypeDef(MetadataReader reader, EntityHandle handle)
+ {
+ if (handle.Kind == HandleKind.TypeDefinition)
+ return (reader, reader.GetTypeDefinition((TypeDefinitionHandle)handle));
+
+ if (handle.Kind == HandleKind.TypeReference)
+ return ResolveTypeReference(reader, reader.GetTypeReference((TypeReferenceHandle)handle));
+
+ return null;
+ }
+
+ private (MetadataReader Reader, TypeDefinition Definition)? ResolveTypeReference(MetadataReader reader, TypeReference tr)
+ {
+ string asmName = GetResolutionAssemblyName(reader, tr.ResolutionScope);
+ if (asmName == null)
+ return null;
+
+ string ns = reader.GetString(tr.Namespace);
+ string name = reader.GetString(tr.Name);
+
+ MetadataReader other = OpenReaderForAssembly(asmName);
+ if (other == null)
+ return null;
+
+ foreach (var handle in other.TypeDefinitions)
+ {
+ var td = other.GetTypeDefinition(handle);
+ if (other.GetString(td.Name) == name && other.GetString(td.Namespace) == ns)
+ return (other, td);
+ }
+
+ return null;
+ }
+
+ private static string GetResolutionAssemblyName(MetadataReader reader, EntityHandle scope)
+ {
+ if (scope.Kind == HandleKind.AssemblyReference)
+ return reader.GetString(reader.GetAssemblyReference((AssemblyReferenceHandle)scope).Name);
+
+ if (scope.Kind == HandleKind.TypeReference)
+ {
+ var parent = reader.GetTypeReference((TypeReferenceHandle)scope);
+ return GetResolutionAssemblyName(reader, parent.ResolutionScope);
+ }
+
+ return null;
+ }
+
+ private static bool TryGetTypeName(MetadataReader reader, EntityHandle handle, out string ns, out string name)
+ {
+ if (handle.Kind == HandleKind.TypeDefinition)
+ {
+ var td = reader.GetTypeDefinition((TypeDefinitionHandle)handle);
+ ns = reader.GetString(td.Namespace);
+ name = reader.GetString(td.Name);
+ return true;
+ }
+
+ if (handle.Kind == HandleKind.TypeReference)
+ {
+ var tr = reader.GetTypeReference((TypeReferenceHandle)handle);
+ ns = reader.GetString(tr.Namespace);
+ name = reader.GetString(tr.Name);
+ return true;
+ }
+
+ ns = null;
+ name = null;
+ return false;
+ }
+
+ private static string GetAttributeTypeName(MetadataReader reader, CustomAttribute ca)
+ {
+ switch (ca.Constructor.Kind)
+ {
+ case HandleKind.MemberReference:
+ var memberRef = reader.GetMemberReference((MemberReferenceHandle)ca.Constructor);
+ if (memberRef.Parent.Kind == HandleKind.TypeReference)
+ return reader.GetString(reader.GetTypeReference((TypeReferenceHandle)memberRef.Parent).Name);
+ if (memberRef.Parent.Kind == HandleKind.TypeDefinition)
+ return reader.GetString(reader.GetTypeDefinition((TypeDefinitionHandle)memberRef.Parent).Name);
+ return "";
+
+ case HandleKind.MethodDefinition:
+ var methodDef = reader.GetMethodDefinition((MethodDefinitionHandle)ca.Constructor);
+ return reader.GetString(reader.GetTypeDefinition(methodDef.GetDeclaringType()).Name);
+
+ default:
+ return "";
+ }
+ }
+
+ private MetadataReader OpenReaderForAssembly(string assemblyName)
+ {
+ MetadataReader cached;
+ if (_readerByAssembly.TryGetValue(assemblyName, out cached))
+ return cached;
+
+ MetadataReader reader = null;
+ foreach (var dir in _probeDirs)
+ {
+ var candidate = Path.Combine(dir, assemblyName + ".dll");
+ if (File.Exists(candidate))
+ {
+ reader = OpenReaderForFile(candidate);
+ if (reader != null)
+ break;
+ }
+ }
+
+ _readerByAssembly[assemblyName] = reader;
+ return reader;
+ }
+
+ private MetadataReader OpenReaderForFile(string path)
+ {
+ string fullPath = Path.GetFullPath(path);
+
+ MetadataReader cached;
+ if (_readerByFile.TryGetValue(fullPath, out cached))
+ return cached;
+
+ MetadataReader reader = null;
+ try
+ {
+ // Read the whole file into memory, so the dll is never locked on disk.
+ var pe = new PEReader(ImmutableArray.Create(File.ReadAllBytes(fullPath)));
+ _disposables.Add(pe);
+ if (pe.HasMetadata)
+ reader = pe.GetMetadataReader();
+ }
+ catch
+ {
+ reader = null;
+ }
+
+ _readerByFile[fullPath] = reader;
+ return reader;
+ }
+
+ public void Dispose()
+ {
+ foreach (var pe in _disposables)
+ {
+ try { pe.Dispose(); }
+ catch { }
+ }
+
+ _disposables.Clear();
+ }
+
+ private sealed class StringTypeProvider : ICustomAttributeTypeProvider
+ {
+ public static readonly StringTypeProvider Instance = new StringTypeProvider();
+
+ public string GetPrimitiveType(PrimitiveTypeCode typeCode) => typeCode.ToString();
+
+ public string GetSystemType() => "System.Type";
+
+ public string GetSZArrayType(string elementType) => elementType + "[]";
+
+ public string GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind)
+ => reader.GetString(reader.GetTypeDefinition(handle).Name);
+
+ public string GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind)
+ => reader.GetString(reader.GetTypeReference(handle).Name);
+
+ public string GetTypeFromSerializedName(string name) => name;
+
+ public PrimitiveTypeCode GetUnderlyingEnumType(string type) => PrimitiveTypeCode.Int32;
+
+ public bool IsSystemType(string type) => type == "System.Type";
+ }
+}
+
+/// A top-level public type discovered by .
+internal sealed class ScannedType
+{
+ private readonly MetadataTypeScanner _owner;
+ private readonly MetadataReader _reader;
+ private readonly TypeDefinition _definition;
+
+ internal ScannedType(MetadataTypeScanner owner, MetadataReader reader, TypeDefinition definition)
+ {
+ _owner = owner;
+ _reader = reader;
+ _definition = definition;
+
+ Namespace = reader.GetString(definition.Namespace);
+ Name = reader.GetString(definition.Name);
+ FullName = string.IsNullOrEmpty(Namespace) ? Name : Namespace + "." + Name;
+ IsAbstract = (definition.Attributes & TypeAttributes.Abstract) != 0;
+ IsInterface = (definition.Attributes & TypeAttributes.Interface) != 0;
+ }
+
+ public string Namespace { get; }
+ public string Name { get; }
+ public string FullName { get; }
+ public bool IsAbstract { get; }
+ public bool IsInterface { get; }
+
+ public bool DerivesFromBaseType(string baseSimpleName)
+ => _owner.DerivesFromBaseType(_reader, _definition, baseSimpleName);
+
+ public bool ImplementsInterface(string interfaceNamespace, string interfaceName)
+ => _owner.ImplementsInterface(_reader, _definition, interfaceNamespace, interfaceName);
+
+ public CrmRegistrationInfo TryGetCrmRegistration()
+ => _owner.ReadCrmRegistration(_reader, _definition);
+}
+
+internal sealed class CrmRegistrationInfo
+{
+ public string Name { get; set; }
+ public string Group { get; set; }
+}
diff --git a/src/Dataverse/Tasks/Tasks/EnsurePluginAssemblyDataXml.cs b/src/Dataverse/Tasks/Tasks/EnsurePluginAssemblyDataXml.cs
index 0a5dab0..2fadb1e 100644
--- a/src/Dataverse/Tasks/Tasks/EnsurePluginAssemblyDataXml.cs
+++ b/src/Dataverse/Tasks/Tasks/EnsurePluginAssemblyDataXml.cs
@@ -1,16 +1,12 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using System.Xml.Linq;
-using System.Collections.Generic;
-using System.Threading;
-#if NET6_0_OR_GREATER
-using System.Runtime.Loader;
-#endif
public sealed class EnsurePluginAssemblyDataXml : Task
{
@@ -107,51 +103,62 @@ private void GeneratePluginAssemblyData(PluginProjectInfo info, string normalize
if (!File.Exists(info.DllPath))
throw new FileNotFoundException("Build not found", info.DllPath);
- string tempDllPath = CopyDllToTempFolder(info);
+ string publicKeyToken = GetPublicKeyTokenFromPath(info.DllPath);
- HashSet probeDirs = BuildProbeDirectories(tempDllPath, info.ProjectDirectory);
- ResolveEventHandler handler = CreateAssemblyResolveHandler(probeDirs);
+ var probeDirs = new List
+ {
+ Path.GetDirectoryName(info.DllPath),
+ Path.Combine(PluginRootPath, "bin", Configuration, TargetFramework),
+ info.ProjectDirectory
+ };
- AppDomain.CurrentDomain.AssemblyResolve += handler;
+ List classList = CollectPluginClasses(info.DllPath, probeDirs);
+ if (!classList.Any())
+ throw new Exception("Plugins not found");
+
+ EnsureDirectoryForFile(info.XmlPath);
+
+ XmlDocument pluginDoc = CreatePluginAssemblyDocument(
+ info.AssemblyName,
+ info.FileVersion,
+ publicKeyToken,
+ normalizedGuid,
+ classList,
+ info.CsprojFileName,
+ info.XmlPath,
+ info.RepositoryRoot
+ );
- try
- {
- TryAddSdkAssemblyProbe(probeDirs);
-
- Assembly pluginAssembly = LoadPluginAssembly(tempDllPath, info.AssemblyName, probeDirs);
- string publicKeyToken = GetPublicKeyToken(pluginAssembly);
-
- List classList = GetPluginClassNames(pluginAssembly);
- if (!classList.Any())
- throw new Exception("Plugins not found");
-
- string xmlDir = EnsureDirectoryForFile(info.XmlPath);
-
- XmlDocument pluginDoc = CreatePluginAssemblyDocument(
- info.AssemblyName,
- info.FileVersion,
- publicKeyToken,
- normalizedGuid,
- classList,
- info.CsprojFileName,
- info.XmlPath,
- info.RepositoryRoot
- );
+ pluginDoc.Save(info.XmlPath);
- pluginDoc.Save(info.XmlPath);
+ UpsertRootComponentIntoSolutionXml(
+ info.RepositoryRoot,
+ normalizedGuid,
+ info.AssemblyName,
+ info.FileVersion,
+ publicKeyToken
+ );
+ }
- UpsertRootComponentIntoSolutionXml(
- info.RepositoryRoot,
- normalizedGuid,
- info.AssemblyName,
- info.FileVersion,
- publicKeyToken
- );
- }
- finally
+ private static List CollectPluginClasses(string dllPath, IEnumerable probeDirs)
+ {
+ var result = new List();
+
+ using (var scanner = new MetadataTypeScanner(dllPath, probeDirs))
{
- AppDomain.CurrentDomain.AssemblyResolve -= handler;
+ foreach (var type in scanner.GetPublicTypes())
+ {
+ if (type.IsInterface)
+ continue;
+ if (!type.ImplementsInterface("Microsoft.Xrm.Sdk", "IPlugin"))
+ continue;
+
+ if (!string.IsNullOrWhiteSpace(type.FullName))
+ result.Add(type.FullName);
+ }
}
+
+ return result;
}
private static string FindProjectFile(string pluginRootPath)
@@ -244,71 +251,9 @@ private string ResolvePluginDllPath(string assemblyName)
return BuildPluginDllPath(assemblyName);
}
- private HashSet BuildProbeDirectories(string dllPath, string projectDirectory)
- {
- string dllDir = Path.GetDirectoryName(dllPath);
- if (string.IsNullOrEmpty(dllDir))
- throw new Exception("dll directory not resolved");
-
- var probeDirs = new HashSet(StringComparer.OrdinalIgnoreCase);
- probeDirs.Add(dllDir);
- probeDirs.Add(Path.Combine(PluginRootPath, "bin", Configuration, TargetFramework));
- probeDirs.Add(projectDirectory);
-
- return probeDirs;
- }
-
- private static ResolveEventHandler CreateAssemblyResolveHandler(HashSet probeDirs)
+ private static string GetPublicKeyTokenFromPath(string dllPath)
{
- return (sender, args) =>
- {
- string name = null;
- try
- {
- var an = new AssemblyName(args.Name);
- name = an.Name;
- }
- catch { /* ignore */ }
-
- if (string.IsNullOrWhiteSpace(name))
- return null;
-
- foreach (var dir in probeDirs)
- {
- var candidate = Path.Combine(dir, name + ".dll");
- if (File.Exists(candidate))
- {
- try
- {
- // Load from byte array to avoid file locking
- var bytes = File.ReadAllBytes(candidate);
- return Assembly.Load(bytes);
- }
- catch { /* ignore */ }
- }
- }
- return null;
- };
- }
-
- private void TryAddSdkAssemblyProbe(HashSet probeDirs)
- {
- string sdkPath = Path.Combine(PluginRootPath, "bin", Configuration, TargetFramework, "Microsoft.Xrm.Sdk.dll");
-
- if (!File.Exists(sdkPath))
- return;
-
- TryLoadAssemblyNoThrow(sdkPath);
-
- string sdkDir = Path.GetDirectoryName(sdkPath);
-
- if (!string.IsNullOrEmpty(sdkDir))
- probeDirs.Add(sdkDir);
- }
-
- private static string GetPublicKeyToken(Assembly pluginAssembly)
- {
- byte[] token = pluginAssembly.GetName().GetPublicKeyToken();
+ byte[] token = AssemblyName.GetAssemblyName(dllPath).GetPublicKeyToken();
if (token == null || token.Length == 0)
throw new Exception("Build not signed");
@@ -316,16 +261,6 @@ private static string GetPublicKeyToken(Assembly pluginAssembly)
return BitConverter.ToString(token).Replace("-", "").ToLowerInvariant();
}
- private static List GetPluginClassNames(Assembly pluginAssembly)
- {
- return GetPluginTypesSafe(pluginAssembly)
- .Where(t => t.IsClass && t.IsPublic)
- .Where(t => ImplementsInterfaceByName(t, "Microsoft.Xrm.Sdk.IPlugin"))
- .Select(t => t.FullName)
- .Where(n => !string.IsNullOrWhiteSpace(n))
- .ToList();
- }
-
private static string EnsureDirectoryForFile(string filePath)
{
string dir = Path.GetDirectoryName(filePath);
@@ -564,7 +499,6 @@ private static string NormalizeGuidBraces(string s)
return s.Trim().Trim('{', '}');
}
-
private static string BuildRelativeDllPath(string xmlPath, string repoRoot, string assemblyName)
{
string xmlDir = Path.GetDirectoryName(xmlPath);
@@ -597,77 +531,6 @@ private static string BuildAssemblyQualifiedTypeName(string className, string as
return className + ", " + BuildAssemblyFullName(assemblyName, fileVersion, publicKeyToken);
}
- private static void TryLoadAssemblyNoThrow(string path)
- {
- try
- {
- // Load from byte array to avoid file locking
- var bytes = File.ReadAllBytes(path);
- Assembly.Load(bytes);
- }
- catch { /* ignore */ }
- }
-
- private Assembly LoadPluginAssembly(string dllPath, string assemblyName, HashSet probeDirs)
- {
- var alreadyLoaded = FindLoadedAssembly(assemblyName);
- if (alreadyLoaded != null)
- return alreadyLoaded;
-
-#if NET6_0_OR_GREATER
- try
- {
- var alc = new AssemblyLoadContext("PluginAssembly-" + Guid.NewGuid().ToString("N"), isCollectible: true);
- alc.Resolving += (context, name) =>
- {
- foreach (var dir in probeDirs)
- {
- var candidate = Path.Combine(dir, name.Name + ".dll");
- if (File.Exists(candidate))
- return context.LoadFromAssemblyPath(candidate);
- }
- return null;
- };
-
- var bytes = File.ReadAllBytes(dllPath);
- var asm = alc.LoadFromStream(new MemoryStream(bytes));
- return asm;
- }
- catch (FileLoadException)
- {
- var loaded = FindLoadedAssembly(assemblyName);
- if (loaded != null)
- return loaded;
- throw;
- }
-#else
- try
- {
- // Load from byte array to avoid file locking
- var bytes = File.ReadAllBytes(dllPath);
- return Assembly.Load(bytes);
- }
- catch (FileLoadException)
- {
- var loaded = FindLoadedAssembly(assemblyName);
- if (loaded != null)
- return loaded;
- throw;
- }
-#endif
- }
-
- private static Assembly FindLoadedAssembly(string assemblyName)
- {
- return AppDomain.CurrentDomain
- .GetAssemblies()
- .FirstOrDefault(a =>
- {
- var name = a.GetName();
- return name != null && string.Equals(name.Name, assemblyName, StringComparison.OrdinalIgnoreCase);
- });
- }
-
private static string NormalizeGuid(string guidText)
{
if (string.IsNullOrWhiteSpace(guidText))
@@ -706,67 +569,6 @@ private static Tuple ReadProjectMetadata(string csprojPath, stri
return Tuple.Create(assemblyName, fileVersion);
}
- private static IEnumerable GetPluginTypesSafe(Assembly asm)
- {
- try
- {
- return asm.GetTypes();
- }
- catch (ReflectionTypeLoadException rtle)
- {
- return rtle.Types.Where(t => t != null).Cast();
- }
- }
-
- private static bool ImplementsInterfaceByName(Type t, string interfaceFullName)
- {
- try
- {
- return t.GetInterfaces().Any(i => string.Equals(i.FullName, interfaceFullName, StringComparison.Ordinal));
- }
- catch
- {
- return false;
- }
- }
-
- private static Dictionary LoadExistingPluginTypeMap(string xmlPath, XmlDocument targetDoc)
- {
- var result = new Dictionary(StringComparer.Ordinal);
-
- if (!File.Exists(xmlPath))
- return result;
-
- var existingDoc = new XmlDocument();
- existingDoc.Load(xmlPath);
-
- var pluginTypesNode = existingDoc.SelectSingleNode("//PluginAssembly/PluginTypes") as XmlElement;
- if (pluginTypesNode == null)
- return result;
-
- foreach (var node in pluginTypesNode.ChildNodes)
- {
- var el = node as XmlElement;
- if (el == null)
- continue;
-
- if (!string.Equals(el.Name, "PluginType", StringComparison.Ordinal))
- continue;
-
- string className = GetPluginTypeClassName(el);
- if (string.IsNullOrWhiteSpace(className))
- continue;
-
- if (result.ContainsKey(className))
- continue;
-
- var imported = (XmlElement)targetDoc.ImportNode(el, true);
- result[className] = imported;
- }
-
- return result;
- }
-
private static string GetPluginTypeClassName(XmlElement pluginTypeElement)
{
string aqn = pluginTypeElement.GetAttribute("AssemblyQualifiedName");
@@ -790,34 +592,6 @@ private static XmlElement CreatePluginTypeElement(XmlDocument doc)
return doc.CreateElement("PluginType");
}
- private string CopyDllToTempFolder(PluginProjectInfo info)
- {
- string tempDir = Path.Combine(
- info.RepositoryRoot,
- "obj",
- Configuration,
- TargetFramework,
- "Temp"
- );
-
- Directory.CreateDirectory(tempDir);
-
- string tempDllPath = Path.Combine(tempDir, info.AssemblyName + ".dll");
- File.Copy(info.DllPath, tempDllPath, true);
-
- return tempDllPath;
- }
-
- private static void CopyPluginAssembly(PluginProjectInfo info)
- {
- string destDir = Path.GetDirectoryName(info.XmlPath);
- if (string.IsNullOrEmpty(destDir))
- throw new Exception("PluginAssembly data directory not resolved");
-
- string destPath = Path.Combine(destDir, info.AssemblyName + ".dll");
- File.Copy(info.DllPath, destPath, true);
- }
-
private sealed class PluginProjectInfo
{
public string RepositoryRoot { get; set; } = "";
diff --git a/src/Dataverse/Tasks/Tasks/EnsureWorkflowActivityAssemblyDataXml.cs b/src/Dataverse/Tasks/Tasks/EnsureWorkflowActivityAssemblyDataXml.cs
index 72ca958..236e589 100644
--- a/src/Dataverse/Tasks/Tasks/EnsureWorkflowActivityAssemblyDataXml.cs
+++ b/src/Dataverse/Tasks/Tasks/EnsureWorkflowActivityAssemblyDataXml.cs
@@ -1,16 +1,12 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using System.Xml.Linq;
-using System.Collections.Generic;
-using System.Threading;
-#if NET6_0_OR_GREATER
-using System.Runtime.Loader;
-#endif
public sealed class EnsureWorkflowActivityAssemblyDataXml : Task
{
@@ -108,51 +104,83 @@ private void GenerateWorkflowActivityAssemblyData(WorkflowActivityProjectInfo in
if (!File.Exists(info.DllPath))
throw new FileNotFoundException("Build not found", info.DllPath);
- string tempDllPath = CopyDllToTempFolder(info);
+ string publicKeyToken = GetPublicKeyTokenFromPath(info.DllPath);
- HashSet probeDirs = BuildProbeDirectories(tempDllPath, info.ProjectDirectory);
- ResolveEventHandler handler = CreateAssemblyResolveHandler(probeDirs);
+ var probeDirs = new List
+ {
+ Path.GetDirectoryName(info.DllPath),
+ Path.Combine(WorkflowActivityRootPath, "bin", Configuration, TargetFramework),
+ info.ProjectDirectory
+ };
- AppDomain.CurrentDomain.AssemblyResolve += handler;
+ List classList = CollectWorkflowActivityClasses(info, probeDirs);
- try
- {
- TryAddSdkAssemblyProbe(probeDirs);
-
- Assembly workflowActivityAssembly = LoadWorkflowActivityAssembly(tempDllPath, info.AssemblyName, probeDirs);
- string publicKeyToken = GetPublicKeyToken(workflowActivityAssembly);
-
- List classList = GetWorkflowActivityClassInfos(workflowActivityAssembly, info.FileVersion);
- if (!classList.Any())
- throw new Exception("WorkflowActivities not found in assembly " + info.AssemblyName);
-
- string xmlDir = EnsureDirectoryForFile(info.XmlPath);
-
- XmlDocument workflowActivityDoc = CreateWorkflowActivityAssemblyDocument(
- info.AssemblyName,
- info.FileVersion,
- publicKeyToken,
- normalizedGuid,
- classList,
- info.CsprojFileName,
- info.XmlPath,
- info.RepositoryRoot
- );
+ if (!classList.Any())
+ throw new Exception("WorkflowActivities not found in assembly " + info.AssemblyName);
- workflowActivityDoc.Save(info.XmlPath);
+ EnsureDirectoryForFile(info.XmlPath);
- UpsertRootComponentIntoSolutionXml(
- info.RepositoryRoot,
- normalizedGuid,
- info.AssemblyName,
- info.FileVersion,
- publicKeyToken
- );
- }
- finally
+ XmlDocument workflowActivityDoc = CreateWorkflowActivityAssemblyDocument(
+ info.AssemblyName,
+ info.FileVersion,
+ publicKeyToken,
+ normalizedGuid,
+ classList,
+ info.CsprojFileName,
+ info.XmlPath,
+ info.RepositoryRoot
+ );
+
+ workflowActivityDoc.Save(info.XmlPath);
+
+ UpsertRootComponentIntoSolutionXml(
+ info.RepositoryRoot,
+ normalizedGuid,
+ info.AssemblyName,
+ info.FileVersion,
+ publicKeyToken
+ );
+ }
+
+ private List CollectWorkflowActivityClasses(WorkflowActivityProjectInfo info, IEnumerable probeDirs)
+ {
+ var result = new List();
+
+ using (var scanner = new MetadataTypeScanner(info.DllPath, probeDirs))
{
- AppDomain.CurrentDomain.AssemblyResolve -= handler;
+ foreach (var type in scanner.GetPublicTypes())
+ {
+ // A workflow activity is a concrete public class deriving from System.Activities.CodeActivity.
+ if (type.IsInterface || type.IsAbstract)
+ continue;
+ if (!type.DerivesFromBaseType("CodeActivity"))
+ continue;
+
+ string displayName = type.Name;
+ string groupBase = !string.IsNullOrWhiteSpace(DefaultWorkflowActivityGroupName)
+ ? DefaultWorkflowActivityGroupName
+ : info.AssemblyName;
+ string groupName = groupBase + " (" + info.FileVersion + ")";
+
+ var registration = type.TryGetCrmRegistration();
+ if (registration != null)
+ {
+ if (!string.IsNullOrWhiteSpace(registration.Name))
+ displayName = registration.Name;
+ if (!string.IsNullOrWhiteSpace(registration.Group))
+ groupName = registration.Group + " (" + info.FileVersion + ")";
+ }
+
+ result.Add(new WorkflowActivityTypeInfo
+ {
+ FullName = type.FullName,
+ DisplayName = displayName,
+ GroupName = groupName
+ });
+ }
}
+
+ return result;
}
private static string FindProjectFile(string rootPath)
@@ -245,122 +273,9 @@ private string ResolveWorkflowActivityDllPath(string assemblyName)
return BuildWorkflowActivityDllPath(assemblyName);
}
- private HashSet BuildProbeDirectories(string dllPath, string projectDirectory)
- {
- string dllDir = Path.GetDirectoryName(dllPath);
- if (string.IsNullOrEmpty(dllDir))
- throw new Exception("dll directory not resolved");
-
- var probeDirs = new HashSet(StringComparer.OrdinalIgnoreCase);
- probeDirs.Add(dllDir);
- probeDirs.Add(Path.Combine(WorkflowActivityRootPath, "bin", Configuration, TargetFramework));
- probeDirs.Add(projectDirectory);
-
- return probeDirs;
- }
-
- private static ResolveEventHandler CreateAssemblyResolveHandler(HashSet probeDirs)
+ private static string GetPublicKeyTokenFromPath(string dllPath)
{
- return (sender, args) =>
- {
- string name = null;
- try
- {
- var an = new AssemblyName(args.Name);
- name = an.Name;
- }
- catch { /* ignore */ }
-
- if (string.IsNullOrWhiteSpace(name))
- return null;
-
- foreach (var dir in probeDirs)
- {
- var candidate = Path.Combine(dir, name + ".dll");
- if (File.Exists(candidate))
- {
- try
- {
- var bytes = File.ReadAllBytes(candidate);
- return Assembly.Load(bytes);
- }
- catch { /* ignore */ }
- }
- }
- return null;
- };
- }
-
- private void TryAddSdkAssemblyProbe(HashSet probeDirs)
- {
- string sdkPath = Path.Combine(WorkflowActivityRootPath, "bin", Configuration, TargetFramework, "Microsoft.Xrm.Sdk.dll");
-
- if (!File.Exists(sdkPath))
- return;
-
- TryLoadAssemblyNoThrow(sdkPath);
-
- string sdkDir = Path.GetDirectoryName(sdkPath);
-
- if (!string.IsNullOrEmpty(sdkDir))
- probeDirs.Add(sdkDir);
-
- // Add .NET Framework reference assemblies for System.Activities
- TryAddFrameworkAssemblyProbe(probeDirs);
- }
-
- private void TryAddFrameworkAssemblyProbe(HashSet probeDirs)
- {
- // Probe the project's own build output directory for framework assemblies.
- // After dotnet build/restore, NuGet reference assemblies (including System.Activities)
- // are resolved into the output folder. This works cross-platform without hardcoded paths.
- // The workflow activity project's bin folder may contain System.Activities
- // from NuGet reference assemblies after restore
- var buildOutputDir = Path.Combine(WorkflowActivityRootPath, "bin", Configuration, TargetFramework);
- var possiblePaths = new List();
- if (Directory.Exists(buildOutputDir))
- possiblePaths.Add(buildOutputDir);
-
- foreach (var path in possiblePaths)
- {
- if (Directory.Exists(path))
- {
- probeDirs.Add(path);
- // Force load System.Activities before loading the workflow assembly
- string systemActivitiesPath = Path.Combine(path, "System.Activities.dll");
- if (File.Exists(systemActivitiesPath))
- {
- ForceLoadAssembly(systemActivitiesPath);
- break; // Only load from first found location
- }
- }
- }
- }
-
- private void ForceLoadAssembly(string path)
- {
- try
- {
- // First check if already loaded
- var name = AssemblyName.GetAssemblyName(path);
- var existing = AppDomain.CurrentDomain.GetAssemblies()
- .FirstOrDefault(a => string.Equals(a.GetName().Name, name.Name, StringComparison.OrdinalIgnoreCase));
-
- if (existing != null)
- return;
-
- // Load from GAC/framework path - this ensures proper binding
- Assembly.LoadFrom(path);
- }
- catch (Exception ex)
- {
- Log.LogMessage(MessageImportance.Low, "Failed to load " + path + ": " + ex.Message);
- }
- }
-
- private static string GetPublicKeyToken(Assembly assembly)
- {
- byte[] token = assembly.GetName().GetPublicKeyToken();
+ byte[] token = AssemblyName.GetAssemblyName(dllPath).GetPublicKeyToken();
if (token == null || token.Length == 0)
throw new Exception("Build not signed");
@@ -368,114 +283,6 @@ private static string GetPublicKeyToken(Assembly assembly)
return BitConverter.ToString(token).Replace("-", "").ToLowerInvariant();
}
- private List GetWorkflowActivityClassInfos(Assembly assembly, string fileVersion)
- {
- var result = new List();
-
- foreach (var type in GetTypesSafe(assembly))
- {
- if (!type.IsClass || !type.IsPublic || type.IsAbstract)
- continue;
-
- if (!InheritsFromByName(type, "System.Activities.CodeActivity"))
- continue;
-
- string fullName = type.FullName;
- if (string.IsNullOrWhiteSpace(fullName))
- continue;
-
- string groupName = GetWorkflowActivityGroupName(type, fileVersion);
- string displayName = GetWorkflowActivityDisplayName(type);
-
- result.Add(new WorkflowActivityTypeInfo
- {
- FullName = fullName,
- GroupName = groupName,
- DisplayName = displayName
- });
- }
-
- return result;
- }
-
- private string GetWorkflowActivityGroupName(Type type, string fileVersion)
- {
- // Try to get from CrmPluginRegistrationAttribute (Group parameter)
- foreach (var attr in type.GetCustomAttributesData())
- {
- if (attr.AttributeType.Name == "CrmPluginRegistrationAttribute")
- {
- // Look for Group named argument
- foreach (var namedArg in attr.NamedArguments)
- {
- if (namedArg.MemberName == "Group" && namedArg.TypedValue.Value is string groupValue)
- {
- if (!string.IsNullOrWhiteSpace(groupValue))
- return groupValue + " (" + fileVersion + ")";
- }
- }
- }
- }
-
- // Fallback to DefaultWorkflowActivityGroupName or assembly name
- string baseName = !string.IsNullOrWhiteSpace(DefaultWorkflowActivityGroupName)
- ? DefaultWorkflowActivityGroupName
- : type.Assembly.GetName().Name;
-
- return baseName + " (" + fileVersion + ")";
- }
-
- private static string GetWorkflowActivityDisplayName(Type type)
- {
- // Try to get from CrmPluginRegistrationAttribute (Name parameter)
- foreach (var attr in type.GetCustomAttributesData())
- {
- if (attr.AttributeType.Name == "CrmPluginRegistrationAttribute")
- {
- // First constructor argument is usually the Name
- if (attr.ConstructorArguments.Count > 0)
- {
- var nameValue = attr.ConstructorArguments[0].Value as string;
- if (!string.IsNullOrWhiteSpace(nameValue))
- return nameValue;
- }
- }
- }
-
- // Fallback to class name
- return type.Name;
- }
-
- private static bool InheritsFromByName(Type t, string baseClassName)
- {
- try
- {
- Type current = t.BaseType;
- while (current != null)
- {
- // Check by FullName
- if (string.Equals(current.FullName, baseClassName, StringComparison.Ordinal))
- return true;
-
- // Also check by Name only (in case namespace differs)
- string simpleClassName = baseClassName;
- int lastDot = baseClassName.LastIndexOf('.');
- if (lastDot >= 0)
- simpleClassName = baseClassName.Substring(lastDot + 1);
-
- if (string.Equals(current.Name, simpleClassName, StringComparison.Ordinal))
- return true;
-
- current = current.BaseType;
- }
- }
- catch
- {
- // If we can't inspect the type hierarchy, return false
- }
- return false;
- }
-
private static string EnsureDirectoryForFile(string filePath)
{
string dir = Path.GetDirectoryName(filePath);
@@ -752,85 +559,6 @@ private static string BuildAssemblyQualifiedTypeName(string className, string as
return className + ", " + BuildAssemblyFullName(assemblyName, fileVersion, publicKeyToken);
}
- private static void TryLoadAssemblyNoThrow(string path)
- {
- try
- {
- var bytes = File.ReadAllBytes(path);
- Assembly.Load(bytes);
- }
- catch { /* ignore */ }
- }
-
- private Assembly LoadWorkflowActivityAssembly(string dllPath, string assemblyName, HashSet probeDirs)
- {
- // Always load fresh copy - don't reuse cached assemblies that may have been loaded
- // without proper dependencies (causes intermittent ReflectionTypeLoadException)
-
-#if NET6_0_OR_GREATER
- try
- {
- var alc = new AssemblyLoadContext("WorkflowActivityAssembly-" + Guid.NewGuid().ToString("N"), isCollectible: true);
- alc.Resolving += (context, name) =>
- {
- foreach (var dir in probeDirs)
- {
- var candidate = Path.Combine(dir, name.Name + ".dll");
- if (File.Exists(candidate))
- return context.LoadFromAssemblyPath(candidate);
- }
- return null;
- };
-
- var bytes = File.ReadAllBytes(dllPath);
- var asm = alc.LoadFromStream(new MemoryStream(bytes));
- return asm;
- }
- catch (FileLoadException)
- {
- // Fallback to already loaded if fresh load fails
- var loaded = FindLoadedAssembly(assemblyName);
- if (loaded != null)
- return loaded;
- throw;
- }
-#else
- try
- {
- // For .NET Framework, use Assembly.LoadFile to load into a separate context
- // This avoids reusing cached assemblies that may be incomplete
- return Assembly.LoadFile(dllPath);
- }
- catch (FileLoadException)
- {
- // Fallback to byte array loading
- try
- {
- var bytes = File.ReadAllBytes(dllPath);
- return Assembly.Load(bytes);
- }
- catch
- {
- var loaded = FindLoadedAssembly(assemblyName);
- if (loaded != null)
- return loaded;
- throw;
- }
- }
-#endif
- }
-
- private static Assembly FindLoadedAssembly(string assemblyName)
- {
- return AppDomain.CurrentDomain
- .GetAssemblies()
- .FirstOrDefault(a =>
- {
- var name = a.GetName();
- return name != null && string.Equals(name.Name, assemblyName, StringComparison.OrdinalIgnoreCase);
- });
- }
-
private static string NormalizeGuid(string guidText)
{
if (string.IsNullOrWhiteSpace(guidText))
@@ -869,55 +597,6 @@ private static Tuple ReadProjectMetadata(string csprojPath, stri
return Tuple.Create(assemblyName, fileVersion);
}
- private static IEnumerable GetTypesSafe(Assembly asm)
- {
- try
- {
- return asm.GetTypes();
- }
- catch (ReflectionTypeLoadException rtle)
- {
- return rtle.Types.Where(t => t != null).Cast();
- }
- }
-
- private static Dictionary LoadExistingPluginTypeMap(string xmlPath, XmlDocument targetDoc)
- {
- var result = new Dictionary(StringComparer.Ordinal);
-
- if (!File.Exists(xmlPath))
- return result;
-
- var existingDoc = new XmlDocument();
- existingDoc.Load(xmlPath);
-
- var pluginTypesNode = existingDoc.SelectSingleNode("//PluginAssembly/PluginTypes") as XmlElement;
- if (pluginTypesNode == null)
- return result;
-
- foreach (var node in pluginTypesNode.ChildNodes)
- {
- var el = node as XmlElement;
- if (el == null)
- continue;
-
- if (!string.Equals(el.Name, "PluginType", StringComparison.Ordinal))
- continue;
-
- string className = GetPluginTypeClassName(el);
- if (string.IsNullOrWhiteSpace(className))
- continue;
-
- if (result.ContainsKey(className))
- continue;
-
- var imported = (XmlElement)targetDoc.ImportNode(el, true);
- result[className] = imported;
- }
-
- return result;
- }
-
private static string GetPluginTypeClassName(XmlElement pluginTypeElement)
{
string aqn = pluginTypeElement.GetAttribute("AssemblyQualifiedName");
@@ -936,24 +615,6 @@ private static XmlElement CreatePluginTypeElement(XmlDocument doc)
return doc.CreateElement("PluginType");
}
- private string CopyDllToTempFolder(WorkflowActivityProjectInfo info)
- {
- string tempDir = Path.Combine(
- info.RepositoryRoot,
- "obj",
- Configuration,
- TargetFramework,
- "Temp"
- );
-
- Directory.CreateDirectory(tempDir);
-
- string tempDllPath = Path.Combine(tempDir, info.AssemblyName + ".dll");
- File.Copy(info.DllPath, tempDllPath, true);
-
- return tempDllPath;
- }
-
private sealed class WorkflowActivityProjectInfo
{
public string RepositoryRoot { get; set; } = "";