diff --git a/src/RoslynTestKit/Helpers/AdhocWorkspace.cs b/src/RoslynTestKit/Helpers/AdhocWorkspace.cs
index 41091e5..f21fcde 100644
--- a/src/RoslynTestKit/Helpers/AdhocWorkspace.cs
+++ b/src/RoslynTestKit/Helpers/AdhocWorkspace.cs
@@ -18,8 +18,8 @@ namespace RoslynTestKit
public sealed class AdhocWorkspace : Workspace
{
#if NET8_0_OR_GREATER
- private static MethodInfo? _cachedCreateMethod;
- private static ParameterInfo[]? _cachedParameters;
+ private static readonly object _reflectionLock = new object();
+ private static (MethodInfo Method, ParameterInfo[] Parameters)? _cachedCreateMethodInfo;
#endif
public AdhocWorkspace(HostServices host, string workspaceKind = "Custom")
@@ -80,8 +80,6 @@ public Solution AddSolution(SolutionInfo solutionInfo)
#if NET8_0_OR_GREATER
public Project? AddProject(string name, string language)
{
- // There's a binary compatibility issue caused by a breaking change in the ProjectInfo.Create method signature between version v17.0.28.6483 and v17.0.28.26016 of Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces.dll.
- // hence we use reflection to call the method in a way that works for both versions
var info = CreateProjectInfoViaReflection(name, language);
return AddProject(info);
}
@@ -90,74 +88,106 @@ public Solution AddSolution(SolutionInfo solutionInfo)
/// Creates a ProjectInfo instance using reflection to handle different versions
/// of Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces that have different method signatures.
///
+ ///
+ /// This method is necessary because the ProjectInfo.Create method signature changed between
+ /// version v17.0.28.6483 and v17.0.28.26016 of Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces.dll.
+ /// By using reflection, we can call the method regardless of which version is loaded at runtime.
+ ///
private static ProjectInfo CreateProjectInfoViaReflection(string name, string language)
{
- if (_cachedCreateMethod == null)
+ var (method, parameters) = GetCachedCreateMethod();
+ var args = BuildMethodArguments(parameters, name, language);
+
+ var result = method.Invoke(null, args);
+ if (result is not ProjectInfo projectInfo)
+ {
+ throw new InvalidOperationException("ProjectInfo.Create did not return a ProjectInfo instance.");
+ }
+
+ return projectInfo;
+ }
+
+ ///
+ /// Gets the cached MethodInfo and ParameterInfo for ProjectInfo.Create, initializing if necessary.
+ /// Thread-safe using double-checked locking pattern.
+ ///
+ private static (MethodInfo Method, ParameterInfo[] Parameters) GetCachedCreateMethod()
+ {
+ var cached = _cachedCreateMethodInfo;
+ if (cached != null)
{
- // Find the Create method - there should be only one static Create method
- _cachedCreateMethod = typeof(ProjectInfo)
+ return cached.Value;
+ }
+
+ lock (_reflectionLock)
+ {
+ cached = _cachedCreateMethodInfo;
+ if (cached != null)
+ {
+ return cached.Value;
+ }
+
+ var createMethod = typeof(ProjectInfo)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m => m.Name == "Create")
- ?? throw new InvalidOperationException("Could not find ProjectInfo.Create method via reflection.");
+ ?? throw new InvalidOperationException(
+ "Could not find ProjectInfo.Create method. Ensure Microsoft.Dynamics.Nav.CodeAnalysis.Workspaces is referenced.");
- _cachedParameters = _cachedCreateMethod.GetParameters();
+ var result = (createMethod, createMethod.GetParameters());
+ _cachedCreateMethodInfo = result;
+ return result;
}
+ }
- var parameters = _cachedParameters!;
+ ///
+ /// Builds the argument array for calling ProjectInfo.Create via reflection.
+ ///
+ private static object?[] BuildMethodArguments(ParameterInfo[] parameters, string name, string language)
+ {
var args = new object?[parameters.Length];
- // The first 5 parameters are always: id, version, name, assemblyName, language (required)
- // All other parameters are optional - use their default values via ParameterInfo.DefaultValue
for (int i = 0; i < parameters.Length; i++)
{
- var param = parameters[i];
- var paramName = param.Name?.ToLowerInvariant();
-
- // Handle the 5 required parameters explicitly
- if (paramName == "id")
- {
- args[i] = ProjectId.CreateNewId();
- }
- else if (paramName == "version")
- {
- args[i] = VersionStamp.Create();
- }
- else if (paramName == "name")
- {
- args[i] = name;
- }
- else if (paramName == "assemblyname")
- {
- args[i] = name;
- }
- else if (paramName == "language")
- {
- args[i] = language;
- }
- // For optional parameters, use their default value or appropriate fallback
- else if (param.HasDefaultValue)
- {
- args[i] = param.DefaultValue;
- }
- else if (!param.ParameterType.IsValueType)
- {
- // Reference types without default - use null
- args[i] = null;
- }
- else
- {
- // Value types without default - use default(T)
- args[i] = Activator.CreateInstance(param.ParameterType);
- }
+ args[i] = GetParameterValue(parameters[i], name, language);
}
- var result = _cachedCreateMethod.Invoke(null, args);
- if (result is not ProjectInfo projectInfo)
+ return args;
+ }
+
+ ///
+ /// Determines the value to pass for a given parameter of ProjectInfo.Create.
+ ///
+ private static object? GetParameterValue(ParameterInfo parameter, string name, string language)
+ {
+ var paramName = parameter.Name?.ToLowerInvariant() ?? string.Empty;
+
+ // Handle the required parameters (first 5 in the method signature)
+ return paramName switch
{
- throw new InvalidOperationException("ProjectInfo.Create did not return a ProjectInfo instance.");
+ "id" => ProjectId.CreateNewId(),
+ "version" => VersionStamp.Create(),
+ "name" => name,
+ "assemblyname" => name,
+ "language" => language,
+ // For optional parameters, use their declared default value or an appropriate fallback
+ _ => GetDefaultParameterValue(parameter)
+ };
+ }
+
+ ///
+ /// Gets the default value for an optional parameter.
+ ///
+ private static object? GetDefaultParameterValue(ParameterInfo parameter)
+ {
+ if (parameter.HasDefaultValue)
+ {
+ return parameter.DefaultValue;
}
- return projectInfo;
+ // Fallback for parameters without explicit defaults
+ return parameter.ParameterType.IsValueType
+ ? Activator.CreateInstance(parameter.ParameterType)
+ : null;
}
#endif