< Summary

Information
Class: DotNetApiDiff.Commands.CompareCommand
Assembly: DotNetApiDiff
File(s): /home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/Commands/CompareCommand.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 339
Coverable lines: 339
Total lines: 619
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 88
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
Validate(...)0%1332360%
Execute(...)0%7280%
ExtractMemberName(...)0%2040%
ExecuteWithConfiguredServices(...)0%110100%
ApplyCommandLineOptions(...)0%506220%
CreateApiComparisonFromResult(...)0%7280%

File(s)

/home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/Commands/CompareCommand.cs

#LineLine coverage
 1// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
 2using DotNetApiDiff.Interfaces;
 3using DotNetApiDiff.Models;
 4using DotNetApiDiff.Models.Configuration;
 5using Microsoft.Extensions.DependencyInjection;
 6using Microsoft.Extensions.Logging;
 7using Spectre.Console;
 8using Spectre.Console.Cli;
 9using System.ComponentModel;
 10using System.Diagnostics.CodeAnalysis;
 11using System.Reflection;
 12
 13namespace DotNetApiDiff.Commands;
 14
 15/// <summary>
 16/// Settings for the compare command
 17/// </summary>
 18[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicPro
 19public class CompareCommandSettings : CommandSettings
 20{
 21    [CommandArgument(0, "<sourceAssembly>")]
 22    [Description("Path to the source/baseline assembly")]
 23    public string? SourceAssemblyPath { get; set; }
 24
 25    [CommandArgument(1, "<targetAssembly>")]
 26    [Description("Path to the target/current assembly")]
 27    public string? TargetAssemblyPath { get; set; }
 28
 29    [CommandOption("-c|--config <configFile>")]
 30    [Description("Path to configuration file")]
 31    public string? ConfigFile { get; set; }
 32
 33    [CommandOption("-o|--output <format>")]
 34    [Description("Output format (console, json, html, markdown)")]
 35    public string? OutputFormat { get; set; }
 36
 37    [CommandOption("-p|--output-file <path>")]
 38    [Description("Output file path (required for json, html, markdown formats)")]
 39    public string? OutputFile { get; set; }
 40
 41    [CommandOption("-f|--filter <namespace>")]
 42    [Description("Filter to specific namespaces (can be specified multiple times)")]
 43    public string[]? NamespaceFilters { get; set; }
 44
 45    [CommandOption("-e|--exclude <pattern>")]
 46    [Description("Exclude types matching pattern (can be specified multiple times)")]
 47    public string[]? ExcludePatterns { get; set; }
 48
 49    [CommandOption("-t|--type <pattern>")]
 50    [Description("Filter to specific type patterns (can be specified multiple times)")]
 51    public string[]? TypePatterns { get; set; }
 52
 53    [CommandOption("--include-internals")]
 54    [Description("Include internal types in the comparison")]
 55    [DefaultValue(false)]
 56    public bool IncludeInternals { get; set; }
 57
 58    [CommandOption("--include-compiler-generated")]
 59    [Description("Include compiler-generated types in the comparison")]
 60    [DefaultValue(false)]
 61    public bool IncludeCompilerGenerated { get; set; }
 62
 63    [CommandOption("--no-color")]
 64    [Description("Disable colored output")]
 65    [DefaultValue(false)]
 66    public bool NoColor { get; set; }
 67
 68    [CommandOption("-v|--verbose")]
 69    [Description("Enable verbose output")]
 70    [DefaultValue(false)]
 71    public bool Verbose { get; set; }
 72}
 73
 74/// <summary>
 75/// Command to compare two assemblies
 76/// </summary>
 77public class CompareCommand : Command<CompareCommandSettings>
 78{
 79    private readonly IServiceProvider _serviceProvider;
 80    private readonly ILogger<CompareCommand> _logger;
 81    private readonly IExitCodeManager _exitCodeManager;
 82    private readonly IGlobalExceptionHandler _exceptionHandler;
 83
 84    /// <summary>
 85    /// Initializes a new instance of the <see cref="CompareCommand"/> class.
 86    /// </summary>
 87    /// <param name="serviceProvider">The service provider.</param>
 88    /// <param name="logger">The logger.</param>
 89    /// <param name="exitCodeManager">The exit code manager.</param>
 90    /// <param name="exceptionHandler">The global exception handler.</param>
 091    public CompareCommand(
 092        IServiceProvider serviceProvider,
 093        ILogger<CompareCommand> logger,
 094        IExitCodeManager exitCodeManager,
 095        IGlobalExceptionHandler exceptionHandler)
 096    {
 097        _serviceProvider = serviceProvider;
 098        _logger = logger;
 099        _exitCodeManager = exitCodeManager;
 0100        _exceptionHandler = exceptionHandler;
 0101    }
 102
 103    /// <summary>
 104    /// Validates the command settings
 105    /// </summary>
 106    /// <param name="context">The command context</param>
 107    /// <param name="settings">The command settings</param>
 108    /// <returns>ValidationResult indicating success or failure</returns>
 109    public override ValidationResult Validate([NotNull] CommandContext context, [NotNull] CompareCommandSettings setting
 0110    {
 111        // Validate source assembly path
 0112        if (string.IsNullOrEmpty(settings.SourceAssemblyPath))
 0113        {
 0114            return ValidationResult.Error("Source assembly path is required");
 115        }
 116
 0117        if (!File.Exists(settings.SourceAssemblyPath))
 0118        {
 0119            return ValidationResult.Error($"Source assembly file not found: {settings.SourceAssemblyPath}");
 120        }
 121
 122        // Validate target assembly path
 0123        if (string.IsNullOrEmpty(settings.TargetAssemblyPath))
 0124        {
 0125            return ValidationResult.Error("Target assembly path is required");
 126        }
 127
 0128        if (!File.Exists(settings.TargetAssemblyPath))
 0129        {
 0130            return ValidationResult.Error($"Target assembly file not found: {settings.TargetAssemblyPath}");
 131        }
 132
 133        // Validate config file if specified
 0134        if (!string.IsNullOrEmpty(settings.ConfigFile) && !File.Exists(settings.ConfigFile))
 0135        {
 0136            return ValidationResult.Error($"Configuration file not found: {settings.ConfigFile}");
 137        }
 138
 139        // Validate output format if provided
 0140        if (!string.IsNullOrEmpty(settings.OutputFormat))
 0141        {
 0142            string format = settings.OutputFormat.ToLowerInvariant();
 0143            if (format != "console" && format != "json" && format != "html" && format != "markdown")
 0144            {
 0145                return ValidationResult.Error($"Invalid output format: {settings.OutputFormat}. Valid formats are: conso
 146            }
 147
 148            // Validate output file requirements
 0149            if (format == "html")
 0150            {
 151                // HTML format requires an output file
 0152                if (string.IsNullOrEmpty(settings.OutputFile))
 0153                {
 0154                    return ValidationResult.Error($"Output file is required for {settings.OutputFormat} format. Use --ou
 155                }
 156
 157                // Validate output directory exists
 0158                var outputDir = Path.GetDirectoryName(settings.OutputFile);
 0159                if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
 0160                {
 0161                    return ValidationResult.Error($"Output directory does not exist: {outputDir}");
 162                }
 0163            }
 0164        }
 0165        else if (!string.IsNullOrEmpty(settings.OutputFile))
 0166        {
 167            // If output file is specified for non-HTML formats, validate the directory exists
 0168            var outputDir = Path.GetDirectoryName(settings.OutputFile);
 0169            if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
 0170            {
 0171                return ValidationResult.Error($"Output directory does not exist: {outputDir}");
 172            }
 0173        }
 174
 0175        return ValidationResult.Success();
 0176    }
 177
 178    /// <summary>
 179    /// Executes the command
 180    /// </summary>
 181    /// <param name="context">The command context</param>
 182    /// <param name="settings">The command settings</param>
 183    /// <returns>Exit code (0 for success, non-zero for failure)</returns>
 184    public override int Execute([NotNull] CommandContext context, [NotNull] CompareCommandSettings settings)
 0185    {
 186        try
 0187        {
 188            // Create a logging scope for this command execution
 0189            using (_logger.BeginScope("Compare command execution"))
 0190            {
 191                // Set up logging level based on verbose flag
 0192                if (settings.Verbose)
 0193                {
 0194                    _logger.LogInformation("Verbose logging enabled");
 0195                }
 196
 197                // Configure console output
 0198                if (settings.NoColor)
 0199                {
 0200                    _logger.LogDebug("Disabling colored output");
 0201                    AnsiConsole.Profile.Capabilities.ColorSystem = ColorSystem.NoColors;
 0202                }
 203
 204                // Load configuration
 205                ComparisonConfiguration config;
 0206                if (!string.IsNullOrEmpty(settings.ConfigFile))
 0207                {
 0208                    using (_logger.BeginScope("Configuration loading"))
 0209                    {
 0210                        _logger.LogInformation("Loading configuration from {ConfigFile}", settings.ConfigFile);
 211
 212                        try
 0213                        {
 214                            // Verify the file exists and is accessible
 0215                            if (!File.Exists(settings.ConfigFile))
 0216                            {
 0217                                throw new FileNotFoundException($"Configuration file not found: {settings.ConfigFile}", 
 218                            }
 219
 220                            // Try to load the configuration
 0221                            config = ComparisonConfiguration.LoadFromJsonFile(settings.ConfigFile);
 0222                            _logger.LogInformation("Configuration loaded successfully");
 0223                        }
 0224                        catch (Exception ex)
 0225                        {
 0226                            _logger.LogError(ex, "Error loading configuration from {ConfigFile}", settings.ConfigFile);
 0227                            AnsiConsole.MarkupLine($"[red]Error loading configuration:[/] {ex.Message}");
 228
 229                            // Use the ExitCodeManager to determine the appropriate exit code for errors
 0230                            return _exitCodeManager.GetExitCodeForException(ex);
 231                        }
 0232                    }
 0233                }
 234                else
 0235                {
 0236                    _logger.LogInformation("Using default configuration");
 0237                    config = ComparisonConfiguration.CreateDefault();
 0238                }
 239
 240                // Apply command-line filters and options
 0241                ApplyCommandLineOptions(settings, config);
 242
 243                // NOW CREATE THE COMMAND-SPECIFIC CONTAINER
 0244                _logger.LogInformation("Creating command-specific service container with loaded configuration");
 245
 0246                var commandServices = new ServiceCollection();
 247
 248                // Reuse shared services from root container
 0249                var loggerFactory = _serviceProvider.GetRequiredService<ILoggerFactory>();
 0250                commandServices.AddSingleton(loggerFactory);
 0251                commandServices.AddLogging(); // This adds ILogger<T> services
 252
 253                // Add the loaded configuration
 0254                commandServices.AddSingleton(config);                // Add all business logic services with configurati
 0255                commandServices.AddScoped<IAssemblyLoader, AssemblyLoading.AssemblyLoader>();
 0256                commandServices.AddScoped<IApiExtractor, ApiExtraction.ApiExtractor>();
 0257                commandServices.AddScoped<IMemberSignatureBuilder, ApiExtraction.MemberSignatureBuilder>();
 0258                commandServices.AddScoped<ITypeAnalyzer, ApiExtraction.TypeAnalyzer>();
 0259                commandServices.AddScoped<IDifferenceCalculator, ApiExtraction.DifferenceCalculator>();
 0260                commandServices.AddScoped<IReportGenerator, Reporting.ReportGenerator>();
 261
 262                // Add configuration-specific services
 0263                commandServices.AddScoped<INameMapper>(provider =>
 0264                {
 0265                    return new ApiExtraction.NameMapper(
 0266                        config.Mappings,
 0267                        loggerFactory.CreateLogger<ApiExtraction.NameMapper>());
 0268                });
 269
 0270                commandServices.AddScoped<IChangeClassifier>(provider =>
 0271                    new ApiExtraction.ChangeClassifier(
 0272                        config.BreakingChangeRules,
 0273                        config.Exclusions,
 0274                        loggerFactory.CreateLogger<ApiExtraction.ChangeClassifier>()));
 275
 276                // Add the main comparison service that depends on configured services
 0277                commandServices.AddScoped<IApiComparer>(provider =>
 0278                    new ApiExtraction.ApiComparer(
 0279                        provider.GetRequiredService<IApiExtractor>(),
 0280                        provider.GetRequiredService<IDifferenceCalculator>(),
 0281                        provider.GetRequiredService<INameMapper>(),
 0282                        provider.GetRequiredService<IChangeClassifier>(),
 0283                        config,
 0284                        provider.GetRequiredService<ILogger<ApiExtraction.ApiComparer>>()));
 285
 286                // Execute the command with the configured services
 0287                using (var commandProvider = commandServices.BuildServiceProvider())
 0288                {
 0289                    return ExecuteWithConfiguredServices(settings, config, commandProvider);
 290                }
 291            }
 292        }
 0293        catch (Exception ex)
 0294        {
 295            // Use our centralized exception handler for any unhandled exceptions
 0296            return _exceptionHandler.HandleException(ex, "Compare command execution");
 297        }
 0298    }
 299
 300    /// <summary>
 301    /// Extracts the member name from a full element name
 302    /// </summary>
 303    /// <param name="elementName">The full element name</param>
 304    /// <returns>The member name</returns>
 305    private static string ExtractMemberName(string elementName)
 0306    {
 0307        if (string.IsNullOrEmpty(elementName))
 0308        {
 0309            return "Unknown"; // Or throw an exception if this is not expected
 310        }
 311
 312        // For full names like "Namespace.Class.Method", extract just "Method"
 0313        var lastDotIndex = elementName.LastIndexOf('.');
 0314        return lastDotIndex >= 0 ? elementName.Substring(lastDotIndex + 1) : elementName;
 0315    }
 316
 317    /// <summary>
 318    /// Executes the comparison logic using the configured services
 319    /// </summary>
 320    /// <param name="settings">Command settings</param>
 321    /// <param name="config">Loaded configuration</param>
 322    /// <param name="serviceProvider">Command-specific service provider</param>
 323    /// <returns>Exit code</returns>
 324    private int ExecuteWithConfiguredServices(CompareCommandSettings settings, ComparisonConfiguration config, IServiceP
 0325    {
 326        // Load assemblies
 327        Assembly sourceAssembly;
 328        Assembly targetAssembly;
 329
 0330        using (_logger.BeginScope("Assembly loading"))
 0331        {
 0332            _logger.LogInformation("Loading source assembly: {Path}", settings.SourceAssemblyPath);
 0333            _logger.LogInformation("Loading target assembly: {Path}", settings.TargetAssemblyPath);
 334
 0335            var assemblyLoader = serviceProvider.GetRequiredService<IAssemblyLoader>();
 336
 337            try
 0338            {
 0339                sourceAssembly = assemblyLoader.LoadAssembly(settings.SourceAssemblyPath!);
 0340            }
 0341            catch (Exception ex)
 0342            {
 0343                _logger.LogError(ex, "Failed to load source assembly: {Path}", settings.SourceAssemblyPath);
 0344                AnsiConsole.MarkupLine($"[red]Error loading source assembly:[/] {ex.Message}");
 345
 0346                return _exitCodeManager.GetExitCodeForException(ex);
 347            }
 348
 349            try
 0350            {
 0351                targetAssembly = assemblyLoader.LoadAssembly(settings.TargetAssemblyPath!);
 0352            }
 0353            catch (Exception ex)
 0354            {
 0355                _logger.LogError(ex, "Failed to load target assembly: {Path}", settings.TargetAssemblyPath);
 0356                AnsiConsole.MarkupLine($"[red]Error loading target assembly:[/] {ex.Message}");
 357
 0358                return _exitCodeManager.GetExitCodeForException(ex);
 359            }
 0360        }
 361
 362        // Extract API information
 0363        using (_logger.BeginScope("API extraction"))
 0364        {
 0365            _logger.LogInformation("Extracting API information from assemblies");
 0366            var apiExtractor = serviceProvider.GetRequiredService<IApiExtractor>();
 367
 368            // Pass the filter configuration to the API extractor
 0369            var sourceApi = apiExtractor.ExtractApiMembers(sourceAssembly, config.Filters);
 0370            var targetApi = apiExtractor.ExtractApiMembers(targetAssembly, config.Filters);
 371
 372            // Log the number of API members extracted
 0373            _logger.LogInformation(
 0374                "Extracted {SourceCount} API members from source and {TargetCount} API members from target",
 0375                sourceApi.Count(),
 0376                targetApi.Count());
 0377        }
 378
 379        // Compare APIs
 380        Models.ComparisonResult comparisonResult;
 0381        using (_logger.BeginScope("API comparison"))
 0382        {
 0383            _logger.LogInformation("Comparing APIs");
 0384            var apiComparer = serviceProvider.GetRequiredService<IApiComparer>();
 385
 386            try
 0387            {
 388                // Use the single CompareAssemblies method - configuration is now injected into dependencies
 0389                comparisonResult = apiComparer.CompareAssemblies(sourceAssembly, targetAssembly);
 390
 391                // Update configuration with actual command-line values ONLY if explicitly provided by user
 0392                if (!string.IsNullOrEmpty(settings.OutputFormat) && Enum.TryParse<ReportFormat>(settings.OutputFormat, t
 0393                {
 0394                    comparisonResult.Configuration.OutputFormat = outputFormat;
 0395                }
 396
 0397                if (!string.IsNullOrEmpty(settings.OutputFile))
 0398                {
 0399                    comparisonResult.Configuration.OutputPath = settings.OutputFile;
 0400                }
 0401            }
 0402            catch (Exception ex)
 0403            {
 0404                _logger.LogError(ex, "Error comparing assemblies");
 0405                AnsiConsole.MarkupLine($"[red]Error comparing assemblies:[/] {ex.Message}");
 406
 0407                return _exitCodeManager.GetExitCodeForException(ex);
 408            }
 0409        }
 410
 411        // Create ApiComparison from ComparisonResult
 0412        var comparison = CreateApiComparisonFromResult(comparisonResult);
 413
 414        // Generate report
 0415        using (_logger.BeginScope("Report generation"))
 0416        {
 417            // Use the configuration from the comparison result, which now has the correct precedence applied
 0418            var effectiveFormat = comparisonResult.Configuration.OutputFormat;
 0419            var effectiveOutputFile = comparisonResult.Configuration.OutputPath;
 420
 0421            _logger.LogInformation("Generating {Format} report", effectiveFormat);
 0422            var reportGenerator = serviceProvider.GetRequiredService<IReportGenerator>();
 423
 424            string report;
 425            try
 0426            {
 0427                if (string.IsNullOrEmpty(effectiveOutputFile))
 0428                {
 429                    // No output file specified - output to console regardless of format
 0430                    report = reportGenerator.GenerateReport(comparisonResult, effectiveFormat);
 431
 432                    // Output the formatted report to the console
 433                    // Use Console.Write to avoid format string interpretation issues
 0434                    Console.Write(report);
 0435                }
 436                else
 0437                {
 438                    // Output file specified - save to the specified file
 0439                    reportGenerator.SaveReportAsync(comparisonResult, effectiveFormat, effectiveOutputFile).GetAwaiter()
 0440                    _logger.LogInformation("Report saved to {OutputFile}", effectiveOutputFile);
 0441                }
 0442            }
 0443            catch (Exception ex)
 0444            {
 0445                _logger.LogError(ex, "Error generating {Format} report", effectiveFormat);
 0446                AnsiConsole.MarkupLine($"[red]Error generating report:[/] {ex.Message}");
 447
 0448                return _exitCodeManager.GetExitCodeForException(ex);
 449            }
 0450        }
 451
 452        // Use the ExitCodeManager to determine the appropriate exit code
 0453        int exitCode = _exitCodeManager.GetExitCode(comparison);
 454
 0455        if (comparison.HasBreakingChanges)
 0456        {
 0457            _logger.LogWarning("{Count} breaking changes detected", comparison.BreakingChangesCount);
 0458        }
 459        else
 0460        {
 0461            _logger.LogInformation("Comparison completed successfully with no breaking changes");
 0462        }
 463
 0464        _logger.LogInformation(
 0465            "Exiting with code {ExitCode}: {Description}",
 0466            exitCode,
 0467            _exitCodeManager.GetExitCodeDescription(exitCode));
 468
 0469        return exitCode;
 0470    }
 471
 472    /// <summary>
 473    /// Applies command-line options to the configuration
 474    /// </summary>
 475    /// <param name="settings">Command settings</param>
 476    /// <param name="config">Configuration to update</param>
 477    /// <param name="logger">Logger for diagnostic information</param>
 478    private void ApplyCommandLineOptions(CompareCommandSettings settings, Models.Configuration.ComparisonConfiguration c
 0479    {
 0480        using (_logger.BeginScope("Applying command-line options"))
 0481        {
 482            // Apply namespace filters if specified
 0483            if (settings.NamespaceFilters != null && settings.NamespaceFilters.Length > 0)
 0484            {
 0485                _logger.LogInformation("Applying namespace filters: {Filters}", string.Join(", ", settings.NamespaceFilt
 486
 487                // Add namespace filters to the configuration
 0488                config.Filters.IncludeNamespaces.AddRange(settings.NamespaceFilters);
 489
 490                // If we have explicit includes, we're filtering to only those namespaces
 0491                if (config.Filters.IncludeNamespaces.Count > 0)
 0492                {
 0493                    _logger.LogInformation("Filtering to only include specified namespaces");
 0494                }
 0495            }
 496
 497            // Apply type pattern filters if specified
 0498            if (settings.TypePatterns != null && settings.TypePatterns.Length > 0)
 0499            {
 0500                _logger.LogInformation("Applying type pattern filters: {Patterns}", string.Join(", ", settings.TypePatte
 501
 502                // Add type pattern filters to the configuration
 0503                config.Filters.IncludeTypes.AddRange(settings.TypePatterns);
 504
 0505                _logger.LogInformation("Filtering to only include types matching specified patterns");
 0506            }
 507
 508            // Apply command-line exclusions if specified
 0509            if (settings.ExcludePatterns != null && settings.ExcludePatterns.Length > 0)
 0510            {
 0511                _logger.LogInformation("Applying exclusion patterns: {Patterns}", string.Join(", ", settings.ExcludePatt
 512
 513                // Add exclusion patterns to the configuration
 0514                foreach (var pattern in settings.ExcludePatterns)
 0515                {
 516                    // Determine if this is a namespace or type pattern based on presence of dot
 0517                    if (pattern.Contains('.'))
 0518                    {
 519                        // Assume it's a type pattern if it contains a dot
 0520                        config.Exclusions.ExcludedTypePatterns.Add(pattern);
 0521                    }
 522                    else
 0523                    {
 524                        // Otherwise assume it's a namespace pattern
 0525                        config.Filters.ExcludeNamespaces.Add(pattern);
 0526                    }
 0527                }
 0528            }
 529
 530            // Apply internal types inclusion if specified
 0531            if (settings.IncludeInternals)
 0532            {
 0533                _logger.LogInformation("Including internal types in comparison");
 0534                config.Filters.IncludeInternals = true;
 0535            }
 536
 537            // Apply compiler-generated types inclusion if specified
 0538            if (settings.IncludeCompilerGenerated)
 0539            {
 0540                _logger.LogInformation("Including compiler-generated types in comparison");
 0541                config.Filters.IncludeCompilerGenerated = true;
 0542            }
 0543        }
 0544    }
 545
 546    /// <summary>
 547    /// Creates an ApiComparison object from a ComparisonResult
 548    /// </summary>
 549    /// <param name="comparisonResult">The comparison result to convert</param>
 550    /// <returns>An ApiComparison object</returns>
 551    private Models.ApiComparison CreateApiComparisonFromResult(Models.ComparisonResult comparisonResult)
 0552    {
 0553        return new Models.ApiComparison
 0554        {
 0555            Additions = comparisonResult.Differences
 0556                .Where(d => d.ChangeType == Models.ChangeType.Added)
 0557                .Select(d => new Models.ApiChange
 0558                {
 0559                    Type = Models.ChangeType.Added,
 0560                    Description = d.Description,
 0561                    TargetMember = new Models.ApiMember
 0562                    {
 0563                        Name = ExtractMemberName(d.ElementName),
 0564                        FullName = d.ElementName,
 0565                        Signature = d.NewSignature ?? "Unknown"
 0566                    },
 0567                    IsBreakingChange = d.IsBreakingChange
 0568                }).ToList(),
 0569            Removals = comparisonResult.Differences
 0570                .Where(d => d.ChangeType == Models.ChangeType.Removed)
 0571                .Select(d => new Models.ApiChange
 0572                {
 0573                    Type = Models.ChangeType.Removed,
 0574                    Description = d.Description,
 0575                    SourceMember = new Models.ApiMember
 0576                    {
 0577                        Name = ExtractMemberName(d.ElementName),
 0578                        FullName = d.ElementName,
 0579                        Signature = d.OldSignature ?? "Unknown"
 0580                    },
 0581                    IsBreakingChange = d.IsBreakingChange
 0582                }).ToList(),
 0583            Modifications = comparisonResult.Differences
 0584                .Where(d => d.ChangeType == Models.ChangeType.Modified)
 0585                .Select(d => new Models.ApiChange
 0586                {
 0587                    Type = Models.ChangeType.Modified,
 0588                    Description = d.Description,
 0589                    SourceMember = new Models.ApiMember
 0590                    {
 0591                        Name = ExtractMemberName(d.ElementName),
 0592                        FullName = d.ElementName,
 0593                        Signature = d.OldSignature ?? "Unknown"
 0594                    },
 0595                    TargetMember = new Models.ApiMember
 0596                    {
 0597                        Name = ExtractMemberName(d.ElementName),
 0598                        FullName = d.ElementName,
 0599                        Signature = d.NewSignature ?? "Unknown"
 0600                    },
 0601                    IsBreakingChange = d.IsBreakingChange
 0602                }).ToList(),
 0603            Excluded = comparisonResult.Differences
 0604                .Where(d => d.ChangeType == Models.ChangeType.Excluded)
 0605                .Select(d => new Models.ApiChange
 0606                {
 0607                    Type = Models.ChangeType.Excluded,
 0608                    Description = d.Description,
 0609                    SourceMember = new Models.ApiMember
 0610                    {
 0611                        Name = ExtractMemberName(d.ElementName),
 0612                        FullName = d.ElementName,
 0613                        Signature = "Unknown"
 0614                    },
 0615                    IsBreakingChange = false
 0616                }).ToList()
 0617        };
 0618    }
 619}