diff --git a/src/Microsoft.ComponentDetection.Common/FileWritingService.cs b/src/Microsoft.ComponentDetection.Common/FileWritingService.cs
index 9539bb9e6..bd46e2f18 100644
--- a/src/Microsoft.ComponentDetection.Common/FileWritingService.cs
+++ b/src/Microsoft.ComponentDetection.Common/FileWritingService.cs
@@ -1,20 +1,32 @@
namespace Microsoft.ComponentDetection.Common;
+
using System;
using System.Collections.Concurrent;
+using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Common.Exceptions;
+using Newtonsoft.Json;
+///
public sealed class FileWritingService : IFileWritingService
{
+ ///
+ /// The format string used to generate the timestamp for the manifest file.
+ ///
public const string TimestampFormatString = "yyyyMMddHHmmssfff";
-
- private readonly object lockObject = new object();
- private readonly string timestamp = DateTime.Now.ToString(TimestampFormatString);
private readonly ConcurrentDictionary bufferedStreams = new();
+ private readonly object lockObject = new();
+ private readonly string timestamp = DateTime.Now.ToString(TimestampFormatString, CultureInfo.InvariantCulture);
+
+ ///
+ /// The base path to write files to.
+ /// If null or empty, the temp path will be used.
+ ///
public string BasePath { get; private set; }
+ ///
public void Init(string basePath)
{
if (!string.IsNullOrEmpty(basePath) && !Directory.Exists(basePath))
@@ -25,19 +37,25 @@ public void Init(string basePath)
this.BasePath = string.IsNullOrEmpty(basePath) ? Path.GetTempPath() : basePath;
}
- public void AppendToFile(string relativeFilePath, string text)
+ ///
+ public void AppendToFile(string relativeFilePath, T obj)
{
relativeFilePath = this.ResolveFilePath(relativeFilePath);
if (!this.bufferedStreams.TryGetValue(relativeFilePath, out var streamWriter))
{
streamWriter = new StreamWriter(relativeFilePath, true);
- this.bufferedStreams.TryAdd(relativeFilePath, streamWriter);
+ _ = this.bufferedStreams.TryAdd(relativeFilePath, streamWriter);
}
- streamWriter.Write(text);
+ var serializer = new JsonSerializer
+ {
+ Formatting = Formatting.Indented,
+ };
+ serializer.Serialize(streamWriter, obj);
}
+ ///
public void WriteFile(string relativeFilePath, string text)
{
relativeFilePath = this.ResolveFilePath(relativeFilePath);
@@ -48,6 +66,7 @@ public void WriteFile(string relativeFilePath, string text)
}
}
+ ///
public async Task WriteFileAsync(string relativeFilePath, string text)
{
relativeFilePath = this.ResolveFilePath(relativeFilePath);
@@ -55,23 +74,44 @@ public async Task WriteFileAsync(string relativeFilePath, string text)
await File.WriteAllTextAsync(relativeFilePath, text);
}
- public void WriteFile(FileInfo relativeFilePath, string text)
+ ///
+ public void WriteFile(FileInfo relativeFilePath, T obj)
{
- File.WriteAllText(relativeFilePath.FullName, text);
+ using var streamWriter = new StreamWriter(relativeFilePath.FullName);
+ using var jsonWriter = new JsonTextWriter(streamWriter);
+ var serializer = new JsonSerializer
+ {
+ Formatting = Formatting.Indented,
+ };
+ serializer.Serialize(jsonWriter, obj);
}
+ ///
public string ResolveFilePath(string relativeFilePath)
{
this.EnsureInit();
- if (relativeFilePath.Contains("{timestamp}"))
+ if (relativeFilePath.Contains("{timestamp}", StringComparison.Ordinal))
{
- relativeFilePath = relativeFilePath.Replace("{timestamp}", this.timestamp);
+ relativeFilePath = relativeFilePath.Replace("{timestamp}", this.timestamp, StringComparison.Ordinal);
}
relativeFilePath = Path.Combine(this.BasePath, relativeFilePath);
return relativeFilePath;
}
+ ///
+ public void Dispose() => this.Dispose(true);
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ foreach (var (filename, streamWriter) in this.bufferedStreams)
+ {
+ await streamWriter.DisposeAsync();
+ _ = this.bufferedStreams.TryRemove(filename, out _);
+ }
+ }
+
private void EnsureInit()
{
if (string.IsNullOrEmpty(this.BasePath))
@@ -90,22 +130,7 @@ private void Dispose(bool disposing)
foreach (var (filename, streamWriter) in this.bufferedStreams)
{
streamWriter.Dispose();
- this.bufferedStreams.TryRemove(filename, out _);
- }
- }
-
- public void Dispose()
- {
- this.Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- public async ValueTask DisposeAsync()
- {
- foreach (var (filename, streamWriter) in this.bufferedStreams)
- {
- await streamWriter.DisposeAsync();
- this.bufferedStreams.TryRemove(filename, out _);
+ _ = this.bufferedStreams.TryRemove(filename, out _);
}
}
}
diff --git a/src/Microsoft.ComponentDetection.Common/IFileWritingService.cs b/src/Microsoft.ComponentDetection.Common/IFileWritingService.cs
index 8ef32b030..ea0c7c9aa 100644
--- a/src/Microsoft.ComponentDetection.Common/IFileWritingService.cs
+++ b/src/Microsoft.ComponentDetection.Common/IFileWritingService.cs
@@ -4,18 +4,53 @@
using System.IO;
using System.Threading.Tasks;
-// All file paths are relative and will replace occurrences of {timestamp} with the shared file timestamp.
+///
+/// Provides methods for writing files.
+///
public interface IFileWritingService : IDisposable, IAsyncDisposable
{
+ ///
+ /// Initializes the file writing service with the given base path.
+ ///
+ /// The base path to use for all file operations.
void Init(string basePath);
- void AppendToFile(string relativeFilePath, string text);
+ ///
+ /// Appends the object to the file as JSON.
+ ///
+ /// The relative path to the file.
+ /// The object to append.
+ /// The type of the object to append.
+ void AppendToFile(string relativeFilePath, T obj);
+ ///
+ /// Writes the text to the file.
+ ///
+ /// The relative path to the file.
+ /// The text to write.
void WriteFile(string relativeFilePath, string text);
+ ///
+ /// Writes the text to the file.
+ ///
+ /// The relative path to the file.
+ /// The text to write.
+ /// A task that represents the asynchronous operation.
Task WriteFileAsync(string relativeFilePath, string text);
- void WriteFile(FileInfo relativeFilePath, string text);
+ ///
+ /// Writes the object to the file as JSON.
+ ///
+ /// The relative path to the file.
+ /// The object to write.
+ /// The type of the object to write.
+ void WriteFile(FileInfo relativeFilePath, T obj);
+ ///
+ /// Resolves the complete file path from the given relative file path.
+ /// Replaces occurrences of {timestamp} with the shared file timestamp.
+ ///
+ /// The relative path to the file.
+ /// The complete file path.
string ResolveFilePath(string relativeFilePath);
}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanCommandService.cs b/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanCommandService.cs
index 6b10e3fe5..d2b754b96 100644
--- a/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanCommandService.cs
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanCommandService.cs
@@ -54,20 +54,23 @@ private void WriteComponentManifest(IDetectionArguments detectionArguments, Scan
this.logger.LogInformation("Scan Manifest file: {ManifestFile}", this.fileWritingService.ResolveFilePath(ManifestRelativePath));
}
- var manifestJson = JsonConvert.SerializeObject(scanResult, Formatting.Indented);
-
if (userRequestedManifestPath == null)
{
- this.fileWritingService.AppendToFile(ManifestRelativePath, manifestJson);
+ this.fileWritingService.AppendToFile(ManifestRelativePath, scanResult);
}
else
{
- this.fileWritingService.WriteFile(userRequestedManifestPath, manifestJson);
+ this.fileWritingService.WriteFile(userRequestedManifestPath, scanResult);
}
if (detectionArguments.PrintManifest)
{
- Console.WriteLine(manifestJson);
+ using var jsonWriter = new JsonTextWriter(Console.Out);
+ var serializer = new JsonSerializer
+ {
+ Formatting = Formatting.Indented,
+ };
+ serializer.Serialize(jsonWriter, scanResult);
}
}
}
diff --git a/test/Microsoft.ComponentDetection.Common.Tests/FileWritingServiceTests.cs b/test/Microsoft.ComponentDetection.Common.Tests/FileWritingServiceTests.cs
index db9f085f9..dcc1014ad 100644
--- a/test/Microsoft.ComponentDetection.Common.Tests/FileWritingServiceTests.cs
+++ b/test/Microsoft.ComponentDetection.Common.Tests/FileWritingServiceTests.cs
@@ -1,5 +1,7 @@
namespace Microsoft.ComponentDetection.Common.Tests;
+
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.IO;
using FluentAssertions;
@@ -11,6 +13,17 @@
[TestCategory("Governance/ComponentDetection")]
public class FileWritingServiceTests
{
+ private const string SampleObjectJson = @"{
+ ""key1"": ""value1"",
+ ""key2"": ""value2""
+}";
+
+ private static readonly IDictionary SampleObject = new Dictionary
+ {
+ { "key1", "value1" },
+ { "key2", "value2" },
+ };
+
private FileWritingService serviceUnderTest;
private string tempFolder;
@@ -29,22 +42,20 @@ public void TestInitialize()
}
[TestCleanup]
- public void TestCleanup()
- {
- Directory.Delete(this.tempFolder, true);
- }
+ public void TestCleanup() => Directory.Delete(this.tempFolder, true);
[TestMethod]
public void AppendToFile_AppendsToFiles()
{
- var relativeDir = "someOtherFileName.txt";
+ var relativeDir = "someOtherFileName.json";
var fileLocation = Path.Combine(this.tempFolder, relativeDir);
File.Create(fileLocation).Dispose();
- this.serviceUnderTest.AppendToFile(relativeDir, "someSampleText");
+
+ this.serviceUnderTest.AppendToFile(relativeDir, SampleObject);
this.serviceUnderTest.Dispose();
+
var text = File.ReadAllText(Path.Combine(this.tempFolder, relativeDir));
- text
- .Should().Be("someSampleText");
+ text.Should().Be(SampleObjectJson);
}
[TestMethod]
@@ -53,22 +64,33 @@ public void WriteFile_CreatesAFile()
var relativeDir = "someFileName.txt";
this.serviceUnderTest.WriteFile(relativeDir, "sampleText");
var text = File.ReadAllText(Path.Combine(this.tempFolder, relativeDir));
- text
- .Should().Be("sampleText");
+ text.Should().Be("sampleText");
+ }
+
+ [TestMethod]
+ public void WriteFile_WritesJson()
+ {
+ var relativeDir = "someFileName.txt";
+ var fileInfo = new FileInfo(Path.Combine(this.tempFolder, relativeDir));
+
+ this.serviceUnderTest.WriteFile(fileInfo, SampleObject);
+
+ var text = File.ReadAllText(Path.Combine(this.tempFolder, relativeDir));
+ text.Should().Be(SampleObjectJson);
}
[TestMethod]
public void WriteFile_AppendToFile_WorkWithTemplatizedPaths()
{
var relativeDir = "somefile_{timestamp}.txt";
+
this.serviceUnderTest.WriteFile(relativeDir, "sampleText");
- this.serviceUnderTest.AppendToFile(relativeDir, "sampleText2");
+ this.serviceUnderTest.AppendToFile(relativeDir, SampleObject);
this.serviceUnderTest.Dispose();
+
var files = Directory.GetFiles(this.tempFolder);
- files
- .Should().NotBeEmpty();
- File.ReadAllText(files[0])
- .Should().Contain($"sampleTextsampleText2");
+ files.Should().NotBeEmpty();
+ File.ReadAllText(files[0]).Should().Contain($"sampleText{SampleObjectJson}");
this.VerifyTimestamp(files[0], "somefile_", ".txt");
}
@@ -76,7 +98,9 @@ public void WriteFile_AppendToFile_WorkWithTemplatizedPaths()
public void ResolveFilePath_ResolvedTemplatizedPaths()
{
var relativeDir = "someOtherFile_{timestamp}.txt";
+
this.serviceUnderTest.WriteFile(relativeDir, string.Empty);
+
var fullPath = this.serviceUnderTest.ResolveFilePath(relativeDir);
this.VerifyTimestamp(fullPath, "someOtherFile_", ".txt");
}
@@ -86,7 +110,8 @@ public void InitLogger_FailsOnDirectoryThatDoesNotExist()
{
var relativeDir = Guid.NewGuid();
var actualServiceUnderTest = new FileWritingService();
- Action action = () => actualServiceUnderTest.Init(Path.Combine(this.serviceUnderTest.BasePath, relativeDir.ToString()));
+
+ var action = () => actualServiceUnderTest.Init(Path.Combine(this.serviceUnderTest.BasePath, relativeDir.ToString()));
action.Should().Throw();
}