-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for reading/writing objects with ReadOnlyMemory<byte>
properties
#228
Comments
Hi @bill-poole, thanks for raising this issue. You're right, with the current API you have to copy data into a new byte array to be able to serialize it. Your proposal looks interesting and should generally work, but I have one concern about it. There is a public property public byte[] Value => _value.ToArray(); I would like to avoid doing changes like this because it can introduce a silent performance degradation for other users. Also, we can't simply delete this property or change its type to Also one question about your use case that could be outside of the scope of the library but still. If your buffer is larger than the final compressed data, how would you know what part of the |
Thanks for considering this feature request @firenero. The compressor writes data to a byte array ( Alternatively, the compressor can write to an One idea I had for how this could be approached:
That would then allow support for more types in the future so this problem doesn't come up again. Alternatively, you could create a specific |
I was able to create a workaround by writing my own converter that read/writes the underlying public class ReadOnlyMemoryBinaryDdbConverter : DdbConverter<ReadOnlyMemory<byte>>
{
public override ReadOnlyMemory<byte> Read(ref DdbReader reader)
=> reader.JsonReader.GetBytesFromBase64();
public override ReadOnlyMemory<byte> Read(in AttributeValue attributeValue)
=> throw new NotImplementedException();
public override void Write(in DdbWriter writer, ref ReadOnlyMemory<byte> value)
{
writer.JsonWriter.WriteStartObject();
writer.JsonWriter.WriteBase64String(DdbTypeNames.Binary, value.Span);
writer.JsonWriter.WriteEndObject();
}
public override AttributeValue Write(ref ReadOnlyMemory<byte> value)
=> throw new NotImplementedException();
} I assume that it is valid/correct to throw an exception in the Note that this workaround would be made easier if a |
Yes, your converter should work, good idea! I think including this converter by default in the library is the easiest way to support the |
Okay great! Given that the I was a bit confused by that example in the documentation having effectively two conversion implementations - one low-level conversion directly to/from JSON, and one higher-level conversion to/from Furthermore, it seems the internal converters in the EfficientDynamoDb source code also do the same (e.g., |
Good point. I've realized that "basic" Read/Write method implementations are used in Document extensions Unfortunately, we can't ship this converter as it is in the library because it can lead to hidden errors in code. In theory, we can ship it with |
Okay thank you! I really appreciate it. |
I found a way to write a converter that supports Document extensions |
Thanks @firenero. I took a look at the source code and I think there might be a problem with the code below from BinaryToMemoryDdbConverter.cs. The code uses If the public override AttributeValue Write(ref Memory<byte> value)
{
var array = MemoryMarshal.TryGetArray((ReadOnlyMemory<byte>)value, out var segment)
? segment.Array
: value.ToArray();
Debug.Assert(array != null);
return new AttributeValue(new BinaryAttributeValue(array));
} |
You're right about this, I need to narrow down the happy path only to cases when
Agree, it's not ideal but on practice the path with allocation will be very rare. In most cases,
In all other cases, it won't allocate because either this converter method is not called or the happy path without allocation is possible. Example of the code that will allocate: public class TestObj
{
[DynamoDbProperty("mem")]
public ReadOnlyMemory<byte> Memory { get; set; }
}
var arr = new byte[] { 0x69, 0xa7, 0x5d, 0xff};
var item = new TestObj
{
Memory = arr.AsMemory(1, 2) // Take only 0xa7 and 0x5d bytes
};
// This conversion allocates new array for 0xa7 and 0x5d bytes
var doc = context.ToDocument(item); Similarly, it will allocate if your memory represents an unmanaged data but that's also happens quite rarely. In all other cases it will NOT allocate so I think it's a decent tradeoff overall. Let me know what you think about it. |
I suspect that if someone is using That is actually my scenario. i.e., I'm using I assume the concern/issue with updating [StructLayout(LayoutKind.Explicit)]
public readonly struct BinaryAttributeValue
{
[FieldOffset(0)]
private readonly ReadOnlyMemory<T> _value;
public byte[] Value => MemoryMarshal.TryGetArray(_value, out var segment)
&& segment.Count == segment.Array.Length
? segment.Array
: _value.ToArray();
public Memory<byte> AsMemory()
=> MemoryMarshal.AsMemory(_value);
public BinaryAttributeValue(byte[] value)
{
_value = value;
}
public BinaryAttributeValue(ReadOnlyMemory<byte> value)
{
_value = value;
}
public void Write(Utf8JsonWriter writer)
{
writer.WriteStartObject();
writer.WriteBase64String(DdbTypeNames.Binary, _value.Span);
writer.WriteEndObject();
}
public override string ToString() => Convert.ToBase64String(_value.Span);
} As far as I can tell, this allows Furthermore, it would only allocate if someone creates a You would then have a public class MemoryBinaryDdbConverter : DdbConverter<Memory<byte>>
{
public override Memory<byte> Read(ref DdbReader reader)
=> reader.JsonReader.GetBytesFromBase64();
public override Memory<byte> Read(in AttributeValue attributeValue)
=> attributeValue.AsBinaryAttribute().AsMemory();
public override void Write(in DdbWriter writer, ref Memory<byte> value)
{
writer.JsonWriter.WriteStartObject();
writer.JsonWriter.WriteBase64String(DdbTypeNames.Binary, value.Span);
writer.JsonWriter.WriteEndObject();
}
public override AttributeValue Write(ref Memory<byte> value)
=> new AttributeValue(new BinaryAttributeValue(value));
} And you would have a public class ReadOnlyMemoryBinaryDdbConverter : DdbConverter<ReadOnlyMemory<byte>>
{
public override ReadOnlyMemory<byte> Read(ref DdbReader reader)
=> reader.JsonReader.GetBytesFromBase64();
public override ReadOnlyMemory<byte> Read(in AttributeValue attributeValue)
=> attributeValue.AsBinaryAttribute().AsMemory();
public override void Write(in DdbWriter writer, ref ReadOnlyMemory<byte> value)
{
writer.JsonWriter.WriteStartObject();
writer.JsonWriter.WriteBase64String(DdbTypeNames.Binary, value.Span);
writer.JsonWriter.WriteEndObject();
}
public override AttributeValue Write(ref ReadOnlyMemory<byte> value)
=> new AttributeValue(new BinaryAttributeValue(value));
} |
I think there is a misunderstanding. My version of converter will only allocate if you create an entity in code and then convert it to public class TestObj
{
[DynamoDbProperty("mem")]
public ReadOnlyMemory<byte> Memory { get; set; }
}
var arr = new byte[] { 0x69, 0xa7, 0x5d, 0xff};
var item = new TestObj
{
Memory = arr.AsMemory(1, 2) // Take only 0xa7 and 0x5d bytes
};
// This conversion allocates new array for 0xa7 and 0x5d bytes
var doc = context.ToDocument(item); I can't imagine a scenario when someone need to create an entity then convert it to var item = await context.GetItemAsync("pk", "sk");
var doc = context.ToDocument(item); // this won't allocate Correct me if I'm wrong, but your use case is:
There is no allocation in the case of writing to DB because the optimized version Your idea with using
I have a feeling that it's a similar assumption to my previous one "No one creates an entity and converts it to a document right away" but I could be mistaken. No one is supposed to do it this way but we never know all the people use cases. Anyways, I will check again your proposed API. Meanwhile, please let me know if we're on the same page about allocations in my current converter approach and if I understand your use case correctly. |
Yes I agree; I couldn't think of a scenario either. However, the
Yes I think we are on the same page and I think you understand my use case correctly. My scenario currently doesn't invoke the However, tomorrow's scenarios are hard to predict, so I was trying to come up with a solution that would always allow allocations to be avoided under all circumstances. |
@bill-poole I've checked the BinaryAttributeValue implementation you suggested in #228 (comment). I don't think we can make this constructor (at least publicly available): public BinaryAttributeValue(ReadOnlyMemory<byte> value)
{
_value = value;
} The data itself is converted to mutable |
This is now available in v0.9.16. Thanks for helping me with with it. Feel free to re-open this ticket or open a new one if you need anything. |
Okay, thanks @firenero. |
I'm new to using EfficientDynamoDb, so apologies if I have this wrong, but I've been through the documentation and code and don't think its currently possible to read/write objects with binary properties of type
ReadOnlyMemory<byte>
. I need to write buffers to DynamoDB that have been produced by compression, and the data is written into a buffer that is larger than the final compressed data (which is typical for writing compressed data).Currently, it seems I need to copy this data into a new byte array so I can set a
byte[]
property on the object I want to write to DynamoDB.I propose modifying the
BinaryAttributeValue
struct to hold aReadOnlyMemory<byte>
field instead of abyte[]
field and then add a new constructor accepting aReadOnlyMemory<byte>
parameter. The existing constructor would of course be retained (byte[]
has an implicit cast toReadOnlyMemory<byte>
).The
BinaryAttributeValue.Write
method would then be updated to:And the
BinaryAttributeValue.ToString
method would be updated to:This would allow me to write my own custom converter for a
ReadOnlyMemory<byte>
property. Although, it would be even better if the library shipped one out of the box.The text was updated successfully, but these errors were encountered: