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
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,235 @@ public void DoSomething() { }
xpc3002Diagnostics.Should().BeEmpty("XPC3002 should NOT be reported when modern API is also present");
}

[Fact]
public void Should_Generate_Types_Even_When_Handler_Method_Not_Found()
{
// Arrange - method reference points to NonExistentMethod but types should still be generated
// This enables a better DX where developers can create the method with correct signature
// using the generated PreImage/PostImage types
const string pluginSource = """

using XrmPluginCore;
using XrmPluginCore.Enums;
using Microsoft.Extensions.DependencyInjection;
using TestNamespace;

namespace TestNamespace
{
public class TestPlugin : Plugin
{
public TestPlugin()
{
RegisterStep<Account, ITestService>(EventOperation.Update, ExecutionStage.PostOperation,
service => service.NonExistentMethod)
.WithPreImage(x => x.Name);
}

protected override IServiceCollection OnBeforeBuildServiceProvider(IServiceCollection services)
{
return services.AddScoped<ITestService, TestService>();
}
}

public interface ITestService
{
void Process(); // Different method, NonExistentMethod doesn't exist
}

public class TestService : ITestService
{
public void Process() { }
}
}
""";

var source = TestFixtures.GetCompleteSource(pluginSource);

// Act
var result = GeneratorTestHelper.RunGenerator(
CompilationHelper.CreateCompilation(source));

// Assert - Types should be generated even though handler method doesn't exist
result.GeneratedTrees.Should().NotBeEmpty(
"PreImage/PostImage types should be generated even when handler method doesn't exist");

// Verify PreImage class is generated
var generatedSource = result.GeneratedTrees.First().ToString();
generatedSource.Should().Contain("public sealed class PreImage",
"PreImage class should be generated to allow developers to create the handler method with correct signature");
}

[Fact]
public void Should_Generate_Types_Even_When_Handler_Method_Wrong_Signature()
{
// Arrange - method reference points to MethodWithoutImage but types should still be generated
// This enables a better DX where developers can create the method with correct signature
// using the generated PreImage/PostImage types
const string pluginSource = """

using XrmPluginCore;
using XrmPluginCore.Enums;
using Microsoft.Extensions.DependencyInjection;
using TestNamespace;

namespace TestNamespace
{
public class TestPlugin : Plugin
{
public TestPlugin()
{
RegisterStep<Account, ITestService>(EventOperation.Update, ExecutionStage.PostOperation,
service => service.MethodWithoutImage)
.WithPreImage(x => x.Name);
}

protected override IServiceCollection OnBeforeBuildServiceProvider(IServiceCollection services)
{
return services.AddScoped<ITestService, TestService>();
}
}

public interface ITestService
{
void Process(); // Different method
void MethodWithoutImage(); // Method exists but wrong signature (missing PreImage parameter)
}

public class TestService : ITestService
{
public void Process() { }
public void MethodWithoutImage() { }
}
}
""";

var source = TestFixtures.GetCompleteSource(pluginSource);

// Act
var result = GeneratorTestHelper.RunGenerator(
CompilationHelper.CreateCompilation(source));

// Assert - Types should be generated even though handler method doesn't exist
result.GeneratedTrees.Should().NotBeEmpty(
"PreImage/PostImage types should be generated even when handler method doesn't exist");

// Verify PreImage class is generated
var generatedSource = result.GeneratedTrees.First().ToString();
generatedSource.Should().Contain("public sealed class PreImage",
"PreImage class should be generated to allow developers to create the handler method with correct signature");

generatedSource.Should().Contain("namespace TestNamespace.PluginRegistrations.TestPlugin.AccountUpdatePostOperation",
"Generated types should be in the correct namespace");
}

[Fact]
public void Should_Generate_Unique_Files_For_Same_Named_Plugins_In_Different_Namespaces()
{
// Arrange - Two plugins with the same class name but in different namespaces
// Both register the same entity/operation/stage combination
// Previously this would cause a hint name collision
// Note: We don't use GetCompleteSource here because it strips namespaces
const string source = """
using System;
using Microsoft.Xrm.Sdk;
using XrmPluginCore;
using XrmPluginCore.Enums;
using Microsoft.Extensions.DependencyInjection;
using XrmPluginCore.Tests.Context.BusinessDomain;

namespace Namespace1
{
public class AccountPlugin : Plugin
{
public AccountPlugin()
{
RegisterStep<Account, ITestService>(EventOperation.Update, ExecutionStage.PostOperation,
service => service.HandleUpdate)
.WithPreImage(x => x.Name);
}

protected override IServiceCollection OnBeforeBuildServiceProvider(IServiceCollection services)
{
return services.AddScoped<ITestService, TestService>();
}
}

public interface ITestService
{
void HandleUpdate();
}

public class TestService : ITestService
{
public void HandleUpdate() { }
}
}

namespace Namespace2
{
public class AccountPlugin : Plugin
{
public AccountPlugin()
{
RegisterStep<Account, ITestService>(EventOperation.Update, ExecutionStage.PostOperation,
service => service.HandleUpdate)
.WithPreImage(x => x.AccountNumber);
}

protected override IServiceCollection OnBeforeBuildServiceProvider(IServiceCollection services)
{
return services.AddScoped<ITestService, TestService>();
}
}

public interface ITestService
{
void HandleUpdate();
}

public class TestService : ITestService
{
public void HandleUpdate() { }
}
}
""";

// Act
var result = GeneratorTestHelper.RunGenerator(
CompilationHelper.CreateCompilation(source));

// Assert - Both plugins should generate separate files with unique hint names
result.GeneratedSources.Should().HaveCount(2,
"both plugins should generate code without hint name collision");

// Index sources by hint name for precise verification
var sourcesByHintName = result.GeneratedSources.ToDictionary(gs => gs.HintName, gs => gs.SourceText);

// Find the hint names for each namespace
var namespace1HintName = sourcesByHintName.Keys.Single(h => h.Contains("Namespace1_"));
var namespace2HintName = sourcesByHintName.Keys.Single(h => h.Contains("Namespace2_"));

// Verify Namespace1 source: correct namespace AND correct property (Name)
var namespace1Source = sourcesByHintName[namespace1HintName];
namespace1Source.Should().Contain("namespace Namespace1.PluginRegistrations.AccountPlugin.AccountUpdatePostOperation",
"Namespace1 hint name should map to Namespace1 generated namespace");
namespace1Source.Should().Contain("public string Name =>",
"Namespace1 plugin registered Name attribute");

// Verify Namespace2 source: correct namespace AND correct property (AccountNumber)
var namespace2Source = sourcesByHintName[namespace2HintName];
namespace2Source.Should().Contain("namespace Namespace2.PluginRegistrations.AccountPlugin.AccountUpdatePostOperation",
"Namespace2 hint name should map to Namespace2 generated namespace");
namespace2Source.Should().Contain("public string AccountNumber =>",
"Namespace2 plugin registered AccountNumber attribute");

// Verify each source only contains its own namespace (not cross-contaminated)
namespace1Source.Should().NotContain("namespace Namespace2",
"Namespace1 source should not contain Namespace2 namespace declaration");
namespace2Source.Should().NotContain("namespace Namespace1",
"Namespace2 source should not contain Namespace1 namespace declaration");
}

private static async Task<ImmutableArray<Diagnostic>> GetAnalyzerDiagnosticsAsync(string source, DiagnosticAnalyzer analyzer)
{
var compilation = CompilationHelper.CreateCompilation(source);
Expand Down
12 changes: 12 additions & 0 deletions XrmPluginCore.SourceGenerator.Tests/Helpers/GeneratorTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,17 @@ public static GeneratorRunResult RunGenerator(CSharpCompilation compilation)
.Where(tree => !compilation.SyntaxTrees.Contains(tree))
.ToArray();

// Get generated sources with hint names from the run result
var generatedSources = runResult.Results[0].GeneratedSources
.Select(gs => new GeneratedSourceInfo(gs.HintName, gs.SourceText.ToString()))
.ToArray();

return new GeneratorRunResult
{
OutputCompilation = (CSharpCompilation)outputCompilation,
Diagnostics = [.. diagnostics],
GeneratedTrees = generatedTrees,
GeneratedSources = generatedSources,
GeneratorDiagnostics = [.. runResult.Results[0].Diagnostics]
};
}
Expand Down Expand Up @@ -105,9 +111,15 @@ public class GeneratorRunResult
public required CSharpCompilation OutputCompilation { get; init; }
public required Diagnostic[] Diagnostics { get; init; }
public required SyntaxTree[] GeneratedTrees { get; init; }
public required GeneratedSourceInfo[] GeneratedSources { get; init; }
public required Diagnostic[] GeneratorDiagnostics { get; init; }
}

/// <summary>
/// Information about a generated source file including its hint name.
/// </summary>
public record GeneratedSourceInfo(string HintName, string SourceText);

/// <summary>
/// Result from compiling generated code.
/// </summary>
Expand Down
10 changes: 0 additions & 10 deletions XrmPluginCore.SourceGenerator/Generators/PluginImageGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using XrmPluginCore.SourceGenerator.Helpers;
using XrmPluginCore.SourceGenerator.Models;
using XrmPluginCore.SourceGenerator.Parsers;
using XrmPluginCore.SourceGenerator.Validation;

namespace XrmPluginCore.SourceGenerator.Generators;

Expand Down Expand Up @@ -94,11 +93,6 @@ private static IEnumerable<PluginStepMetadata> TransformToMetadata(
if (mergedMetadata is null)
continue;

// Validate handler method signature
HandlerMethodValidator.ValidateHandlerMethod(
mergedMetadata,
semanticModel.Compilation);

// Include if:
// - Has method reference (for ActionWrapper generation)
// - OR has images with attributes (for image wrapper generation)
Expand Down Expand Up @@ -185,10 +179,6 @@ private void GenerateSourceFromMetadata(
}
}

// Skip generation if validation failed (analyzer will report the error)
if (metadata?.HasValidationError == true)
return;

// Generate code if we have a handler method reference (ActionWrapper always needed)
if (string.IsNullOrEmpty(metadata?.HandlerMethodName))
return;
Expand Down
11 changes: 3 additions & 8 deletions XrmPluginCore.SourceGenerator/Models/PluginStepMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ internal sealed class PluginStepMetadata
/// </summary>
public List<DiagnosticInfo> Diagnostics { get; set; } = [];

/// <summary>
/// If true, generation should be skipped for this registration due to validation errors.
/// The analyzer will report the appropriate diagnostic. Not included in equality comparison.
/// </summary>
public bool HasValidationError { get; set; }

/// <summary>
/// Gets the namespace for generated wrapper classes.
/// Format: {OriginalNamespace}.PluginRegistrations.{PluginClassName}.{Entity}{Op}{Stage}
Expand All @@ -57,10 +51,11 @@ internal sealed class PluginStepMetadata

/// <summary>
/// Gets a unique identifier for this registration.
/// Includes plugin class name to differentiate multiple registrations for the same entity/operation/stage.
/// Includes namespace and plugin class name to differentiate multiple registrations
/// for the same entity/operation/stage across different namespaces.
/// </summary>
public string UniqueId =>
$"{PluginClassName}_{EntityTypeName}_{EventOperation}_{ExecutionStage}";
$"{Namespace?.Replace(".", "_")}_{PluginClassName}_{EntityTypeName}_{EventOperation}_{ExecutionStage}";

public override bool Equals(object obj)
{
Expand Down
46 changes: 0 additions & 46 deletions XrmPluginCore.SourceGenerator/Validation/HandlerMethodValidator.cs

This file was deleted.