/// <summary> /// Inspects the provided <paramref name="file"/>. If the file is a solution (.sln) file all /// non-excluded projects are inspected. If the file is a project (.csproj) file that project is /// inspected. /// </summary> /// <param name="file">An absolute file path to either a solution (.sln) or project (.csproj) file.</param> /// <param name="parameters">The inspection parameters provided by the user.</param> /// <param name="ct">A cancellation token.</param> /// <exception cref="InspectionException">This exception is thrown if the inspection process encounters an error.</exception> public async IAsyncEnumerable <ProjectInspectionResult> InspectAsync(FileSystemInfo file, InspectionParameters parameters, [EnumeratorCancellation] CancellationToken ct) { MSBuildLocator.RegisterDefaults(); _logger.LogInformation("Initializing"); using (var workspace = MSBuildWorkspace.Create()) { workspace.LoadMetadataForReferencedProjects = true; var context = new InspectionContext(file, workspace, parameters); var metrics = context.CreateMetricInstances(context.Parameters).ToList(); if (file.HasExtension(".csproj")) { _logger.LogVerbose($"Inspecting project: {file}"); yield return(await InspectProject(context, metrics, ct)); } else if (file.HasExtension(".sln")) { _logger.LogVerbose($"Inspecting solution: {file}"); await foreach (var result in InspectSolution(context, metrics, ct)) { yield return(result); } } else { throw new InspectionException("The specified file path is not a reference to a solution or project file"); } } }
/// <summary> /// Records the execution time of the processing for the provided <paramref name="project"/>. /// </summary> /// <param name="context">The inspection context.</param> /// <param name="metrics">The metrics to compute on each package of each project in the solution.</param> /// <param name="project">The project to inspect.</param> /// <param name="ct">A cancellation token.</param> private async Task <ProjectInspectionResult> TimedProcessAsync(InspectionContext context, IList <IMetric> metrics, Project project, CancellationToken ct) { var sw = Stopwatch.StartNew(); var result = await ProcessAsync(context.Parameters, project, metrics, ct); sw.Stop(); result.Elapsed = sw.Elapsed; return(result); }
/// <summary> /// Inspects a single project. /// </summary> /// <param name="context">The inspection context.</param> /// <param name="metrics">The metrics to compute on each package of each project in the solution.</param> /// <param name="ct">A cancellation token.</param> private async Task <ProjectInspectionResult> InspectProject(InspectionContext context, IList <IMetric> metrics, CancellationToken ct) { var project = await context.Workspace.OpenProjectAsync(context.File.FullName, progress : null, ct); if (project is null) { _logger.LogInformation("Project: Load failed"); throw new InspectionException("Failed to load the project"); } _logger.LogInformation("Done"); return(await TimedProcessAsync(context, metrics, project, ct)); }
/// <summary> /// Processes the provided <paramref name="projects"/> in parallel with a max concurrency declared by <see cref="InspectionParameters.MaxConcurrency"/>. /// </summary> /// <param name="projects">A collection projects to process.</param> /// <param name="context">The inspection context.</param> /// <param name="metrics">The metrics to compute on each package of each project in the solution.</param> /// <param name="results">The output channel for the processing results.</param> /// <param name="ct">A cancellation token.</param> /// <exception cref="AggregateException"></exception> private async Task ParallelProcessAsync(IEnumerable <Project> projects, InspectionContext context, IList <IMetric> metrics, ChannelWriter <ProjectInspectionResult> results, CancellationToken ct) { var filtered = projects.Where(project => !context.Parameters.IsProjectExcluded(project)).ToList(); var exceptions = new List <Exception>(); var partitions = Partitioner.Create(filtered).GetPartitions(context.Parameters.MaxConcurrency); try { var tasks = partitions.Select(partition => Task.Run(async delegate { using (partition) { while (!ct.IsCancellationRequested && partition.MoveNext()) { try { var result = await TimedProcessAsync(context, metrics, partition.Current, ct); await results.WriteAsync(result, ct); } catch (Exception exception) { exceptions.Add(new InspectionException($"Failed to process project: {partition.Current.Name}", exception)); } } } }, CancellationToken.None)); await Task.WhenAll(tasks); } finally { // This call is required to ensure the channel is completed and the inspection stops. The caller // is awaiting an async enumerable from the channel, which blocks indefinitely otherwise. results.Complete(); } if (exceptions.Any()) { throw new AggregateException(exceptions).Flatten(); } }
/// <summary> /// Inspects all non-excluded projects in a solution. /// </summary> /// <param name="context">The inspection context.</param> /// <param name="metrics">The metrics to compute on each package of each project in the solution.</param> /// <param name="ct">A cancellation token.</param> private async IAsyncEnumerable <ProjectInspectionResult> InspectSolution(InspectionContext context, IList <IMetric> metrics, [EnumeratorCancellation] CancellationToken ct) { var solution = await context.Workspace.OpenSolutionAsync(context.File.FullName, progress : null, ct); if (solution is null) { _logger.LogInformation("Solution: Load failed"); throw new InspectionException("Failed to load the solution"); } _logger.LogInformation("Done"); var results = Channel.CreateUnbounded <ProjectInspectionResult>(); var producerTask = ParallelProcessAsync(solution.Projects, context, metrics, results.Writer, ct); await foreach (var result in results.Reader.ReadAllAsync(ct)) { yield return(result); } await producerTask; }