Initialize CBDD solution and add a .NET-focused gitignore for generated artifacts.
This commit is contained in:
14
src/CBDD.Bson/Attributes.cs
Executable file
14
src/CBDD.Bson/Attributes.cs
Executable 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
262
src/CBDD.Bson/BsonBufferWriter.cs
Executable 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
292
src/CBDD.Bson/BsonDocument.cs
Executable 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<byte> 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
396
src/CBDD.Bson/BsonSpanReader.cs
Executable 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<byte>.
|
||||
/// 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
382
src/CBDD.Bson/BsonSpanWriter.cs
Executable 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<byte>.
|
||||
/// 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
30
src/CBDD.Bson/BsonType.cs
Executable 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
112
src/CBDD.Bson/ObjectId.cs
Executable 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
128
src/CBDD.Bson/Schema/BsonField.cs
Executable 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();
|
||||
}
|
||||
129
src/CBDD.Bson/Schema/BsonSchema.cs
Executable file
129
src/CBDD.Bson/Schema/BsonSchema.cs
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/CBDD.Bson/ZB.MOM.WW.CBDD.Bson.csproj
Executable file
28
src/CBDD.Bson/ZB.MOM.WW.CBDD.Bson.csproj
Executable 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>
|
||||
Reference in New Issue
Block a user