diff --git a/src/DataCore.Adapter.Core/Common/ByteString.cs b/src/DataCore.Adapter.Core/Common/ByteString.cs
new file mode 100644
index 00000000..d59110a2
--- /dev/null
+++ b/src/DataCore.Adapter.Core/Common/ByteString.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace DataCore.Adapter.Common {
+
+ ///
+ /// represents an immutable sequence of bytes.
+ ///
+ [JsonConverter(typeof(ByteStringConverter))]
+ public readonly struct ByteString : IEquatable {
+
+ ///
+ /// An empty instance.
+ ///
+ public static ByteString Empty => default;
+
+ ///
+ /// The underlying byte sequence.
+ ///
+ public ReadOnlyMemory Bytes { get; }
+
+ ///
+ /// The length of the byte sequence.
+ ///
+ public int Length => Bytes.Length;
+
+ ///
+ /// Specifies if the byte sequence is empty.
+ ///
+ public bool IsEmpty => Bytes.IsEmpty;
+
+
+ ///
+ /// Creates a new instance.
+ ///
+ ///
+ /// The byte sequence.
+ ///
+ public ByteString(ReadOnlyMemory bytes) {
+ Bytes = bytes;
+ }
+
+
+ ///
+ /// Creates a new instance.
+ ///
+ ///
+ /// The byte sequence.
+ ///
+ public ByteString(byte[] bytes) {
+ Bytes = bytes ?? Array.Empty();
+ }
+
+
+ ///
+ public override string ToString() {
+ if (Bytes.IsEmpty) {
+ return string.Empty;
+ }
+
+ if (MemoryMarshal.TryGetArray(Bytes, out ArraySegment segment)) {
+ return Convert.ToBase64String(segment.Array!, segment.Offset, segment.Count);
+ }
+ else {
+ return Convert.ToBase64String(Bytes.ToArray());
+ }
+ }
+
+
+ ///
+ public override int GetHashCode() {
+ // We need to calculate a hash code that distributes evenly across a hash space, but
+ // we don't want to have to iterate over the entire byte sequence to do so. Therefore,
+ // we will compute a hash code based on the following criteria:
+ //
+ // * Length of the byte sequence
+ // * First byte (non-empty byte sequences only)
+ // * Middle byte (non-empty byte sequences only)
+ // * Last byte (non-empty byte sequences only)
+
+ if (IsEmpty) {
+ return HashCode.Combine(0);
+ }
+
+ return HashCode.Combine(Length, Bytes.Span[0], Bytes.Span[(Bytes.Span.Length - 1) / 2], Bytes.Span[Bytes.Span.Length - 1]);
+ }
+
+
+ ///
+ public override bool Equals(object obj) {
+ return obj is ByteString other && Equals(other);
+ }
+
+
+ ///
+ public bool Equals(ByteString other) {
+ return Length == other.Length && Bytes.Span.SequenceEqual(other.Bytes.Span);
+ }
+
+
+ ///
+ public static implicit operator ByteString(ReadOnlyMemory bytes) => new ByteString(bytes);
+
+ ///
+ public static implicit operator ByteString(byte[] bytes) => new ByteString(bytes);
+
+ ///
+ public static implicit operator ReadOnlyMemory(ByteString bytes) => bytes.Bytes;
+
+ ///
+ public static implicit operator byte[](ByteString bytes) => bytes.Bytes.ToArray();
+
+ }
+
+
+ ///
+ /// JSON converter for .
+ ///
+ internal sealed class ByteStringConverter : JsonConverter {
+
+ ///
+ public override ByteString Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
+ if (reader.TokenType == JsonTokenType.Null) {
+ return ByteString.Empty;
+ }
+
+ if (reader.TokenType != JsonTokenType.String) {
+ throw new JsonException();
+ }
+
+ return new ByteString(reader.GetBytesFromBase64());
+ }
+
+
+ ///
+ public override void Write(Utf8JsonWriter writer, ByteString value, JsonSerializerOptions options) {
+ if (value.IsEmpty) {
+ writer.WriteNullValue();
+ return;
+ }
+ writer.WriteBase64StringValue(value.Bytes.Span);
+ }
+
+ }
+
+}
diff --git a/src/DataCore.Adapter.Core/Common/Variant.Operators.cs b/src/DataCore.Adapter.Core/Common/Variant.Operators.cs
index 9e9187f5..e35db444 100644
--- a/src/DataCore.Adapter.Core/Common/Variant.Operators.cs
+++ b/src/DataCore.Adapter.Core/Common/Variant.Operators.cs
@@ -56,6 +56,19 @@ partial struct Variant {
public static explicit operator byte[]?(Variant val) => (byte[]?) val.Value;
+ ///
+ public static implicit operator Variant(ByteString val) => new Variant(val);
+
+ ///
+ public static explicit operator ByteString(Variant val) => val.Value == null ? default : (ByteString) val.Value;
+
+ ///
+ public static implicit operator Variant(ByteString[]? val) => new Variant(val);
+
+ ///
+ public static explicit operator ByteString[]?(Variant val) => (ByteString[]?) val.Value;
+
+
///
public static implicit operator Variant(short val) => new Variant(val);
diff --git a/src/DataCore.Adapter.Core/Common/Variant.cs b/src/DataCore.Adapter.Core/Common/Variant.cs
index 2d87037a..3130e2e6 100644
--- a/src/DataCore.Adapter.Core/Common/Variant.cs
+++ b/src/DataCore.Adapter.Core/Common/Variant.cs
@@ -19,6 +19,7 @@ public partial struct Variant : IEquatable, IFormattable {
public static IReadOnlyDictionary VariantTypeMap { get; } = new System.Collections.ObjectModel.ReadOnlyDictionary(new Dictionary() {
[typeof(bool)] = VariantType.Boolean,
[typeof(byte)] = VariantType.Byte,
+ [typeof(ByteString)] = VariantType.ByteString,
[typeof(DateTime)] = VariantType.DateTime,
[typeof(double)] = VariantType.Double,
[typeof(EncodedObject)] = VariantType.ExtensionObject,
@@ -283,6 +284,43 @@ public Variant(byte[]? value) {
}
+ ///
+ /// Creates a new instance with the specified value.
+ ///
+ ///
+ /// The value.
+ ///
+ public Variant(ByteString value) {
+ Value = value;
+ Type = VariantType.ByteString;
+ ArrayDimensions = null;
+ }
+
+
+ ///
+ /// Creates a new instance with the specified array value.
+ ///
+ ///
+ /// The array value.
+ ///
+ ///
+ /// If is , the
+ /// will be equal to .
+ ///
+ public Variant(ByteString[]? value) {
+ if (value == null) {
+ Value = null;
+ Type = VariantType.Null;
+ ArrayDimensions = null;
+ return;
+ }
+
+ Value = value;
+ Type = VariantType.ByteString;
+ ArrayDimensions = GetArrayDimensions(value);
+ }
+
+
///
/// Creates a new instance with the specified value.
///
@@ -1085,7 +1123,6 @@ public string ToString(string? format, IFormatProvider? formatProvider) {
if (Value is string s) {
return s;
}
-
if (Value is Array a) {
return a.ToString();
}
@@ -1189,6 +1226,10 @@ public override Variant Read(ref Utf8JsonReader reader, Type typeToConvert, Json
return isArray
? new Variant(JsonExtensions.ReadArray(valueElement, arrayDimensions!, options))
: valueElement.Deserialize(options);
+ case VariantType.ByteString:
+ return isArray
+ ? new Variant(JsonExtensions.ReadArray(valueElement, arrayDimensions!, options))
+ : valueElement.Deserialize(options);
case VariantType.DateTime:
return isArray
? new Variant(JsonExtensions.ReadArray(valueElement, arrayDimensions!, options))
diff --git a/src/DataCore.Adapter.Core/Common/VariantType.cs b/src/DataCore.Adapter.Core/Common/VariantType.cs
index 060de858..b9cb966b 100644
--- a/src/DataCore.Adapter.Core/Common/VariantType.cs
+++ b/src/DataCore.Adapter.Core/Common/VariantType.cs
@@ -5,7 +5,6 @@ namespace DataCore.Adapter.Common {
///
/// Describes the type of a variant value.
///
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1720:Identifier contains type name", Justification = "Enum members all refer to data types")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum VariantType {
@@ -102,7 +101,12 @@ public enum VariantType {
///
/// JSON
///
- Json = 18
+ Json = 18,
+
+ ///
+ /// An immutable byte sequence.
+ ///
+ ByteString = 19,
}
diff --git a/src/DataCore.Adapter.Core/Json/AdapterJsonConverter.cs b/src/DataCore.Adapter.Core/Json/AdapterJsonConverter.cs
index 8c63b710..bb9e5a5e 100644
--- a/src/DataCore.Adapter.Core/Json/AdapterJsonConverter.cs
+++ b/src/DataCore.Adapter.Core/Json/AdapterJsonConverter.cs
@@ -13,13 +13,6 @@ namespace DataCore.Adapter.Json {
///
internal abstract class AdapterJsonConverter : JsonConverter {
- ///
- /// A flag indicating if is serliazed/deserialized as a JSON
- /// object.
- ///
- protected virtual bool SerializeAsObject { get; set; } = true;
-
-
///
/// Throws a to indicate that the JSON structure is invalid.
///
diff --git a/src/DataCore.Adapter.Core/PublicAPI.Unshipped.txt b/src/DataCore.Adapter.Core/PublicAPI.Unshipped.txt
index 85f172af..924c4d29 100644
--- a/src/DataCore.Adapter.Core/PublicAPI.Unshipped.txt
+++ b/src/DataCore.Adapter.Core/PublicAPI.Unshipped.txt
@@ -12,12 +12,23 @@ DataCore.Adapter.AssetModel.FindAssetModelNodesRequest.Page.get -> int
DataCore.Adapter.AssetModel.FindAssetModelNodesRequest.Page.set -> void
DataCore.Adapter.AssetModel.FindAssetModelNodesRequest.PageSize.get -> int
DataCore.Adapter.AssetModel.FindAssetModelNodesRequest.PageSize.set -> void
+DataCore.Adapter.Common.ByteString
+DataCore.Adapter.Common.ByteString.Bytes.get -> System.ReadOnlyMemory
+DataCore.Adapter.Common.ByteString.ByteString() -> void
+DataCore.Adapter.Common.ByteString.ByteString(byte[]! bytes) -> void
+DataCore.Adapter.Common.ByteString.ByteString(System.ReadOnlyMemory bytes) -> void
+DataCore.Adapter.Common.ByteString.Equals(DataCore.Adapter.Common.ByteString other) -> bool
+DataCore.Adapter.Common.ByteString.IsEmpty.get -> bool
+DataCore.Adapter.Common.ByteString.Length.get -> int
DataCore.Adapter.Common.FindAdaptersRequest.Page.get -> int
DataCore.Adapter.Common.FindAdaptersRequest.Page.set -> void
DataCore.Adapter.Common.FindAdaptersRequest.PageSize.get -> int
DataCore.Adapter.Common.FindAdaptersRequest.PageSize.set -> void
+DataCore.Adapter.Common.Variant.Variant(DataCore.Adapter.Common.ByteString value) -> void
+DataCore.Adapter.Common.Variant.Variant(DataCore.Adapter.Common.ByteString[]? value) -> void
DataCore.Adapter.Common.Variant.Variant(System.Text.Json.JsonElement value) -> void
DataCore.Adapter.Common.Variant.Variant(System.Text.Json.JsonElement[]? value) -> void
+DataCore.Adapter.Common.VariantType.ByteString = 19 -> DataCore.Adapter.Common.VariantType
DataCore.Adapter.DataValidation.MaxUriLengthAttribute
DataCore.Adapter.DataValidation.MaxUriLengthAttribute.Length.get -> int
DataCore.Adapter.DataValidation.MaxUriLengthAttribute.MaxUriLengthAttribute(int length) -> void
@@ -33,8 +44,20 @@ DataCore.Adapter.Tags.GetTagPropertiesRequest.Page.get -> int
DataCore.Adapter.Tags.GetTagPropertiesRequest.Page.set -> void
DataCore.Adapter.Tags.GetTagPropertiesRequest.PageSize.get -> int
DataCore.Adapter.Tags.GetTagPropertiesRequest.PageSize.set -> void
+override DataCore.Adapter.Common.ByteString.Equals(object! obj) -> bool
+override DataCore.Adapter.Common.ByteString.GetHashCode() -> int
+override DataCore.Adapter.Common.ByteString.ToString() -> string!
override DataCore.Adapter.Common.FindAdaptersRequest.Validate(System.ComponentModel.DataAnnotations.ValidationContext! validationContext) -> System.Collections.Generic.IEnumerable!
override DataCore.Adapter.DataValidation.MaxUriLengthAttribute.FormatErrorMessage(string! name) -> string!
+static DataCore.Adapter.Common.ByteString.Empty.get -> DataCore.Adapter.Common.ByteString
+static DataCore.Adapter.Common.ByteString.implicit operator byte[]!(DataCore.Adapter.Common.ByteString bytes) -> byte[]!
+static DataCore.Adapter.Common.ByteString.implicit operator DataCore.Adapter.Common.ByteString(byte[]! bytes) -> DataCore.Adapter.Common.ByteString
+static DataCore.Adapter.Common.ByteString.implicit operator DataCore.Adapter.Common.ByteString(System.ReadOnlyMemory bytes) -> DataCore.Adapter.Common.ByteString
+static DataCore.Adapter.Common.ByteString.implicit operator System.ReadOnlyMemory(DataCore.Adapter.Common.ByteString bytes) -> System.ReadOnlyMemory
+static DataCore.Adapter.Common.Variant.explicit operator DataCore.Adapter.Common.ByteString(DataCore.Adapter.Common.Variant val) -> DataCore.Adapter.Common.ByteString
+static DataCore.Adapter.Common.Variant.explicit operator DataCore.Adapter.Common.ByteString[]?(DataCore.Adapter.Common.Variant val) -> DataCore.Adapter.Common.ByteString[]?
+static DataCore.Adapter.Common.Variant.implicit operator DataCore.Adapter.Common.Variant(DataCore.Adapter.Common.ByteString val) -> DataCore.Adapter.Common.Variant
+static DataCore.Adapter.Common.Variant.implicit operator DataCore.Adapter.Common.Variant(DataCore.Adapter.Common.ByteString[]? val) -> DataCore.Adapter.Common.Variant
static DataCore.Adapter.Common.VariantExtensions.IsFloatingPointNumericType(this DataCore.Adapter.Common.Variant variant) -> bool
static DataCore.Adapter.Common.VariantExtensions.IsFloatingPointNumericType(this DataCore.Adapter.Common.VariantType variantType) -> bool
static DataCore.Adapter.RealTimeData.TagValueExtensions.IsFloatingPointNumericType(this DataCore.Adapter.RealTimeData.TagValue! value) -> bool
diff --git a/src/DataCore.Adapter.Json.Newtonsoft/ByteStringConverter.cs b/src/DataCore.Adapter.Json.Newtonsoft/ByteStringConverter.cs
new file mode 100644
index 00000000..0021f927
--- /dev/null
+++ b/src/DataCore.Adapter.Json.Newtonsoft/ByteStringConverter.cs
@@ -0,0 +1,39 @@
+using System;
+
+using DataCore.Adapter.Common;
+
+using Newtonsoft.Json;
+
+namespace DataCore.Adapter.NewtonsoftJson {
+
+ ///
+ /// JSON converter for .
+ ///
+ public class ByteStringConverter : JsonConverter {
+
+ ///
+ public override void WriteJson(JsonWriter writer, ByteString value, JsonSerializer serializer) {
+ if (value.IsEmpty) {
+ writer.WriteNull();
+ return;
+ }
+
+ writer.WriteValue(value.Bytes.ToArray());
+ }
+
+
+ ///
+ public override ByteString ReadJson(JsonReader reader, Type objectType, ByteString existingValue, bool hasExistingValue, JsonSerializer serializer) {
+ if (reader.TokenType == JsonToken.Null) {
+ return ByteString.Empty;
+ }
+
+ if (reader.TokenType != JsonToken.Bytes) {
+ throw new JsonException();
+ }
+
+ return new ByteString(reader.ReadAsBytes()!);
+ }
+
+ }
+}
diff --git a/src/DataCore.Adapter.Json.Newtonsoft/JsonSerializerSettingsExtensions.cs b/src/DataCore.Adapter.Json.Newtonsoft/JsonSerializerSettingsExtensions.cs
index 0ed167f4..21e8a811 100644
--- a/src/DataCore.Adapter.Json.Newtonsoft/JsonSerializerSettingsExtensions.cs
+++ b/src/DataCore.Adapter.Json.Newtonsoft/JsonSerializerSettingsExtensions.cs
@@ -79,6 +79,7 @@ public static void AddDataCoreAdapterConverters(this ICollection
converters.Add(new JsonElementConverter(jsonElementConverterOptions));
converters.Add(new NullableJsonElementConverter(jsonElementConverterOptions));
converters.Add(new VariantConverter());
+ converters.Add(new ByteStringConverter());
}
}
diff --git a/src/DataCore.Adapter.Json.Newtonsoft/PublicAPI.Unshipped.txt b/src/DataCore.Adapter.Json.Newtonsoft/PublicAPI.Unshipped.txt
index 957af52f..74a348b8 100644
--- a/src/DataCore.Adapter.Json.Newtonsoft/PublicAPI.Unshipped.txt
+++ b/src/DataCore.Adapter.Json.Newtonsoft/PublicAPI.Unshipped.txt
@@ -1,5 +1,9 @@
#nullable enable
+DataCore.Adapter.NewtonsoftJson.ByteStringConverter
+DataCore.Adapter.NewtonsoftJson.ByteStringConverter.ByteStringConverter() -> void
DataCore.Adapter.NewtonsoftJson.JsonElementConverter.JsonElementConverter(System.Text.Json.JsonSerializerOptions? options) -> void
DataCore.Adapter.NewtonsoftJson.NullableJsonElementConverter.NullableJsonElementConverter(System.Text.Json.JsonSerializerOptions? options) -> void
+override DataCore.Adapter.NewtonsoftJson.ByteStringConverter.ReadJson(Newtonsoft.Json.JsonReader! reader, System.Type! objectType, DataCore.Adapter.Common.ByteString existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer! serializer) -> DataCore.Adapter.Common.ByteString
+override DataCore.Adapter.NewtonsoftJson.ByteStringConverter.WriteJson(Newtonsoft.Json.JsonWriter! writer, DataCore.Adapter.Common.ByteString value, Newtonsoft.Json.JsonSerializer! serializer) -> void
static DataCore.Adapter.NewtonsoftJson.JsonSerializerSettingsExtensions.AddDataCoreAdapterConverters(this Newtonsoft.Json.JsonSerializerSettings! settings, System.Text.Json.JsonSerializerOptions? jsonElementConverterOptions) -> void
static DataCore.Adapter.NewtonsoftJson.JsonSerializerSettingsExtensions.AddDataCoreAdapterConverters(this System.Collections.Generic.ICollection! converters, System.Text.Json.JsonSerializerOptions? jsonElementConverterOptions) -> void
diff --git a/src/DataCore.Adapter.Json.Newtonsoft/VariantConverter.cs b/src/DataCore.Adapter.Json.Newtonsoft/VariantConverter.cs
index efa30cae..df56e3de 100644
--- a/src/DataCore.Adapter.Json.Newtonsoft/VariantConverter.cs
+++ b/src/DataCore.Adapter.Json.Newtonsoft/VariantConverter.cs
@@ -82,6 +82,10 @@ public override Variant ReadJson(JsonReader reader, Type objectType, Variant exi
return isArray
? new Variant(ReadArray(valueToken, arrayDimensions!, serializer))
: new Variant(valueToken.ToObject());
+ case VariantType.ByteString:
+ return isArray
+ ? new Variant(ReadArray(valueToken, arrayDimensions!, serializer))
+ : new Variant(valueToken.ToObject());
case VariantType.DateTime:
return isArray
? new Variant(ReadArray(valueToken, arrayDimensions!, serializer))
diff --git a/test/DataCore.Adapter.Tests/JsonTests.cs b/test/DataCore.Adapter.Tests/JsonTests.cs
index 7a508330..2745de50 100644
--- a/test/DataCore.Adapter.Tests/JsonTests.cs
+++ b/test/DataCore.Adapter.Tests/JsonTests.cs
@@ -101,6 +101,22 @@ public void Variant_ByteShouldRoundTrip(params byte[] values) {
}
+ [DataTestMethod]
+ [DataRow(false, byte.MinValue)]
+ [DataRow(false, byte.MaxValue)]
+ [DataRow(false, byte.MinValue, byte.MaxValue)]
+ [DataRow(true, byte.MinValue, byte.MaxValue)]
+ public void Variant_ByteStringShouldRoundTrip(bool arrayTest, params byte[] values) {
+ var options = GetOptions();
+ if (arrayTest) {
+ VariantRoundTripTest(values.Select(x => new ByteString(new[] { x })).ToArray(), options);
+ }
+ else {
+ VariantRoundTripTest((ByteString) values, options);
+ }
+ }
+
+
[TestMethod]
public void Variant_DateTimeShouldRoundTrip() {
var now = DateTime.UtcNow;
diff --git a/test/DataCore.Adapter.Tests/VariantTests.cs b/test/DataCore.Adapter.Tests/VariantTests.cs
index 59996554..9823c082 100644
--- a/test/DataCore.Adapter.Tests/VariantTests.cs
+++ b/test/DataCore.Adapter.Tests/VariantTests.cs
@@ -16,6 +16,7 @@ public class VariantTests : TestsBase {
[DataTestMethod]
[DataRow(typeof(bool), typeof(bool[]), typeof(bool[,]), typeof(bool[,,]))]
[DataRow(typeof(byte), typeof(byte[]), typeof(byte[,]), typeof(byte[,,]))]
+ [DataRow(typeof(ByteString), typeof(ByteString[]), typeof(ByteString[,]), typeof(ByteString[,,]))]
[DataRow(typeof(DateTime), typeof(DateTime[]), typeof(DateTime[,]), typeof(DateTime[,,]))]
[DataRow(typeof(double), typeof(double[]), typeof(double[,]), typeof(double[,,]))]
[DataRow(typeof(EncodedObject), typeof(EncodedObject[]), typeof(EncodedObject[,]), typeof(EncodedObject[,,]))]
@@ -102,7 +103,7 @@ public void VariantShouldAllowImplicitConversionFromByte() {
[TestMethod]
public void VariantShouldAllowImplicitConversionFromByteArray() {
- byte[] value = new byte [] { 255, 254 };
+ byte[] value = new byte[] { 255, 254 };
Variant variant = value;
ValidateVariant(variant, VariantType.Byte, value, new[] { value.Length });
}
@@ -126,6 +127,46 @@ public void VariantShouldAllowExplicitConversionToByteArray() {
}
+ [TestMethod]
+ public void VariantShouldAllowImplicitConversionFromByteString() {
+ ByteString value = new byte[] { 255, 254 };
+ Variant variant = value;
+ ValidateVariant(variant, VariantType.ByteString, value, null);
+ }
+
+
+ [TestMethod]
+ public void VariantShouldAllowImplicitConversionFromByteStringArray() {
+ ByteString[] value = {
+ (ByteString) new byte[] { 255, 254 },
+ (ByteString) new byte[] { 128, 127 }
+ };
+ Variant variant = value;
+ ValidateVariant(variant, VariantType.ByteString, value, new[] { value.Length });
+ }
+
+
+ [TestMethod]
+ public void VariantShouldAllowExplicitConversionToByteString() {
+ ByteString value = new byte[] { 255, 254 };
+ Variant variant = value;
+ var actualValue = (ByteString) variant;
+ Assert.AreEqual(value, actualValue);
+ }
+
+
+ [TestMethod]
+ public void VariantShouldAllowExplicitConversionToByteStringArray() {
+ ByteString[] value = {
+ (ByteString) new byte[] { 255, 254 },
+ (ByteString) new byte[] { 128, 127 }
+ };
+ Variant variant = value;
+ var actualValue = (ByteString[]) variant;
+ Assert.IsTrue(value.SequenceEqual(actualValue));
+ }
+
+
[TestMethod]
public void VariantShouldAllowImplicitConversionFromDateTime() {
var value = DateTime.UtcNow;