If your application code needs to know whether it runs on MongoDB or Cassandra, that is a design flaw. Not necessarily a disaster, but a flaw, because the business logic then depends on an infrastructure decision. PolyPersist separates the two in .NET by putting uniform interfaces over document, column, and blob stores.
What exactly is the problem?
ORMs solve part of this, but only for SQL databases. If a project uses MongoDB for documents, Cassandra for time-series data, and S3 for files, you have to write a separate access layer for each. Those layers are incompatible, you can't swap one for another, and every project writes them again.
Cloud-agnostic development also asks that no Azure or AWS dependency be baked into the business logic. I agree, but the only way to do that is with uniform interfaces, and PolyPersist provides them.
How does it work?
The library abstracts three store types:
- DocumentStore: MongoDB, in-memory (for development and testing)
- ColumnStore: Cassandra, Amazon Redshift, Google BigQuery
- BlobStore: Azure Blob, Amazon S3, Google Cloud Storage, MinIO, GridFS, FileSystem
Consuming code sees only the interfaces (IDocumentStore, IBlobStore, IColumnStore).
Whether MongoDB or an in-memory implementation sits behind them is set in the dependency injection configuration, not in the code.
You can run in-memory in tests and MongoDB in production, and nobody has to touch the business logic to switch.
Multiplatform interfaces
PolyPersist is not limited to C#. All store interface definitions
are generated by UniContract, so the IDocumentStore,
IBlobStore, and IColumnStore contracts are identical
in C#, Java, and Python. When an interface changes, it changes in all three languages at once.
A service written in Java therefore works against the same contract as a C# microservice, and nobody has to keep the two definitions in sync by hand. The toolchain keeps them identical, so it doesn't depend on developer discipline. The same generation step keeps the C#, Java, and Python implementations in line with each other.
Design decisions that matter
ETag-based optimistic locking works the same way for every entity and every database. That takes work, because Cassandra and MongoDB handle concurrency very differently. Making them uniform is a compromise, but from the consuming code's point of view I think it is the right call.
Every entity has a PartitionKey. It is there for compatibility with Cosmos DB and DynamoDB,
and when a database doesn't need it, it stays empty. Better to have it and not need it than to need it and not have it.
The design is async-first: every operation is Task-based. Database access is always
I/O bound, so a synchronous API only causes trouble in the long run.
What is it good for in practice?
I use it in three situations: when I want to avoid cloud provider lock-in in the data layer; when different projects use different databases but I want developers to see the same API everywhere; or when I want an in-memory implementation in tests and a real database in production.
Databases do differ, and the differences matter. They belong in the implementation layer, though, and the business logic doesn't need to know about them. PolyPersist is built on that rule.