Initialize CBDD solution and add a .NET-focused gitignore for generated artifacts.

This commit is contained in:
Joseph Doherty
2026-02-20 12:54:07 -05:00
commit b8ed5ec500
214 changed files with 101452 additions and 0 deletions

14
src/CBDD.Bson/Attributes.cs Executable file
View File

@@ -0,0 +1,14 @@
using System;
namespace ZB.MOM.WW.CBDD.Bson
{
[AttributeUsage(AttributeTargets.Property)]
public class BsonIdAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Property)]
public class BsonIgnoreAttribute : Attribute
{
}
}

262
src/CBDD.Bson/BsonBufferWriter.cs Executable file
View File

@@ -0,0 +1,262 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Text;
namespace ZB.MOM.WW.CBDD.Bson;
/// <summary>
/// BSON writer that serializes to an IBufferWriter, enabling streaming serialization
/// without fixed buffer size limits.
/// </summary>
public ref struct BsonBufferWriter
{
private IBufferWriter<byte> _writer;
private int _totalBytesWritten;
/// <summary>
/// Initializes a new instance of the <see cref="BsonBufferWriter"/> struct.
/// </summary>
/// <param name="writer">The buffer writer to write BSON bytes to.</param>
public BsonBufferWriter(IBufferWriter<byte> writer)
{
_writer = writer;
_totalBytesWritten = 0;
}
/// <summary>
/// Gets the current write position in bytes.
/// </summary>
public int Position => _totalBytesWritten;
private void WriteBytes(ReadOnlySpan<byte> data)
{
var destination = _writer.GetSpan(data.Length);
data.CopyTo(destination);
_writer.Advance(data.Length);
_totalBytesWritten += data.Length;
}
private void WriteByte(byte value)
{
var span = _writer.GetSpan(1);
span[0] = value;
_writer.Advance(1);
_totalBytesWritten++;
}
/// <summary>
/// Writes a BSON date-time field.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The date-time value.</param>
public void WriteDateTime(string name, DateTime value)
{
WriteByte((byte)BsonType.DateTime);
WriteCString(name);
// BSON DateTime: milliseconds since Unix epoch (UTC)
var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var milliseconds = (long)(value.ToUniversalTime() - unixEpoch).TotalMilliseconds;
WriteInt64Internal(milliseconds);
}
/// <summary>
/// Begins writing a BSON document.
/// </summary>
/// <returns>The position where the document size placeholder was written.</returns>
public int BeginDocument()
{
// Write placeholder for size (4 bytes)
var sizePosition = _totalBytesWritten;
var span = _writer.GetSpan(4);
// Initialize with default value (will be patched later)
span[0] = 0; span[1] = 0; span[2] = 0; span[3] = 0;
_writer.Advance(4);
_totalBytesWritten += 4;
return sizePosition;
}
/// <summary>
/// Ends the current BSON document by writing the document terminator.
/// </summary>
/// <param name="sizePosition">The position of the size placeholder for this document.</param>
public void EndDocument(int sizePosition)
{
// Write document terminator
WriteByte(0);
// Note: Size patching must be done by caller after accessing WrittenSpan
// from ArrayBufferWriter (or equivalent)
}
/// <summary>
/// Begins writing a nested BSON document field.
/// </summary>
/// <param name="name">The field name.</param>
/// <returns>The position where the nested document size placeholder was written.</returns>
public int BeginDocument(string name)
{
WriteByte((byte)BsonType.Document);
WriteCString(name);
return BeginDocument();
}
/// <summary>
/// Begins writing a BSON array field.
/// </summary>
/// <param name="name">The field name.</param>
/// <returns>The position where the array document size placeholder was written.</returns>
public int BeginArray(string name)
{
WriteByte((byte)BsonType.Array);
WriteCString(name);
return BeginDocument();
}
/// <summary>
/// Ends the current BSON array.
/// </summary>
/// <param name="sizePosition">The position of the size placeholder for this array.</param>
public void EndArray(int sizePosition)
{
EndDocument(sizePosition);
}
// Private helper methods
private void WriteInt32Internal(int value)
{
var span = _writer.GetSpan(4);
BinaryPrimitives.WriteInt32LittleEndian(span, value);
_writer.Advance(4);
_totalBytesWritten += 4;
}
private void WriteInt64Internal(long value)
{
var span = _writer.GetSpan(8);
BinaryPrimitives.WriteInt64LittleEndian(span, value);
_writer.Advance(8);
_totalBytesWritten += 8;
}
/// <summary>
/// Writes a BSON ObjectId field.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The ObjectId value.</param>
public void WriteObjectId(string name, ObjectId value)
{
WriteByte((byte)BsonType.ObjectId);
WriteCString(name);
WriteBytes(value.ToByteArray());
}
/// <summary>
/// Writes a BSON string field.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The string value.</param>
public void WriteString(string name, string value)
{
WriteByte((byte)BsonType.String);
WriteCString(name);
WriteStringValue(value);
}
/// <summary>
/// Writes a BSON boolean field.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The boolean value.</param>
public void WriteBoolean(string name, bool value)
{
WriteByte((byte)BsonType.Boolean);
WriteCString(name);
WriteByte((byte)(value ? 1 : 0));
}
/// <summary>
/// Writes a BSON null field.
/// </summary>
/// <param name="name">The field name.</param>
public void WriteNull(string name)
{
WriteByte((byte)BsonType.Null);
WriteCString(name);
}
private void WriteStringValue(string value)
{
// String: length (int32) + UTF8 bytes + null terminator
var bytes = Encoding.UTF8.GetBytes(value);
WriteInt32Internal(bytes.Length + 1); // +1 for null terminator
WriteBytes(bytes);
WriteByte(0);
}
private void WriteDoubleInternal(double value)
{
var span = _writer.GetSpan(8);
BinaryPrimitives.WriteDoubleLittleEndian(span, value);
_writer.Advance(8);
_totalBytesWritten += 8;
}
/// <summary>
/// Writes a BSON binary field.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="data">The binary data.</param>
public void WriteBinary(string name, ReadOnlySpan<byte> data)
{
WriteByte((byte)BsonType.Binary);
WriteCString(name);
WriteInt32Internal(data.Length);
WriteByte(0); // Binary subtype: Generic
WriteBytes(data);
}
/// <summary>
/// Writes a BSON 64-bit integer field.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The 64-bit integer value.</param>
public void WriteInt64(string name, long value)
{
WriteByte((byte)BsonType.Int64);
WriteCString(name);
WriteInt64Internal(value);
}
/// <summary>
/// Writes a BSON double field.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The double value.</param>
public void WriteDouble(string name, double value)
{
WriteByte((byte)BsonType.Double);
WriteCString(name);
WriteDoubleInternal(value);
}
private void WriteCString(string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
WriteBytes(bytes);
WriteByte(0); // Null terminator
}
/// <summary>
/// Writes a BSON 32-bit integer field.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The 32-bit integer value.</param>
public void WriteInt32(string name, int value)
{
WriteByte((byte)BsonType.Int32);
WriteCString(name);
WriteInt32Internal(value);
}
}

292
src/CBDD.Bson/BsonDocument.cs Executable file
View File

@@ -0,0 +1,292 @@
using System;
namespace ZB.MOM.WW.CBDD.Bson;
/// <summary>
/// Represents an in-memory BSON document with lazy parsing.
/// Uses Memory&lt;byte&gt; to store raw BSON data for zero-copy operations.
/// </summary>
public sealed class BsonDocument
{
private readonly Memory<byte> _rawData;
private readonly System.Collections.Concurrent.ConcurrentDictionary<ushort, string>? _keys;
/// <summary>
/// Initializes a new instance of the <see cref="BsonDocument"/> class from raw BSON memory.
/// </summary>
/// <param name="rawBsonData">The raw BSON data.</param>
/// <param name="keys">The optional key dictionary.</param>
public BsonDocument(Memory<byte> rawBsonData, System.Collections.Concurrent.ConcurrentDictionary<ushort, string>? keys = null)
{
_rawData = rawBsonData;
_keys = keys;
}
/// <summary>
/// Initializes a new instance of the <see cref="BsonDocument"/> class from raw BSON bytes.
/// </summary>
/// <param name="rawBsonData">The raw BSON data.</param>
/// <param name="keys">The optional key dictionary.</param>
public BsonDocument(byte[] rawBsonData, System.Collections.Concurrent.ConcurrentDictionary<ushort, string>? keys = null)
{
_rawData = rawBsonData;
_keys = keys;
}
/// <summary>
/// Gets the raw BSON bytes
/// </summary>
public ReadOnlySpan<byte> RawData => _rawData.Span;
/// <summary>
/// Gets the document size in bytes
/// </summary>
public int Size => BitConverter.ToInt32(_rawData.Span[..4]);
/// <summary>
/// Creates a reader for this document
/// </summary>
public BsonSpanReader GetReader() => new BsonSpanReader(_rawData.Span, _keys ?? new System.Collections.Concurrent.ConcurrentDictionary<ushort, string>());
/// <summary>
/// Tries to get a field value by name.
/// Returns false if field not found.
/// </summary>
/// <param name="fieldName">The field name.</param>
/// <param name="value">When this method returns, contains the field value if found; otherwise <see langword="null"/>.</param>
/// <returns><see langword="true"/> if the field is found; otherwise, <see langword="false"/>.</returns>
public bool TryGetString(string fieldName, out string? value)
{
value = null;
var reader = GetReader();
if (reader.Remaining < 5)
return false;
_ = reader.ReadDocumentSize();
fieldName = fieldName.ToLowerInvariant();
while (reader.Remaining > 1)
{
var type = reader.ReadBsonType();
if (type == BsonType.EndOfDocument)
break;
var name = reader.ReadElementHeader();
if (name == fieldName && type == BsonType.String)
{
value = reader.ReadString();
return true;
}
reader.SkipValue(type);
}
return false;
}
/// <summary>
/// Tries to get an Int32 field value by name.
/// </summary>
/// <param name="fieldName">The field name.</param>
/// <param name="value">When this method returns, contains the field value if found; otherwise zero.</param>
/// <returns><see langword="true"/> if the field is found; otherwise, <see langword="false"/>.</returns>
public bool TryGetInt32(string fieldName, out int value)
{
value = 0;
var reader = GetReader();
if (reader.Remaining < 5)
return false;
_ = reader.ReadDocumentSize();
fieldName = fieldName.ToLowerInvariant();
while (reader.Remaining > 1)
{
var type = reader.ReadBsonType();
if (type == BsonType.EndOfDocument)
break;
var name = reader.ReadElementHeader();
if (name == fieldName && type == BsonType.Int32)
{
value = reader.ReadInt32();
return true;
}
reader.SkipValue(type);
}
return false;
}
/// <summary>
/// Tries to get an ObjectId field value by name.
/// </summary>
/// <param name="fieldName">The field name.</param>
/// <param name="value">When this method returns, contains the field value if found; otherwise default.</param>
/// <returns><see langword="true"/> if the field is found; otherwise, <see langword="false"/>.</returns>
public bool TryGetObjectId(string fieldName, out ObjectId value)
{
value = default;
var reader = GetReader();
if (reader.Remaining < 5)
return false;
_ = reader.ReadDocumentSize();
fieldName = fieldName.ToLowerInvariant();
while (reader.Remaining > 1)
{
var type = reader.ReadBsonType();
if (type == BsonType.EndOfDocument)
break;
var name = reader.ReadElementHeader();
if (name == fieldName && type == BsonType.ObjectId)
{
value = reader.ReadObjectId();
return true;
}
reader.SkipValue(type);
}
return false;
}
/// <summary>
/// Creates a new BsonDocument from field values using a builder pattern
/// </summary>
/// <param name="keyMap">The key map used for field name encoding.</param>
/// <param name="buildAction">The action that populates the builder.</param>
/// <returns>The created BSON document.</returns>
public static BsonDocument Create(System.Collections.Concurrent.ConcurrentDictionary<string, ushort> keyMap, Action<BsonDocumentBuilder> buildAction)
{
var builder = new BsonDocumentBuilder(keyMap);
buildAction(builder);
return builder.Build();
}
}
/// <summary>
/// Builder for creating BSON documents
/// </summary>
public sealed class BsonDocumentBuilder
{
private byte[] _buffer = new byte[1024]; // Start with 1KB
private int _position;
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, ushort> _keyMap;
/// <summary>
/// Initializes a new instance of the <see cref="BsonDocumentBuilder"/> class.
/// </summary>
/// <param name="keyMap">The key map used for field name encoding.</param>
public BsonDocumentBuilder(System.Collections.Concurrent.ConcurrentDictionary<string, ushort> keyMap)
{
_keyMap = keyMap;
var writer = new BsonSpanWriter(_buffer, _keyMap);
_position = writer.Position;
}
/// <summary>
/// Adds a string field to the document.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The field value.</param>
/// <returns>The current builder.</returns>
public BsonDocumentBuilder AddString(string name, string value)
{
EnsureCapacity(256); // Conservative estimate
var writer = new BsonSpanWriter(_buffer.AsSpan(_position..), _keyMap);
writer.WriteString(name, value);
_position += writer.Position;
return this;
}
/// <summary>
/// Adds an Int32 field to the document.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The field value.</param>
/// <returns>The current builder.</returns>
public BsonDocumentBuilder AddInt32(string name, int value)
{
EnsureCapacity(64);
var writer = new BsonSpanWriter(_buffer.AsSpan(_position..), _keyMap);
writer.WriteInt32(name, value);
_position += writer.Position;
return this;
}
/// <summary>
/// Adds an Int64 field to the document.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The field value.</param>
/// <returns>The current builder.</returns>
public BsonDocumentBuilder AddInt64(string name, long value)
{
EnsureCapacity(64);
var writer = new BsonSpanWriter(_buffer.AsSpan(_position..), _keyMap);
writer.WriteInt64(name, value);
_position += writer.Position;
return this;
}
/// <summary>
/// Adds a Boolean field to the document.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The field value.</param>
/// <returns>The current builder.</returns>
public BsonDocumentBuilder AddBoolean(string name, bool value)
{
EnsureCapacity(64);
var writer = new BsonSpanWriter(_buffer.AsSpan(_position..), _keyMap);
writer.WriteBoolean(name, value);
_position += writer.Position;
return this;
}
/// <summary>
/// Adds an ObjectId field to the document.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The field value.</param>
/// <returns>The current builder.</returns>
public BsonDocumentBuilder AddObjectId(string name, ObjectId value)
{
EnsureCapacity(64);
var writer = new BsonSpanWriter(_buffer.AsSpan(_position..), _keyMap);
writer.WriteObjectId(name, value);
_position += writer.Position;
return this;
}
/// <summary>
/// Builds a BSON document from the accumulated fields.
/// </summary>
/// <returns>The constructed BSON document.</returns>
public BsonDocument Build()
{
// Layout: [int32 size][field bytes...][0x00 terminator]
var totalSize = _position + 5;
var finalBuffer = new byte[totalSize];
BitConverter.TryWriteBytes(finalBuffer.AsSpan(0, 4), totalSize);
_buffer.AsSpan(0, _position).CopyTo(finalBuffer.AsSpan(4, _position));
finalBuffer[totalSize - 1] = 0;
return new BsonDocument(finalBuffer);
}
private void EnsureCapacity(int additional)
{
if (_position + additional > _buffer.Length)
{
var newBuffer = new byte[_buffer.Length * 2];
_buffer.CopyTo(newBuffer, 0);
_buffer = newBuffer;
}
}
}

396
src/CBDD.Bson/BsonSpanReader.cs Executable file
View File

@@ -0,0 +1,396 @@
using System;
using System.Buffers.Binary;
using System.Text;
namespace ZB.MOM.WW.CBDD.Bson;
/// <summary>
/// Zero-allocation BSON reader using ReadOnlySpan&lt;byte&gt;.
/// Implemented as ref struct to ensure stack-only allocation.
/// </summary>
public ref struct BsonSpanReader
{
private ReadOnlySpan<byte> _buffer;
private int _position;
private readonly System.Collections.Concurrent.ConcurrentDictionary<ushort, string> _keys;
/// <summary>
/// Initializes a new instance of the <see cref="BsonSpanReader"/> struct.
/// </summary>
/// <param name="buffer">The BSON buffer to read.</param>
/// <param name="keys">The reverse key dictionary used for compressed element headers.</param>
public BsonSpanReader(ReadOnlySpan<byte> buffer, System.Collections.Concurrent.ConcurrentDictionary<ushort, string> keys)
{
_buffer = buffer;
_position = 0;
_keys = keys;
}
/// <summary>
/// Gets the current read position in the buffer.
/// </summary>
public int Position => _position;
/// <summary>
/// Gets the number of unread bytes remaining in the buffer.
/// </summary>
public int Remaining => _buffer.Length - _position;
/// <summary>
/// Reads the document size (first 4 bytes of a BSON document)
/// </summary>
public int ReadDocumentSize()
{
if (Remaining < 4)
throw new InvalidOperationException("Not enough bytes to read document size");
var size = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4));
_position += 4;
return size;
}
/// <summary>
/// Reads a BSON element type
/// </summary>
public BsonType ReadBsonType()
{
if (Remaining < 1)
throw new InvalidOperationException("Not enough bytes to read BSON type");
var type = (BsonType)_buffer[_position];
_position++;
return type;
}
/// <summary>
/// Reads a C-style null-terminated string (e-name in BSON spec)
/// </summary>
public string ReadCString()
{
var start = _position;
while (_position < _buffer.Length && _buffer[_position] != 0)
_position++;
if (_position >= _buffer.Length)
throw new InvalidOperationException("Unterminated C-string");
var nameBytes = _buffer.Slice(start, _position - start);
_position++; // Skip null terminator
return Encoding.UTF8.GetString(nameBytes);
}
/// <summary>
/// Reads a C-string into a destination span. Returns the number of bytes written.
/// </summary>
/// <param name="destination">The destination character span.</param>
public int ReadCString(Span<char> destination)
{
var start = _position;
while (_position < _buffer.Length && _buffer[_position] != 0)
_position++;
if (_position >= _buffer.Length)
throw new InvalidOperationException("Unterminated C-string");
var nameBytes = _buffer.Slice(start, _position - start);
_position++; // Skip null terminator
return Encoding.UTF8.GetChars(nameBytes, destination);
}
/// <summary>
/// Reads a BSON string (4-byte length + UTF-8 bytes + null terminator)
/// </summary>
public string ReadString()
{
var length = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4));
_position += 4;
if (length < 1)
throw new InvalidOperationException("Invalid string length");
var stringBytes = _buffer.Slice(_position, length - 1); // Exclude null terminator
_position += length;
return Encoding.UTF8.GetString(stringBytes);
}
/// <summary>
/// Reads a 32-bit integer.
/// </summary>
public int ReadInt32()
{
if (Remaining < 4)
throw new InvalidOperationException("Not enough bytes to read Int32");
var value = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4));
_position += 4;
return value;
}
/// <summary>
/// Reads a 64-bit integer.
/// </summary>
public long ReadInt64()
{
if (Remaining < 8)
throw new InvalidOperationException("Not enough bytes to read Int64");
var value = BinaryPrimitives.ReadInt64LittleEndian(_buffer.Slice(_position, 8));
_position += 8;
return value;
}
/// <summary>
/// Reads a double-precision floating point value.
/// </summary>
public double ReadDouble()
{
if (Remaining < 8)
throw new InvalidOperationException("Not enough bytes to read Double");
var value = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(_position, 8));
_position += 8;
return value;
}
/// <summary>
/// Reads spatial coordinates from a BSON array [X, Y].
/// Returns a (double, double) tuple.
/// </summary>
public (double, double) ReadCoordinates()
{
// Skip array size (4 bytes)
_position += 4;
// Skip element 0 header: Type(1) + Name("0\0") (3 bytes)
_position += 3;
var x = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(_position, 8));
_position += 8;
// Skip element 1 header: Type(1) + Name("1\0") (3 bytes)
_position += 3;
var y = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(_position, 8));
_position += 8;
// Skip end of array marker (1 byte)
_position++;
return (x, y);
}
/// <summary>
/// Reads a Decimal128 value.
/// </summary>
public decimal ReadDecimal128()
{
if (Remaining < 16)
throw new InvalidOperationException("Not enough bytes to read Decimal128");
var bits = new int[4];
bits[0] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4));
bits[1] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position + 4, 4));
bits[2] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position + 8, 4));
bits[3] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position + 12, 4));
_position += 16;
return new decimal(bits);
}
/// <summary>
/// Reads a boolean value.
/// </summary>
public bool ReadBoolean()
{
if (Remaining < 1)
throw new InvalidOperationException("Not enough bytes to read Boolean");
var value = _buffer[_position] != 0;
_position++;
return value;
}
/// <summary>
/// Reads a BSON DateTime (UTC milliseconds since Unix epoch)
/// </summary>
public DateTime ReadDateTime()
{
var milliseconds = ReadInt64();
return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).UtcDateTime;
}
/// <summary>
/// Reads a BSON DateTime as DateTimeOffset (UTC milliseconds since Unix epoch)
/// </summary>
public DateTimeOffset ReadDateTimeOffset()
{
var milliseconds = ReadInt64();
return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds);
}
/// <summary>
/// Reads a TimeSpan from BSON Int64 (ticks)
/// </summary>
public TimeSpan ReadTimeSpan()
{
var ticks = ReadInt64();
return TimeSpan.FromTicks(ticks);
}
/// <summary>
/// Reads a DateOnly from BSON Int32 (day number)
/// </summary>
public DateOnly ReadDateOnly()
{
var dayNumber = ReadInt32();
return DateOnly.FromDayNumber(dayNumber);
}
/// <summary>
/// Reads a TimeOnly from BSON Int64 (ticks)
/// </summary>
public TimeOnly ReadTimeOnly()
{
var ticks = ReadInt64();
return new TimeOnly(ticks);
}
/// <summary>
/// Reads a GUID value.
/// </summary>
public Guid ReadGuid()
{
return Guid.Parse(ReadString());
}
/// <summary>
/// Reads a BSON ObjectId (12 bytes)
/// </summary>
public ObjectId ReadObjectId()
{
if (Remaining < 12)
throw new InvalidOperationException("Not enough bytes to read ObjectId");
var oidBytes = _buffer.Slice(_position, 12);
_position += 12;
return new ObjectId(oidBytes);
}
/// <summary>
/// Reads binary data (subtype + length + bytes)
/// </summary>
/// <param name="subtype">When this method returns, contains the BSON binary subtype.</param>
public ReadOnlySpan<byte> ReadBinary(out byte subtype)
{
var length = ReadInt32();
if (Remaining < 1)
throw new InvalidOperationException("Not enough bytes to read binary subtype");
subtype = _buffer[_position];
_position++;
if (Remaining < length)
throw new InvalidOperationException("Not enough bytes to read binary data");
var data = _buffer.Slice(_position, length);
_position += length;
return data;
}
/// <summary>
/// Skips the current value based on type
/// </summary>
/// <param name="type">The BSON type of the value to skip.</param>
public void SkipValue(BsonType type)
{
switch (type)
{
case BsonType.Double:
_position += 8;
break;
case BsonType.String:
var stringLength = ReadInt32();
_position += stringLength;
break;
case BsonType.Document:
case BsonType.Array:
var docLength = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4));
_position += docLength;
break;
case BsonType.Binary:
var binaryLength = ReadInt32();
_position += 1 + binaryLength; // subtype + data
break;
case BsonType.ObjectId:
_position += 12;
break;
case BsonType.Boolean:
_position += 1;
break;
case BsonType.DateTime:
case BsonType.Int64:
case BsonType.Timestamp:
_position += 8;
break;
case BsonType.Decimal128:
_position += 16;
break;
case BsonType.Int32:
_position += 4;
break;
case BsonType.Null:
// No data
break;
default:
throw new NotSupportedException($"Skipping type {type} not supported");
}
}
/// <summary>
/// Reads a single byte.
/// </summary>
public byte ReadByte()
{
if (Remaining < 1)
throw new InvalidOperationException("Not enough bytes to read byte");
var value = _buffer[_position];
_position++;
return value;
}
/// <summary>
/// Peeks a 32-bit integer at the current position without advancing.
/// </summary>
public int PeekInt32()
{
if (Remaining < 4)
throw new InvalidOperationException("Not enough bytes to peek Int32");
return BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4));
}
/// <summary>
/// Reads an element header key identifier and resolves it to a key name.
/// </summary>
public string ReadElementHeader()
{
if (Remaining < 2)
throw new InvalidOperationException("Not enough bytes to read BSON element key ID");
var id = BinaryPrimitives.ReadUInt16LittleEndian(_buffer.Slice(_position, 2));
_position += 2;
if (!_keys.TryGetValue(id, out var key))
{
throw new InvalidOperationException($"BSON Key ID {id} not found in reverse key dictionary.");
}
return key;
}
/// <summary>
/// Returns a span containing all unread bytes.
/// </summary>
public ReadOnlySpan<byte> RemainingBytes() => _buffer[_position..];
}

382
src/CBDD.Bson/BsonSpanWriter.cs Executable file
View File

@@ -0,0 +1,382 @@
using System;
using System.Buffers.Binary;
using System.Text;
namespace ZB.MOM.WW.CBDD.Bson;
/// <summary>
/// Zero-allocation BSON writer using Span&lt;byte&gt;.
/// Implemented as ref struct to ensure stack-only allocation.
/// </summary>
public ref struct BsonSpanWriter
{
private Span<byte> _buffer;
private int _position;
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, ushort> _keyMap;
/// <summary>
/// Initializes a new instance of the <see cref="BsonSpanWriter"/> struct.
/// </summary>
/// <param name="buffer">The destination buffer to write BSON bytes into.</param>
/// <param name="keyMap">The cached key-name to key-id mapping.</param>
public BsonSpanWriter(Span<byte> buffer, System.Collections.Concurrent.ConcurrentDictionary<string, ushort> keyMap)
{
_buffer = buffer;
_keyMap = keyMap;
_position = 0;
}
/// <summary>
/// Gets the current write position in the buffer.
/// </summary>
public int Position => _position;
/// <summary>
/// Gets the number of bytes remaining in the buffer.
/// </summary>
public int Remaining => _buffer.Length - _position;
/// <summary>
/// Writes document size placeholder and returns the position to patch later
/// </summary>
public int WriteDocumentSizePlaceholder()
{
var sizePosition = _position;
BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), 0);
_position += 4;
return sizePosition;
}
/// <summary>
/// Patches the document size at the given position
/// </summary>
/// <param name="sizePosition">The position where the size placeholder was written.</param>
public void PatchDocumentSize(int sizePosition)
{
var size = _position - sizePosition;
BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(sizePosition, 4), size);
}
/// <summary>
/// Writes a BSON element header (type + name)
/// </summary>
/// <param name="type">The BSON element type.</param>
/// <param name="name">The field name.</param>
public void WriteElementHeader(BsonType type, string name)
{
_buffer[_position] = (byte)type;
_position++;
if (!_keyMap.TryGetValue(name, out var id))
{
throw new InvalidOperationException($"BSON Key '{name}' not found in dictionary cache. Ensure all keys are registered before serialization.");
}
BinaryPrimitives.WriteUInt16LittleEndian(_buffer.Slice(_position, 2), id);
_position += 2;
}
/// <summary>
/// Writes a C-style null-terminated string
/// </summary>
private void WriteCString(string value)
{
var bytesWritten = Encoding.UTF8.GetBytes(value, _buffer[_position..]);
_position += bytesWritten;
_buffer[_position] = 0; // Null terminator
_position++;
}
/// <summary>
/// Writes end-of-document marker
/// </summary>
public void WriteEndOfDocument()
{
_buffer[_position] = 0;
_position++;
}
/// <summary>
/// Writes a BSON string element
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The string value.</param>
public void WriteString(string name, string value)
{
WriteElementHeader(BsonType.String, name);
var valueBytes = Encoding.UTF8.GetByteCount(value);
var stringLength = valueBytes + 1; // Include null terminator
BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), stringLength);
_position += 4;
Encoding.UTF8.GetBytes(value, _buffer[_position..]);
_position += valueBytes;
_buffer[_position] = 0; // Null terminator
_position++;
}
/// <summary>
/// Writes a BSON int32 element.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The 32-bit integer value.</param>
public void WriteInt32(string name, int value)
{
WriteElementHeader(BsonType.Int32, name);
BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), value);
_position += 4;
}
/// <summary>
/// Writes a BSON int64 element.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The 64-bit integer value.</param>
public void WriteInt64(string name, long value)
{
WriteElementHeader(BsonType.Int64, name);
BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(_position, 8), value);
_position += 8;
}
/// <summary>
/// Writes a BSON double element.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The double-precision value.</param>
public void WriteDouble(string name, double value)
{
WriteElementHeader(BsonType.Double, name);
BinaryPrimitives.WriteDoubleLittleEndian(_buffer.Slice(_position, 8), value);
_position += 8;
}
/// <summary>
/// Writes spatial coordinates as a BSON array [X, Y].
/// Optimized for (double, double) tuples.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="coordinates">The coordinate tuple as (X, Y).</param>
public void WriteCoordinates(string name, (double, double) coordinates)
{
WriteElementHeader(BsonType.Array, name);
var startPos = _position;
_position += 4; // Placeholder for array size
// Element 0: X
_buffer[_position++] = (byte)BsonType.Double;
_buffer[_position++] = 0x30; // '0'
_buffer[_position++] = 0x00; // Null
BinaryPrimitives.WriteDoubleLittleEndian(_buffer.Slice(_position, 8), coordinates.Item1);
_position += 8;
// Element 1: Y
_buffer[_position++] = (byte)BsonType.Double;
_buffer[_position++] = 0x31; // '1'
_buffer[_position++] = 0x00; // Null
BinaryPrimitives.WriteDoubleLittleEndian(_buffer.Slice(_position, 8), coordinates.Item2);
_position += 8;
_buffer[_position++] = 0x00; // End of array marker
// Patch array size
var size = _position - startPos;
BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(startPos, 4), size);
}
/// <summary>
/// Writes a BSON Decimal128 element from a <see cref="decimal"/> value.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The decimal value.</param>
public void WriteDecimal128(string name, decimal value)
{
WriteElementHeader(BsonType.Decimal128, name);
// Note: usage of C# decimal bits for round-trip fidelity within ZB.MOM.WW.CBDD.
// This makes it compatible with CBDD Reader but strictly speaking not standard IEEE 754-2008 Decimal128.
var bits = decimal.GetBits(value);
BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), bits[0]);
BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position + 4, 4), bits[1]);
BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position + 8, 4), bits[2]);
BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position + 12, 4), bits[3]);
_position += 16;
}
/// <summary>
/// Writes a BSON boolean element.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The boolean value.</param>
public void WriteBoolean(string name, bool value)
{
WriteElementHeader(BsonType.Boolean, name);
_buffer[_position] = (byte)(value ? 1 : 0);
_position++;
}
/// <summary>
/// Writes a BSON UTC datetime element.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The date and time value.</param>
public void WriteDateTime(string name, DateTime value)
{
WriteElementHeader(BsonType.DateTime, name);
var milliseconds = new DateTimeOffset(value.ToUniversalTime()).ToUnixTimeMilliseconds();
BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(_position, 8), milliseconds);
_position += 8;
}
/// <summary>
/// Writes a BSON UTC datetime element from a <see cref="DateTimeOffset"/> value.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The date and time offset value.</param>
public void WriteDateTimeOffset(string name, DateTimeOffset value)
{
WriteElementHeader(BsonType.DateTime, name);
var milliseconds = value.ToUnixTimeMilliseconds();
BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(_position, 8), milliseconds);
_position += 8;
}
/// <summary>
/// Writes a BSON int64 element containing ticks from a <see cref="TimeSpan"/>.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The time span value.</param>
public void WriteTimeSpan(string name, TimeSpan value)
{
WriteElementHeader(BsonType.Int64, name);
BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(_position, 8), value.Ticks);
_position += 8;
}
/// <summary>
/// Writes a BSON int32 element containing the <see cref="DateOnly.DayNumber"/>.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The date-only value.</param>
public void WriteDateOnly(string name, DateOnly value)
{
WriteElementHeader(BsonType.Int32, name);
BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), value.DayNumber);
_position += 4;
}
/// <summary>
/// Writes a BSON int64 element containing ticks from a <see cref="TimeOnly"/>.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The time-only value.</param>
public void WriteTimeOnly(string name, TimeOnly value)
{
WriteElementHeader(BsonType.Int64, name);
BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(_position, 8), value.Ticks);
_position += 8;
}
/// <summary>
/// Writes a GUID as a BSON string element.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The GUID value.</param>
public void WriteGuid(string name, Guid value)
{
WriteString(name, value.ToString());
}
/// <summary>
/// Writes a BSON ObjectId element.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="value">The ObjectId value.</param>
public void WriteObjectId(string name, ObjectId value)
{
WriteElementHeader(BsonType.ObjectId, name);
value.WriteTo(_buffer.Slice(_position, 12));
_position += 12;
}
/// <summary>
/// Writes a BSON null element.
/// </summary>
/// <param name="name">The field name.</param>
public void WriteNull(string name)
{
WriteElementHeader(BsonType.Null, name);
// No value to write for null
}
/// <summary>
/// Writes binary data
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="data">The binary payload.</param>
/// <param name="subtype">The BSON binary subtype.</param>
public void WriteBinary(string name, ReadOnlySpan<byte> data, byte subtype = 0)
{
WriteElementHeader(BsonType.Binary, name);
BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), data.Length);
_position += 4;
_buffer[_position] = subtype;
_position++;
data.CopyTo(_buffer[_position..]);
_position += data.Length;
}
/// <summary>
/// Begins writing a subdocument and returns the size position to patch later
/// </summary>
/// <param name="name">The field name for the subdocument.</param>
public int BeginDocument(string name)
{
WriteElementHeader(BsonType.Document, name);
return WriteDocumentSizePlaceholder();
}
/// <summary>
/// Begins writing the root document and returns the size position to patch later
/// </summary>
public int BeginDocument()
{
return WriteDocumentSizePlaceholder();
}
/// <summary>
/// Ends the current document
/// </summary>
/// <param name="sizePosition">The position returned by <see cref="WriteDocumentSizePlaceholder"/>.</param>
public void EndDocument(int sizePosition)
{
WriteEndOfDocument();
PatchDocumentSize(sizePosition);
}
/// <summary>
/// Begins writing a BSON array and returns the size position to patch later
/// </summary>
/// <param name="name">The field name for the array.</param>
public int BeginArray(string name)
{
WriteElementHeader(BsonType.Array, name);
return WriteDocumentSizePlaceholder();
}
/// <summary>
/// Ends the current BSON array
/// </summary>
/// <param name="sizePosition">The position returned by <see cref="WriteDocumentSizePlaceholder"/>.</param>
public void EndArray(int sizePosition)
{
WriteEndOfDocument();
PatchDocumentSize(sizePosition);
}
}

30
src/CBDD.Bson/BsonType.cs Executable file
View File

@@ -0,0 +1,30 @@
namespace ZB.MOM.WW.CBDD.Bson;
/// <summary>
/// BSON type codes as defined in BSON spec
/// </summary>
public enum BsonType : byte
{
EndOfDocument = 0x00,
Double = 0x01,
String = 0x02,
Document = 0x03,
Array = 0x04,
Binary = 0x05,
Undefined = 0x06, // Deprecated
ObjectId = 0x07,
Boolean = 0x08,
DateTime = 0x09,
Null = 0x0A,
Regex = 0x0B,
DBPointer = 0x0C, // Deprecated
JavaScript = 0x0D,
Symbol = 0x0E, // Deprecated
JavaScriptWithScope = 0x0F,
Int32 = 0x10,
Timestamp = 0x11,
Int64 = 0x12,
Decimal128 = 0x13,
MinKey = 0xFF,
MaxKey = 0x7F
}

112
src/CBDD.Bson/ObjectId.cs Executable file
View File

@@ -0,0 +1,112 @@
using System;
using System.Runtime.InteropServices;
namespace ZB.MOM.WW.CBDD.Bson;
/// <summary>
/// 12-byte ObjectId compatible with MongoDB ObjectId.
/// Implemented as readonly struct for zero allocation.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 12)]
public readonly struct ObjectId : IEquatable<ObjectId>
{
[FieldOffset(0)] private readonly int _timestamp;
[FieldOffset(4)] private readonly long _randomAndCounter;
/// <summary>
/// Empty ObjectId (all zeros)
/// </summary>
public static readonly ObjectId Empty = new ObjectId(0, 0);
/// <summary>
/// Maximum ObjectId (all 0xFF bytes) - useful for range queries
/// </summary>
public static readonly ObjectId MaxValue = new ObjectId(int.MaxValue, long.MaxValue);
/// <summary>
/// Initializes a new instance of the <see cref="ObjectId"/> struct from raw bytes.
/// </summary>
/// <param name="bytes">The 12-byte ObjectId value.</param>
public ObjectId(ReadOnlySpan<byte> bytes)
{
if (bytes.Length != 12)
throw new ArgumentException("ObjectId must be exactly 12 bytes", nameof(bytes));
_timestamp = BitConverter.ToInt32(bytes[..4]);
_randomAndCounter = BitConverter.ToInt64(bytes[4..12]);
}
/// <summary>
/// Initializes a new instance of the <see cref="ObjectId"/> struct from its components.
/// </summary>
/// <param name="timestamp">The Unix timestamp portion.</param>
/// <param name="randomAndCounter">The random and counter portion.</param>
public ObjectId(int timestamp, long randomAndCounter)
{
_timestamp = timestamp;
_randomAndCounter = randomAndCounter;
}
/// <summary>
/// Creates a new ObjectId with current timestamp
/// </summary>
public static ObjectId NewObjectId()
{
var timestamp = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var random = Random.Shared.NextInt64();
return new ObjectId(timestamp, random);
}
/// <summary>
/// Writes the ObjectId to the destination span (must be 12 bytes)
/// </summary>
/// <param name="destination">The destination span to write into.</param>
public void WriteTo(Span<byte> destination)
{
if (destination.Length < 12)
throw new ArgumentException("Destination must be at least 12 bytes", nameof(destination));
BitConverter.TryWriteBytes(destination[..4], _timestamp);
BitConverter.TryWriteBytes(destination[4..12], _randomAndCounter);
}
/// <summary>
/// Converts ObjectId to byte array
/// </summary>
public byte[] ToByteArray()
{
var bytes = new byte[12];
WriteTo(bytes);
return bytes;
}
/// <summary>
/// Gets timestamp portion as UTC DateTime
/// </summary>
public DateTime Timestamp => DateTimeOffset.FromUnixTimeSeconds(_timestamp).UtcDateTime;
/// <summary>
/// Determines whether this instance and another <see cref="ObjectId"/> have the same value.
/// </summary>
/// <param name="other">The object to compare with this instance.</param>
/// <returns><see langword="true"/> if the values are equal; otherwise, <see langword="false"/>.</returns>
public bool Equals(ObjectId other) =>
_timestamp == other._timestamp && _randomAndCounter == other._randomAndCounter;
/// <inheritdoc />
public override bool Equals(object? obj) => obj is ObjectId other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(_timestamp, _randomAndCounter);
public static bool operator ==(ObjectId left, ObjectId right) => left.Equals(right);
public static bool operator !=(ObjectId left, ObjectId right) => !left.Equals(right);
/// <inheritdoc />
public override string ToString()
{
Span<byte> bytes = stackalloc byte[12];
WriteTo(bytes);
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}

128
src/CBDD.Bson/Schema/BsonField.cs Executable file
View File

@@ -0,0 +1,128 @@
namespace ZB.MOM.WW.CBDD.Bson.Schema;
public partial class BsonField
{
/// <summary>
/// Gets the field name.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Gets the field BSON type.
/// </summary>
public BsonType Type { get; init; }
/// <summary>
/// Gets a value indicating whether the field is nullable.
/// </summary>
public bool IsNullable { get; init; }
/// <summary>
/// Gets the nested schema when this field is a document.
/// </summary>
public BsonSchema? NestedSchema { get; init; }
/// <summary>
/// Gets the array item type when this field is an array.
/// </summary>
public BsonType? ArrayItemType { get; init; }
/// <summary>
/// Writes this field definition to BSON.
/// </summary>
/// <param name="writer">The BSON writer.</param>
public void ToBson(ref BsonSpanWriter writer)
{
var size = writer.BeginDocument();
writer.WriteString("n", Name);
writer.WriteInt32("t", (int)Type);
writer.WriteBoolean("b", IsNullable);
if (NestedSchema != null)
{
writer.WriteElementHeader(BsonType.Document, "s");
NestedSchema.ToBson(ref writer);
}
if (ArrayItemType != null)
{
writer.WriteInt32("a", (int)ArrayItemType.Value);
}
writer.EndDocument(size);
}
/// <summary>
/// Reads a field definition from BSON.
/// </summary>
/// <param name="reader">The BSON reader.</param>
/// <returns>The deserialized field.</returns>
public static BsonField FromBson(ref BsonSpanReader reader)
{
reader.ReadInt32(); // Read doc size
string name = "";
BsonType type = BsonType.Null;
bool isNullable = false;
BsonSchema? nestedSchema = null;
BsonType? arrayItemType = null;
while (reader.Remaining > 1)
{
var btype = reader.ReadBsonType();
if (btype == BsonType.EndOfDocument) break;
var key = reader.ReadElementHeader();
switch (key)
{
case "n": name = reader.ReadString(); break;
case "t": type = (BsonType)reader.ReadInt32(); break;
case "b": isNullable = reader.ReadBoolean(); break;
case "s": nestedSchema = BsonSchema.FromBson(ref reader); break;
case "a": arrayItemType = (BsonType)reader.ReadInt32(); break;
default: reader.SkipValue(btype); break;
}
}
return new BsonField
{
Name = name,
Type = type,
IsNullable = isNullable,
NestedSchema = nestedSchema,
ArrayItemType = arrayItemType
};
}
/// <summary>
/// Computes a hash representing the field definition.
/// </summary>
/// <returns>The computed hash value.</returns>
public long GetHash()
{
var hash = new HashCode();
hash.Add(Name);
hash.Add((int)Type);
hash.Add(IsNullable);
hash.Add(ArrayItemType);
if (NestedSchema != null) hash.Add(NestedSchema.GetHash());
return hash.ToHashCode();
}
/// <summary>
/// Determines whether this field is equal to another field.
/// </summary>
/// <param name="other">The other field.</param>
/// <returns><see langword="true"/> if the fields are equal; otherwise, <see langword="false"/>.</returns>
public bool Equals(BsonField? other)
{
if (other == null) return false;
return GetHash() == other.GetHash();
}
/// <inheritdoc />
public override bool Equals(object? obj) => Equals(obj as BsonField);
/// <inheritdoc />
public override int GetHashCode() => (int)GetHash();
}

View File

@@ -0,0 +1,129 @@
namespace ZB.MOM.WW.CBDD.Bson.Schema;
public partial class BsonSchema
{
/// <summary>
/// Gets or sets the schema title.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Gets or sets the schema version.
/// </summary>
public int? Version { get; set; }
/// <summary>
/// Gets the schema fields.
/// </summary>
public List<BsonField> Fields { get; } = new();
/// <summary>
/// Serializes this schema instance to BSON.
/// </summary>
/// <param name="writer">The BSON writer to write into.</param>
public void ToBson(ref BsonSpanWriter writer)
{
var size = writer.BeginDocument();
if (Title != null) writer.WriteString("t", Title);
if (Version != null) writer.WriteInt32("_v", Version.Value);
var fieldsSize = writer.BeginArray("f");
for (int i = 0; i < Fields.Count; i++)
{
writer.WriteElementHeader(BsonType.Document, i.ToString());
Fields[i].ToBson(ref writer);
}
writer.EndArray(fieldsSize);
writer.EndDocument(size);
}
/// <summary>
/// Deserializes a schema instance from BSON.
/// </summary>
/// <param name="reader">The BSON reader to read from.</param>
/// <returns>The deserialized schema.</returns>
public static BsonSchema FromBson(ref BsonSpanReader reader)
{
reader.ReadInt32(); // Read doc size
var schema = new BsonSchema();
while (reader.Remaining > 1)
{
var btype = reader.ReadBsonType();
if (btype == BsonType.EndOfDocument) break;
var key = reader.ReadElementHeader();
switch (key)
{
case "t": schema.Title = reader.ReadString(); break;
case "_v": schema.Version = reader.ReadInt32(); break;
case "f":
reader.ReadInt32(); // array size
while (reader.Remaining > 1)
{
var itemType = reader.ReadBsonType();
if (itemType == BsonType.EndOfDocument) break;
reader.ReadElementHeader(); // index
schema.Fields.Add(BsonField.FromBson(ref reader));
}
break;
default: reader.SkipValue(btype); break;
}
}
return schema;
}
/// <summary>
/// Computes a hash value for this schema based on its contents.
/// </summary>
/// <returns>The computed hash value.</returns>
public long GetHash()
{
var hash = new HashCode();
hash.Add(Title);
foreach (var field in Fields)
{
hash.Add(field.GetHash());
}
return hash.ToHashCode();
}
/// <summary>
/// Determines whether this schema is equal to another schema.
/// </summary>
/// <param name="other">The schema to compare with.</param>
/// <returns><see langword="true"/> when schemas are equal; otherwise, <see langword="false"/>.</returns>
public bool Equals(BsonSchema? other)
{
if (other == null) return false;
return GetHash() == other.GetHash();
}
/// <inheritdoc />
public override bool Equals(object? obj) => Equals(obj as BsonSchema);
/// <inheritdoc />
public override int GetHashCode() => (int)GetHash();
/// <summary>
/// Enumerates all field keys in this schema, including nested schema keys.
/// </summary>
/// <returns>An enumerable of field keys.</returns>
public IEnumerable<string> GetAllKeys()
{
foreach (var field in Fields)
{
yield return field.Name;
if (field.NestedSchema != null)
{
foreach (var nestedKey in field.NestedSchema.GetAllKeys())
{
yield return nestedKey;
}
}
}
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>ZB.MOM.WW.CBDD.Bson</AssemblyName>
<RootNamespace>ZB.MOM.WW.CBDD.Bson</RootNamespace>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PackageId>ZB.MOM.WW.CBDD.Bson</PackageId>
<Version>1.3.1</Version>
<Authors>CBDD Team</Authors>
<Description>BSON Serialization Library for High-Performance Database Engine</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/EntglDb/CBDD</RepositoryUrl>
<PackageTags>database;embedded;bson;nosql;net10;zero-allocation</PackageTags>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>