Files
BizTalkPlatformManagementTool/src/BizTalkPlatformManagementTool/Services/JsonFileStore.cs
T

193 lines
6.5 KiB
C#

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
namespace BizTalkPlatformManagementTool.Services
{
/// <summary>
/// Persists DataContract models as JSON with repository-defined encoding rules.
/// </summary>
public static class JsonFileStore
{
/// <summary>
/// UTF-8 encoding instance that writes JSON without a byte order mark.
/// </summary>
private static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
/// <summary>
/// Serializes a value to a UTF-8 JSON file without a byte order mark.
/// </summary>
/// <typeparam name="T">The model type to serialize.</typeparam>
/// <param name="path">The target JSON file path.</param>
/// <param name="value">The value to serialize.</param>
public static void Save<T>(string path, T value)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("A JSON target path is required.", "path");
}
var fullPath = Path.GetFullPath(path);
var directory = Path.GetDirectoryName(fullPath);
Directory.CreateDirectory(directory);
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
{
UseSimpleDictionaryFormat = true
});
using (var stream = new MemoryStream())
{
serializer.WriteObject(stream, value);
var json = Utf8NoBom.GetString(stream.ToArray());
WriteAtomically(fullPath, json);
}
}
/// <summary>
/// Loads a JSON file into the requested DataContract model type.
/// </summary>
/// <typeparam name="T">The model type to deserialize.</typeparam>
/// <param name="path">The JSON file path to load.</param>
/// <returns>The deserialized model.</returns>
public static T Load<T>(string path)
{
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
{
UseSimpleDictionaryFormat = true
});
var json = NormalizeJson(File.ReadAllText(path, Encoding.UTF8));
using (var stream = new MemoryStream(Utf8NoBom.GetBytes(json)))
{
try
{
return (T)serializer.ReadObject(stream);
}
catch (SerializationException ex)
{
throw new InvalidDataException("Failed to load JSON file '" + path + "'. " + DescribeJsonStart(json), ex);
}
}
}
/// <summary>
/// Removes byte order mark variants that may exist in previously written files.
/// </summary>
/// <param name="json">The raw JSON text read from disk.</param>
/// <returns>The JSON text without a leading BOM marker.</returns>
private static string NormalizeJson(string json)
{
if (string.IsNullOrEmpty(json))
{
return json;
}
if (json[0] == '\uFEFF')
{
json = json.Substring(1);
}
if (json.StartsWith("\u00EF\u00BB\u00BF"))
{
json = json.Substring(3);
}
return json;
}
/// <summary>
/// Builds a short diagnostic message for a JSON deserialization failure.
/// </summary>
/// <param name="json">The normalized JSON text that failed to deserialize.</param>
/// <returns>A diagnostic suffix describing the beginning of the JSON content.</returns>
private static string DescribeJsonStart(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return "The file is empty or contains only whitespace.";
}
var trimmed = json.TrimStart();
var first = trimmed.Length == 0 ? '\0' : trimmed[0];
if (first != '{' && first != '[')
{
return "The first JSON character is '" + first + "' instead of '{' or '['.";
}
return "The file starts with valid JSON syntax but could not be deserialized into the expected model.";
}
/// <summary>
/// Writes a file through a same-directory temporary file so an interrupted
/// save cannot leave a truncated snapshot or operation plan behind.
/// </summary>
private static void WriteAtomically(string path, string content)
{
var temporaryPath = path + ".tmp." + Guid.NewGuid().ToString("N");
var backupPath = path + ".bak." + Guid.NewGuid().ToString("N");
try
{
File.WriteAllText(temporaryPath, content, Utf8NoBom);
if (!File.Exists(path))
{
File.Move(temporaryPath, path);
return;
}
try
{
File.Replace(temporaryPath, path, backupPath, true);
TryDelete(backupPath);
}
catch (PlatformNotSupportedException)
{
ReplaceWithRenameFallback(path, temporaryPath, backupPath);
}
catch (NotSupportedException)
{
ReplaceWithRenameFallback(path, temporaryPath, backupPath);
}
}
finally
{
TryDelete(temporaryPath);
}
}
private static void ReplaceWithRenameFallback(string path, string temporaryPath, string backupPath)
{
File.Move(path, backupPath);
try
{
File.Move(temporaryPath, path);
TryDelete(backupPath);
}
catch
{
if (!File.Exists(path) && File.Exists(backupPath))
{
File.Move(backupPath, path);
}
throw;
}
}
private static void TryDelete(string path)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch
{
// Temporary cleanup is best-effort and must not hide the save result.
}
}
}
}