feat: use `Newtonsoft.Json` streaming methods (#596)

This commit is contained in:
Jamie Magee 2023-06-06 13:28:34 -07:00 коммит произвёл GitHub
Родитель e89a151051
Коммит cb6204f542
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 138 добавлений и 50 удалений

Просмотреть файл

@ -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;
/// <inheritdoc />
public sealed class FileWritingService : IFileWritingService
{
/// <summary>
/// The format string used to generate the timestamp for the manifest file.
/// </summary>
public const string TimestampFormatString = "yyyyMMddHHmmssfff";
private readonly object lockObject = new object();
private readonly string timestamp = DateTime.Now.ToString(TimestampFormatString);
private readonly ConcurrentDictionary<string, StreamWriter> bufferedStreams = new();
private readonly object lockObject = new();
private readonly string timestamp = DateTime.Now.ToString(TimestampFormatString, CultureInfo.InvariantCulture);
/// <summary>
/// The base path to write files to.
/// If null or empty, the temp path will be used.
/// </summary>
public string BasePath { get; private set; }
/// <inheritdoc />
public void Init(string basePath)
{
if (!string.IsNullOrEmpty(basePath) && !Directory.Exists(basePath))
@ -25,19 +37,25 @@ public sealed class FileWritingService : IFileWritingService
this.BasePath = string.IsNullOrEmpty(basePath) ? Path.GetTempPath() : basePath;
}
public void AppendToFile(string relativeFilePath, string text)
/// <inheritdoc />
public void AppendToFile<T>(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);
}
/// <inheritdoc />
public void WriteFile(string relativeFilePath, string text)
{
relativeFilePath = this.ResolveFilePath(relativeFilePath);
@ -48,6 +66,7 @@ public sealed class FileWritingService : IFileWritingService
}
}
/// <inheritdoc />
public async Task WriteFileAsync(string relativeFilePath, string text)
{
relativeFilePath = this.ResolveFilePath(relativeFilePath);
@ -55,23 +74,44 @@ public sealed class FileWritingService : IFileWritingService
await File.WriteAllTextAsync(relativeFilePath, text);
}
public void WriteFile(FileInfo relativeFilePath, string text)
/// <inheritdoc />
public void WriteFile<T>(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);
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public void Dispose() => this.Dispose(true);
/// <inheritdoc />
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 @@ public sealed class FileWritingService : IFileWritingService
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 _);
}
}
}

Просмотреть файл

@ -4,18 +4,53 @@ using System;
using System.IO;
using System.Threading.Tasks;
// All file paths are relative and will replace occurrences of {timestamp} with the shared file timestamp.
/// <summary>
/// Provides methods for writing files.
/// </summary>
public interface IFileWritingService : IDisposable, IAsyncDisposable
{
/// <summary>
/// Initializes the file writing service with the given base path.
/// </summary>
/// <param name="basePath">The base path to use for all file operations.</param>
void Init(string basePath);
void AppendToFile(string relativeFilePath, string text);
/// <summary>
/// Appends the object to the file as JSON.
/// </summary>
/// <param name="relativeFilePath">The relative path to the file.</param>
/// <param name="obj">The object to append.</param>
/// <typeparam name="T">The type of the object to append.</typeparam>
void AppendToFile<T>(string relativeFilePath, T obj);
/// <summary>
/// Writes the text to the file.
/// </summary>
/// <param name="relativeFilePath">The relative path to the file.</param>
/// <param name="text">The text to write.</param>
void WriteFile(string relativeFilePath, string text);
/// <summary>
/// Writes the text to the file.
/// </summary>
/// <param name="relativeFilePath">The relative path to the file.</param>
/// <param name="text">The text to write.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task WriteFileAsync(string relativeFilePath, string text);
void WriteFile(FileInfo relativeFilePath, string text);
/// <summary>
/// Writes the object to the file as JSON.
/// </summary>
/// <param name="relativeFilePath">The relative path to the file.</param>
/// <param name="obj">The object to write.</param>
/// <typeparam name="T">The type of the object to write.</typeparam>
void WriteFile<T>(FileInfo relativeFilePath, T obj);
/// <summary>
/// Resolves the complete file path from the given relative file path.
/// Replaces occurrences of {timestamp} with the shared file timestamp.
/// </summary>
/// <param name="relativeFilePath">The relative path to the file.</param>
/// <returns>The complete file path.</returns>
string ResolveFilePath(string relativeFilePath);
}

Просмотреть файл

@ -54,20 +54,23 @@ public class BcdeScanCommandService : IArgumentHandlingService
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);
}
}
}

Просмотреть файл

@ -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 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestCategory("Governance/ComponentDetection")]
public class FileWritingServiceTests
{
private const string SampleObjectJson = @"{
""key1"": ""value1"",
""key2"": ""value2""
}";
private static readonly IDictionary<string, string> SampleObject = new Dictionary<string, string>
{
{ "key1", "value1" },
{ "key2", "value2" },
};
private FileWritingService serviceUnderTest;
private string tempFolder;
@ -29,22 +42,20 @@ public class FileWritingServiceTests
}
[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 class FileWritingServiceTests
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 class FileWritingServiceTests
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 class FileWritingServiceTests
{
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<InvalidUserInputException>();
}