< 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: 303
Coverable lines: 303
Total lines: 573
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 76
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%
ExecuteWithConfiguredServices(...)0%110100%
ApplyCommandLineOptions(...)0%506220%
CreateApiComparisonFromResult(...)100%210%

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    /// Executes the comparison logic using the configured services
 302    /// </summary>
 303    /// <param name="settings">Command settings</param>
 304    /// <param name="config">Loaded configuration</param>
 305    /// <param name="serviceProvider">Command-specific service provider</param>
 306    /// <returns>Exit code</returns>
 307    private int ExecuteWithConfiguredServices(CompareCommandSettings settings, ComparisonConfiguration config, IServiceP
 0308    {
 309        // Load assemblies
 310        Assembly sourceAssembly;
 311        Assembly targetAssembly;
 312
 0313        using (_logger.BeginScope("Assembly loading"))
 0314        {
 0315            _logger.LogInformation("Loading source assembly: {Path}", settings.SourceAssemblyPath);
 0316            _logger.LogInformation("Loading target assembly: {Path}", settings.TargetAssemblyPath);
 317
 0318            var assemblyLoader = serviceProvider.GetRequiredService<IAssemblyLoader>();
 319
 320            try
 0321            {
 0322                sourceAssembly = assemblyLoader.LoadAssembly(settings.SourceAssemblyPath!);
 0323            }
 0324            catch (Exception ex)
 0325            {
 0326                _logger.LogError(ex, "Failed to load source assembly: {Path}", settings.SourceAssemblyPath);
 0327                AnsiConsole.MarkupLine($"[red]Error loading source assembly:[/] {ex.Message}");
 328
 0329                return _exitCodeManager.GetExitCodeForException(ex);
 330            }
 331
 332            try
 0333            {
 0334                targetAssembly = assemblyLoader.LoadAssembly(settings.TargetAssemblyPath!);
 0335            }
 0336            catch (Exception ex)
 0337            {
 0338                _logger.LogError(ex, "Failed to load target assembly: {Path}", settings.TargetAssemblyPath);
 0339                AnsiConsole.MarkupLine($"[red]Error loading target assembly:[/] {ex.Message}");
 340
 0341                return _exitCodeManager.GetExitCodeForException(ex);
 342            }
 0343        }
 344
 345        // Extract API information
 0346        using (_logger.BeginScope("API extraction"))
 0347        {
 0348            _logger.LogInformation("Extracting API information from assemblies");
 0349            var apiExtractor = serviceProvider.GetRequiredService<IApiExtractor>();
 350
 351            // Pass the filter configuration to the API extractor
 0352            var sourceApi = apiExtractor.ExtractApiMembers(sourceAssembly, config.Filters);
 0353            var targetApi = apiExtractor.ExtractApiMembers(targetAssembly, config.Filters);
 354
 355            // Log the number of API members extracted
 0356            _logger.LogInformation(
 0357                "Extracted {SourceCount} API members from source and {TargetCount} API members from target",
 0358                sourceApi.Count(),
 0359                targetApi.Count());
 0360        }
 361
 362        // Compare APIs
 363        Models.ComparisonResult comparisonResult;
 0364        using (_logger.BeginScope("API comparison"))
 0365        {
 0366            _logger.LogInformation("Comparing APIs");
 0367            var apiComparer = serviceProvider.GetRequiredService<IApiComparer>();
 368
 369            try
 0370            {
 371                // Use the single CompareAssemblies method - configuration is now injected into dependencies
 0372                comparisonResult = apiComparer.CompareAssemblies(sourceAssembly, targetAssembly);
 373
 374                // Update configuration with actual command-line values ONLY if explicitly provided by user
 0375                if (!string.IsNullOrEmpty(settings.OutputFormat) && Enum.TryParse<ReportFormat>(settings.OutputFormat, t
 0376                {
 0377                    comparisonResult.Configuration.OutputFormat = outputFormat;
 0378                }
 379
 0380                if (!string.IsNullOrEmpty(settings.OutputFile))
 0381                {
 0382                    comparisonResult.Configuration.OutputPath = settings.OutputFile;
 0383                }
 0384            }
 0385            catch (Exception ex)
 0386            {
 0387                _logger.LogError(ex, "Error comparing assemblies");
 0388                AnsiConsole.MarkupLine($"[red]Error comparing assemblies:[/] {ex.Message}");
 389
 0390                return _exitCodeManager.GetExitCodeForException(ex);
 391            }
 0392        }
 393
 394        // Create ApiComparison from ComparisonResult
 0395        var comparison = CreateApiComparisonFromResult(comparisonResult);
 396
 397        // Generate report
 0398        using (_logger.BeginScope("Report generation"))
 0399        {
 400            // Use the configuration from the comparison result, which now has the correct precedence applied
 0401            var effectiveFormat = comparisonResult.Configuration.OutputFormat;
 0402            var effectiveOutputFile = comparisonResult.Configuration.OutputPath;
 403
 0404            _logger.LogInformation("Generating {Format} report", effectiveFormat);
 0405            var reportGenerator = serviceProvider.GetRequiredService<IReportGenerator>();
 406
 407            string report;
 408            try
 0409            {
 0410                if (string.IsNullOrEmpty(effectiveOutputFile))
 0411                {
 412                    // No output file specified - output to console regardless of format
 0413                    report = reportGenerator.GenerateReport(comparisonResult, effectiveFormat);
 414
 415                    // Output the formatted report to the console
 416                    // Use Console.Write to avoid format string interpretation issues
 0417                    Console.Write(report);
 0418                }
 419                else
 0420                {
 421                    // Output file specified - save to the specified file
 0422                    reportGenerator.SaveReportAsync(comparisonResult, effectiveFormat, effectiveOutputFile).GetAwaiter()
 0423                    _logger.LogInformation("Report saved to {OutputFile}", effectiveOutputFile);
 0424                }
 0425            }
 0426            catch (Exception ex)
 0427            {
 0428                _logger.LogError(ex, "Error generating {Format} report", effectiveFormat);
 0429                AnsiConsole.MarkupLine($"[red]Error generating report:[/] {ex.Message}");
 430
 0431                return _exitCodeManager.GetExitCodeForException(ex);
 432            }
 0433        }
 434
 435        // Use the ExitCodeManager to determine the appropriate exit code
 0436        int exitCode = _exitCodeManager.GetExitCode(comparison);
 437
 0438        if (comparison.HasBreakingChanges)
 0439        {
 0440            _logger.LogWarning("{Count} breaking changes detected", comparison.BreakingChangesCount);
 0441        }
 442        else
 0443        {
 0444            _logger.LogInformation("Comparison completed successfully with no breaking changes");
 0445        }
 446
 0447        _logger.LogInformation(
 0448            "Exiting with code {ExitCode}: {Description}",
 0449            exitCode,
 0450            _exitCodeManager.GetExitCodeDescription(exitCode));
 451
 0452        return exitCode;
 0453    }
 454
 455    /// <summary>
 456    /// Applies command-line options to the configuration
 457    /// </summary>
 458    /// <param name="settings">Command settings</param>
 459    /// <param name="config">Configuration to update</param>
 460    /// <param name="logger">Logger for diagnostic information</param>
 461    private void ApplyCommandLineOptions(CompareCommandSettings settings, Models.Configuration.ComparisonConfiguration c
 0462    {
 0463        using (_logger.BeginScope("Applying command-line options"))
 0464        {
 465            // Apply namespace filters if specified
 0466            if (settings.NamespaceFilters != null && settings.NamespaceFilters.Length > 0)
 0467            {
 0468                _logger.LogInformation("Applying namespace filters: {Filters}", string.Join(", ", settings.NamespaceFilt
 469
 470                // Add namespace filters to the configuration
 0471                config.Filters.IncludeNamespaces.AddRange(settings.NamespaceFilters);
 472
 473                // If we have explicit includes, we're filtering to only those namespaces
 0474                if (config.Filters.IncludeNamespaces.Count > 0)
 0475                {
 0476                    _logger.LogInformation("Filtering to only include specified namespaces");
 0477                }
 0478            }
 479
 480            // Apply type pattern filters if specified
 0481            if (settings.TypePatterns != null && settings.TypePatterns.Length > 0)
 0482            {
 0483                _logger.LogInformation("Applying type pattern filters: {Patterns}", string.Join(", ", settings.TypePatte
 484
 485                // Add type pattern filters to the configuration
 0486                config.Filters.IncludeTypes.AddRange(settings.TypePatterns);
 487
 0488                _logger.LogInformation("Filtering to only include types matching specified patterns");
 0489            }
 490
 491            // Apply command-line exclusions if specified
 0492            if (settings.ExcludePatterns != null && settings.ExcludePatterns.Length > 0)
 0493            {
 0494                _logger.LogInformation("Applying exclusion patterns: {Patterns}", string.Join(", ", settings.ExcludePatt
 495
 496                // Add exclusion patterns to the configuration
 0497                foreach (var pattern in settings.ExcludePatterns)
 0498                {
 499                    // Determine if this is a namespace or type pattern based on presence of dot
 0500                    if (pattern.Contains('.'))
 0501                    {
 502                        // Assume it's a type pattern if it contains a dot
 0503                        config.Exclusions.ExcludedTypePatterns.Add(pattern);
 0504                    }
 505                    else
 0506                    {
 507                        // Otherwise assume it's a namespace pattern
 0508                        config.Filters.ExcludeNamespaces.Add(pattern);
 0509                    }
 0510                }
 0511            }
 512
 513            // Apply internal types inclusion if specified
 0514            if (settings.IncludeInternals)
 0515            {
 0516                _logger.LogInformation("Including internal types in comparison");
 0517                config.Filters.IncludeInternals = true;
 0518            }
 519
 520            // Apply compiler-generated types inclusion if specified
 0521            if (settings.IncludeCompilerGenerated)
 0522            {
 0523                _logger.LogInformation("Including compiler-generated types in comparison");
 0524                config.Filters.IncludeCompilerGenerated = true;
 0525            }
 0526        }
 0527    }
 528
 529    /// <summary>
 530    /// Creates an ApiComparison object from a ComparisonResult
 531    /// </summary>
 532    /// <param name="comparisonResult">The comparison result to convert</param>
 533    /// <returns>An ApiComparison object</returns>
 534    private Models.ApiComparison CreateApiComparisonFromResult(Models.ComparisonResult comparisonResult)
 0535    {
 0536        return new Models.ApiComparison
 0537        {
 0538            Additions = comparisonResult.Differences
 0539                .Where(d => d.ChangeType == Models.ChangeType.Added)
 0540                .Select(d => new Models.ApiChange
 0541                {
 0542                    Type = Models.ChangeType.Added,
 0543                    TargetMember = new Models.ApiMember { Name = d.ElementName },
 0544                    IsBreakingChange = d.IsBreakingChange
 0545                }).ToList(),
 0546            Removals = comparisonResult.Differences
 0547                .Where(d => d.ChangeType == Models.ChangeType.Removed)
 0548                .Select(d => new Models.ApiChange
 0549                {
 0550                    Type = Models.ChangeType.Removed,
 0551                    SourceMember = new Models.ApiMember { Name = d.ElementName },
 0552                    IsBreakingChange = d.IsBreakingChange
 0553                }).ToList(),
 0554            Modifications = comparisonResult.Differences
 0555                .Where(d => d.ChangeType == Models.ChangeType.Modified)
 0556                .Select(d => new Models.ApiChange
 0557                {
 0558                    Type = Models.ChangeType.Modified,
 0559                    SourceMember = new Models.ApiMember { Name = d.ElementName },
 0560                    TargetMember = new Models.ApiMember { Name = d.ElementName },
 0561                    IsBreakingChange = d.IsBreakingChange
 0562                }).ToList(),
 0563            Excluded = comparisonResult.Differences
 0564                .Where(d => d.ChangeType == Models.ChangeType.Excluded)
 0565                .Select(d => new Models.ApiChange
 0566                {
 0567                    Type = Models.ChangeType.Excluded,
 0568                    SourceMember = new Models.ApiMember { Name = d.ElementName },
 0569                    IsBreakingChange = false
 0570                }).ToList()
 0571        };
 0572    }
 573}