Skip to content
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Rnwood.Dataverse.Data.PowerShell.Model;
using System;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Text.Json;

namespace Rnwood.Dataverse.Data.PowerShell.Commands
{
/// <summary>
/// Extracts source code and build metadata from a plugin assembly created by New-DataversePluginAssembly.
/// Extracts source code and build metadata from a plugin assembly created by Set-DataverseDynamicPluginAssembly.
/// </summary>
[Cmdlet(VerbsCommon.Get, "DataverseDynamicPluginAssembly")]
[Cmdlet(VerbsCommon.Get, "DataverseDynamicPluginAssembly", DefaultParameterSetName = "ById")]
[OutputType(typeof(PSObject))]
public class GetDataverseDynamicPluginAssemblyCmdlet : PSCmdlet
public class GetDataverseDynamicPluginAssemblyCmdlet : OrganizationServiceCmdlet
{
/// <summary>
/// Gets or sets the ID of the plugin assembly to retrieve from Dataverse.
/// </summary>
[Parameter(Mandatory = true, ParameterSetName = "ById", ValueFromPipelineByPropertyName = true, HelpMessage = "ID of the plugin assembly")]
[Parameter(Mandatory = true, ParameterSetName = "VSProjectById", ValueFromPipelineByPropertyName = true, HelpMessage = "ID of the plugin assembly")]
public Guid Id { get; set; }

/// <summary>
/// Gets or sets the name of the plugin assembly to retrieve from Dataverse.
/// </summary>
[Parameter(Mandatory = true, ParameterSetName = "ByName", HelpMessage = "Name of the plugin assembly")]
[Parameter(Mandatory = true, ParameterSetName = "VSProjectByName", HelpMessage = "Name of the plugin assembly")]
public string Name { get; set; }

/// <summary>
/// Gets or sets the assembly bytes to extract from.
/// </summary>
[Parameter(Mandatory = true, ParameterSetName = "Bytes", ValueFromPipeline = true, HelpMessage = "Assembly bytes")]
[Parameter(Mandatory = true, ParameterSetName = "VSProjectFromBytes", ValueFromPipeline = true, HelpMessage = "Assembly bytes")]
public byte[] AssemblyBytes { get; set; }

/// <summary>
/// Gets or sets the path to the assembly file.
/// </summary>
[Parameter(Mandatory = true, ParameterSetName = "FilePath", HelpMessage = "Path to assembly file")]
[Parameter(Mandatory = true, ParameterSetName = "VSProjectFromFile", HelpMessage = "Path to assembly file")]
public string FilePath { get; set; }

/// <summary>
Expand All @@ -32,17 +51,81 @@ public class GetDataverseDynamicPluginAssemblyCmdlet : PSCmdlet
[Parameter(HelpMessage = "Output path for extracted source code")]
public string OutputSourceFile { get; set; }

/// <summary>
/// Gets or sets the output directory for a complete Visual Studio project.
/// </summary>
[Parameter(Mandatory = true, ParameterSetName = "VSProjectById", HelpMessage = "Output directory for Visual Studio project")]
[Parameter(Mandatory = true, ParameterSetName = "VSProjectByName", HelpMessage = "Output directory for Visual Studio project")]
[Parameter(Mandatory = true, ParameterSetName = "VSProjectFromBytes", HelpMessage = "Output directory for Visual Studio project")]
[Parameter(Mandatory = true, ParameterSetName = "VSProjectFromFile", HelpMessage = "Output directory for Visual Studio project")]
public string OutputProjectPath { get; set; }

/// <summary>
/// Initializes the cmdlet processing. Skips connection validation for parameter sets that don't need a connection.
/// </summary>
protected override void BeginProcessing()
{
// Only call base (which validates connection) for parameter sets that need it
if (ParameterSetName == "ById" || ParameterSetName == "VSProjectById" ||
ParameterSetName == "ByName" || ParameterSetName == "VSProjectByName")
{
base.BeginProcessing();
}
else
{
// For Bytes and FilePath parameter sets, skip connection validation
// Just call PSCmdlet.BeginProcessing() directly
// We can't call it directly, so we do nothing here - connection won't be needed
}
}

/// <summary>
/// Process the cmdlet.
/// </summary>
protected override void ProcessRecord()
{
base.ProcessRecord();
// Only call base.ProcessRecord() for parameter sets that need connection
if (ParameterSetName == "ById" || ParameterSetName == "VSProjectById" ||
ParameterSetName == "ByName" || ParameterSetName == "VSProjectByName")
{
base.ProcessRecord();
}

try
{
byte[] assemblyBytes = AssemblyBytes;
if (ParameterSetName == "FilePath")
byte[] assemblyBytes = null;

// Retrieve from Dataverse if using connection-based parameter sets
if (ParameterSetName == "ById" || ParameterSetName == "VSProjectById" ||
ParameterSetName == "ByName" || ParameterSetName == "VSProjectByName")
{
Entity pluginAssembly = RetrievePluginAssembly();

if (pluginAssembly == null)
{
string identifier = ParameterSetName.Contains("ById") ? Id.ToString() : Name;
WriteWarning($"Plugin assembly not found: {identifier}");
return;
}

if (!pluginAssembly.Contains("content"))
{
WriteWarning("Plugin assembly does not contain content. The assembly may not be stored in the database.");
return;
}

string base64Content = pluginAssembly.GetAttributeValue<string>("content");
assemblyBytes = Convert.FromBase64String(base64Content);

WriteVerbose($"Retrieved plugin assembly from Dataverse: {pluginAssembly.GetAttributeValue<string>("name")}");
}
// Load from bytes parameter
else if (ParameterSetName == "Bytes" || ParameterSetName == "VSProjectFromBytes")
{
assemblyBytes = AssemblyBytes;
}
// Load from file parameter
else if (ParameterSetName == "FilePath" || ParameterSetName == "VSProjectFromFile")
{
if (!File.Exists(FilePath))
{
Expand All @@ -57,12 +140,20 @@ protected override void ProcessRecord()

if (metadata == null)
{
WriteWarning("No embedded metadata found in assembly. This assembly was not created with New-DataversePluginAssembly or metadata is missing.");
WriteWarning("No embedded metadata found in assembly. This assembly was not created with Set-DataverseDynamicPluginAssembly or metadata is missing.");
return;
}

WriteVerbose($"Extracted metadata for assembly: {metadata.AssemblyName}");

// Handle VS Project generation
if (ParameterSetName == "VSProjectById" || ParameterSetName == "VSProjectByName" ||
ParameterSetName == "VSProjectFromBytes" || ParameterSetName == "VSProjectFromFile")
{
GenerateVSProject(metadata, OutputProjectPath);
return;
}

// Write source to file if requested
if (!string.IsNullOrEmpty(OutputSourceFile) && !string.IsNullOrEmpty(metadata.SourceCode))
{
Expand Down Expand Up @@ -93,6 +184,26 @@ protected override void ProcessRecord()
}
}

private Entity RetrievePluginAssembly()
{
QueryExpression query = new QueryExpression("pluginassembly")
{
ColumnSet = new ColumnSet("content", "name")
};

if (ParameterSetName == "ById" || ParameterSetName == "VSProjectById")
{
query.Criteria.AddCondition("pluginassemblyid", ConditionOperator.Equal, Id);
}
else if (ParameterSetName == "ByName" || ParameterSetName == "VSProjectByName")
{
query.Criteria.AddCondition("name", ConditionOperator.Equal, Name);
}

EntityCollection results = Connection.RetrieveMultiple(query);
return results.Entities.FirstOrDefault();
}

private PluginAssemblyMetadata ExtractMetadata(byte[] assemblyBytes)
{
try
Expand Down Expand Up @@ -139,5 +250,119 @@ private PluginAssemblyMetadata ExtractMetadata(byte[] assemblyBytes)
return null;
}
}

private void GenerateVSProject(PluginAssemblyMetadata metadata, string outputPath)
{
// Create output directory
if (!Directory.Exists(outputPath))
{
Directory.CreateDirectory(outputPath);
}

WriteVerbose($"Generating Visual Studio project in: {outputPath}");

// Write source code file
string sourceFileName = $"{metadata.AssemblyName}.cs";
string sourceFilePath = Path.Combine(outputPath, sourceFileName);
File.WriteAllText(sourceFilePath, metadata.SourceCode);
WriteVerbose($"Created source file: {sourceFileName}");

// Write strong name key file if available
string snkFileName = $"{metadata.AssemblyName}.snk";
string snkFilePath = Path.Combine(outputPath, snkFileName);
if (!string.IsNullOrEmpty(metadata.StrongNameKey))
{
byte[] keyBytes = Convert.FromBase64String(metadata.StrongNameKey);
File.WriteAllBytes(snkFilePath, keyBytes);
WriteVerbose($"Created strong name key file: {snkFileName}");
}

// Generate .csproj file
string csprojFileName = $"{metadata.AssemblyName}.csproj";
string csprojFilePath = Path.Combine(outputPath, csprojFileName);
string csprojContent = GenerateCsprojContent(metadata, sourceFileName, snkFileName);
File.WriteAllText(csprojFilePath, csprojContent);
WriteVerbose($"Created project file: {csprojFileName}");

WriteObject($"Visual Studio project generated successfully in: {outputPath}");
WriteObject($" - {sourceFileName}");
if (!string.IsNullOrEmpty(metadata.StrongNameKey))
{
WriteObject($" - {snkFileName}");
}
WriteObject($" - {csprojFileName}");
WriteVerbose($"You can now open {csprojFileName} in Visual Studio and build the project.");
}

private string GenerateCsprojContent(PluginAssemblyMetadata metadata, string sourceFileName, string snkFileName)
{
StringBuilder csproj = new StringBuilder();

csproj.AppendLine("<Project Sdk=\"Microsoft.NET.Sdk\">");
csproj.AppendLine(" <PropertyGroup>");
csproj.AppendLine(" <TargetFramework>net462</TargetFramework>");
csproj.AppendLine($" <AssemblyName>{metadata.AssemblyName}</AssemblyName>");
csproj.AppendLine($" <AssemblyVersion>{metadata.Version}</AssemblyVersion>");
csproj.AppendLine($" <FileVersion>{metadata.Version}</FileVersion>");

if (!string.IsNullOrEmpty(metadata.Culture) && metadata.Culture != "neutral")
{
csproj.AppendLine($" <NeutralLanguage>{metadata.Culture}</NeutralLanguage>");
}

// Add strong name signing if key is available
if (!string.IsNullOrEmpty(metadata.StrongNameKey))
{
csproj.AppendLine(" <SignAssembly>true</SignAssembly>");
csproj.AppendLine($" <AssemblyOriginatorKeyFile>{snkFileName}</AssemblyOriginatorKeyFile>");
}

csproj.AppendLine(" </PropertyGroup>");
csproj.AppendLine();

// Add package references
if (metadata.PackageReferences != null && metadata.PackageReferences.Count > 0)
{
csproj.AppendLine(" <ItemGroup>");
foreach (string packageRef in metadata.PackageReferences)
{
string[] parts = packageRef.Split('@');
string packageName = parts[0];
string version = parts.Length > 1 ? parts[1] : "*";
csproj.AppendLine($" <PackageReference Include=\"{packageName}\" Version=\"{version}\" />");
}
csproj.AppendLine(" </ItemGroup>");
csproj.AppendLine();
}

// Always add Microsoft.CrmSdk.CoreAssemblies as it's required for plugins
const string CrmSdkPackageName = "Microsoft.CrmSdk.CoreAssemblies";
bool hasCrmSdk = metadata.PackageReferences?.Any(p => p.StartsWith(CrmSdkPackageName, StringComparison.OrdinalIgnoreCase)) ?? false;
if (!hasCrmSdk)
{
csproj.AppendLine(" <ItemGroup>");
csproj.AppendLine($" <PackageReference Include=\"{CrmSdkPackageName}\" Version=\"9.*\" />");
csproj.AppendLine(" </ItemGroup>");
csproj.AppendLine();
}

// Add framework references if needed
if (metadata.FrameworkReferences != null && metadata.FrameworkReferences.Count > 0)
{
csproj.AppendLine(" <ItemGroup>");
foreach (string frameworkRef in metadata.FrameworkReferences)
{
// Remove .dll extension if present
string refName = Path.GetFileNameWithoutExtension(frameworkRef);
csproj.AppendLine($" <Reference Include=\"{refName}\" />");
}
csproj.AppendLine(" </ItemGroup>");
csproj.AppendLine();
}

csproj.AppendLine("</Project>");

return csproj.ToString();
}
}
}
Loading