Skip to content
Auto
Products

Binary data storage

BinaryDataStorageManager routes binary content by a prefix in its path. An application that keeps a record of a file somewhere - a database row, a link, an entity - and the bytes somewhere else, stores a short path such as files://a3f2c1.png and lets the manager find the storage that holds it.

This is a standalone helper. It is not part of the FileSystem pipeline, and nothing in the library calls it.

A storage implements IBinaryDataStorage, three methods over whatever holds the bytes:

internal class DiskStorage(string root) : IBinaryDataStorage
{
public Stream Read(string contentPath) => File.OpenRead(Path.Combine(root, contentPath));
public string Write(string? extension, Stream stream)
{
var name = Guid.NewGuid().ToString("N") + extension;
using (var target = File.Create(Path.Combine(root, name)))
stream.CopyTo(target);
return name;
}
public bool Delete(string contentPath)
{
var full = Path.Combine(root, contentPath);
if (!File.Exists(full))
return false;
File.Delete(full);
return true;
}
}

Each storage is registered under a prefix:

var manager = new BinaryDataStorageManager();
manager.RegisterStorage("files://", new DiskStorage(@"C:\data\files"));

A prefix ends with :// and the part before it holds only letters, digits, - and _. It is stored in lower case and matched without regard to case, so FILES://a3f2c1.png reaches the same storage. BinaryDataStorageInfo.Code exposes the prefix without the separator.

Write takes the resource, so the storage can use its extension, and returns the path to keep:

var contentPath = manager.Write(resource, stream); // "files://8f14e45f.png"

Read and Delete accept either that path or the resource that carries it:

using var content = manager.Read(contentPath);
var removed = manager.Delete(contentPath); // true when the storage held it

The prefix decides which storage answers. A path with no registered prefix throws BinaryDataStorageNotFoundException, and a resource whose ContentPath is empty throws InvalidOperationException.

Registration order matters. Write sends new content to the storage the resource already uses, and falls back to the first registered one:

var manager = new BinaryDataStorageManager();
manager.RegisterStorage("s3://", new S3Storage()); // new content goes here
manager.RegisterStorage("files://", new DiskStorage(...)); // old content still reads

This is what makes a migration possible without rewriting the stored paths. Content written earlier as files://... keeps reading from disk, and everything new lands in the storage registered first. A resource already stored under s3:// is rewritten in place rather than moved.

GetCurrentStorage reports where a resource is held now, GetPreferredStorage where it would be written. GetPreferredStorage is virtual, so a different policy - by extension, by size, by tenant - is a subclass away.