NosDB is a NoSQL Document Database written 100% in .NET. A document database stores data in the form of a key and a value. In this case the key is a string and the value is a JSON document. Storing data in JSON format allows NosDB to be completely schema-less i.e. no need to define rows and columns at creation time. To develop a robust application with a scalable database tier it is essential that you understand the .NET API that NosDB provides.
The NosDB client API helps its users to connect to a database (similar to a traditional database) and then perform all kinds of database operations. The following categories of programming APIs can be used while working with NosDB:
To access any of the above APIs, the first step is to connect to a database and for that you will need a connection string. A connection string is comprised of:
In the following example I am using a standalone database so I am required to set the Local Instance to true.
// connection string
string conn = "Data Source=20.200.20.44; Port=9950; " +
"Database=northwind; " +
"Local Instance=true; " +
"Integrated Security=true;";
// initialize database
Database db = NosDB.InitializeDatabase(conn);
The next step is to get a collection to work with. A collection is analogous to a table in a relational database. Like a table in a relational database, a collection can store multiple documents (records) but they are not bound by any schema. Collections can also be considered as a logical separation of different types of documents. For example, the collection "Products" stores all of the products of a company in one logical location. Whereas physically, if the database is clustered, the data might have been distributed to multiple shards (Servers).
So when you get a collection to work with, the client API automatically fetches and initiates a connection with all the required server machines in the background simplifying a lot of the complexities.
While working with NosDB collections, the Client API allows you the option to either work with your .NET objects while fetching or inserting data or the JSONDocument object (Alachisoft.NosDB.Common.JSONDocument). Irrespective of what you choose, NosDB stores data in the standard JSON format and the client API is responsible to automatically convert your objects into JSON format. The examples in this article will largely be using .NET objects.
The following example shows how to fetch the collection where, for instance, all the products of a company are stored.
// create collection instance of Product class type
Collection<Products> collection = db.GetCollection<Products>("Products");
In the above code, the string "Products" is the collection name. The other way to get a collection is as a container of JSONDocuments in which you can individually add/fetch keys and values.
//create collection instance of JSONDocument type
Collection<JSONDocument> collection = null;
collection = db.GetCollection("Products");
Here-on, all of the objects fetched from the above collection will be objects of the Product Class.
After you have your database and collection objects initiated you can now perform basic and advanced CRUD operations. NosDB offers four basic operation types which you may use on a single document:
The InserDocument() API inserts a single document in the database. Following is a code snippet demonstrating the use of this API specific to a custom .NET object. If your collection is a multi-sharded collection, the client API will direct this operation to the specific shard. Data will be inserted in a single network hop.
Collection<Products> collection = db.GetCollection<Products>("Products");
Product product = new Product();
product.ProductID = "12";
product.Name = "Chai";
Product.Count = 10;
collection.InsertDocument(product);
Or If you're working with JSONDocuments
Collection collection = db.GetCollection("Products");
JSONDocument product = new JSONDocument();
product.Key = "key1";
product.Add("ProductID", "12");
product.Add("Name", "Chai");
product.Add("Count",15);
collection.InsertDocument(product);
The GetDocument() API fetches an existing document from the database. All JSON documents in NosDB are stored with a key associated with it. To fetch a document, simply provide that key. When you're inserting documents without specifying keys, NosDB automatically generates one for you upon data insertion. Keys are used to uniquely identify a JSON document in the database. Following is how to fetch documents when the key is known.
Following is how to fetch documents when the key is known.
//Automatically fetches the JSON format and converts it to Product object
Product result = collection.GetDocument("key1");
//perform operations
If you're working with JSON Documents
JSONDocument result = collection.GetDocument("key1");
//perform operations
The UpdateDocument() API will replace a document that already exists in the database. Once again, knowing the key helps you to directly work with the JSON document.
Product product = new Product();
product.Key = 'key1';
product.ProductID = "12";
product.Name = "Chai";
Product.Count = 15;
collection.ReplaceDocument(product);
The DeleteDocument() API deletes a document from the database by taking a string-based key as its input:
//with default (InMemory) WriteConcern
collection.DeleteDocument("key1");
With bulk operations, you can insert, get or update multiple documents at any given instance in a single API call. Performing operations in bulk helps decrease the network cost which ultimately improves performance. When performing bulk operation on a clustered collection, you do not need to worry how the documents are distributed instead NosDB client API takes care of that for you. Bulk operations automatically fetch or insert documents to its respective shard (server) and combines the result whether success or failure individually. Operations to each shard are grouped and then sent together to save precious network bandwidth. Similar to basic operations, the bulk operations also support the four basic CRUD operations:
The InserDocuments() method uses an ICollection of documents along with an optional WriteConcern value to insert multiple documents into the database,. As previously stated, the documents will be automatically grouped together and sent to their respective shards saving network bandwidth.
product1.ProductID = "12";
product1.Name = "Chai";
Product.Count = 15;
product2.ProductID = "34";
product2.Name = "Aniseed Syrup";
Product.Count = 10;
List<Product> docs = new List<Product>();
docs.Add(product1);
docs.Add(product2);
List<FailedDocument> failedDocs = collection.InsertDocuments(docs);
The GetDocuments() method takes a list of keys or a specific criteria as input along with an optional ReadPreference value and the option to use caching. An IDBCollectionReader is returned in the result set which can be enumerated to read the results. The following example demonstrates fetching a set of documents on the basis of a defined criterion:
List<string> docKeys = new List<string> { "12", "34" };
// document keys to be fetched
// with default (PrimaryOnly) ReadPreference
ICollectionReader reader = collection.GetDocuments(docKeys);
while (reader.ReadNext())
{
Product result = reader.GetObject<Product>();
//perform operations
}
The UpdateDocuments() method takes a list of documents as input and replaces them in the database(against the unchanged keys):
product1.ProductID = "12";
product1.Name = "Chai";
Product.Count = 15;
product2.ProductID = "34";
product2.Name = "Aniseed Syrup";
Product.Count = 10;
List<Product> docs = new List<Products>();
docs.Add(product1);
docs.Add(product2);
List<FailedDocument> failedDocs = collection.UpdateDocuments(docs);
To delete documents in bulk, use the DeleteDocuments() API, which takes a list of string-based keys as input along with an optional WriteConcern value:
List<string> docKeys = new List<string> { "key1", "key2" };
// collection of document keys
List<FailedDocument> failedDocs = collection.DeleteDocuments(docKeys);
NosDB also provides an asynchronous counterpart for almost every synchronous operation. These can be used at times when you have high priority tasks to run and certain operations if executed in a synchronous fashion might cause unnecessary delays. This increases performance as well as do not hinder the normal working of your code. The failure or the success of these operations is returned in the callback later, if registered. As an example, here is the example of the basic async CRUD operations;
Simply create a list of JSONDocuments that you want to insert. You also have the option to provide Callbacks, object state and write concerns.
List<JSONDocument> docs = new List<JSONDocument>();
docs.Add(product1);
docs.Add(product2);
//provide list of
collection.InsertDocumentsAsync(docs, AsyncOperationCallBack, state, WriteConcern.InMemory);
In the above API call, Callbacks (BasicAsyncOperationCallBack) are methods which are triggered when the asynchronous operations are completed and return with a result either successful or failures.
private void AsyncOperationCallBack(AsyncEventArgs eventArgs)
{
if (eventArgs.Success)
{
//Successful operation
}
else
{
//Failed operation
}
}
The object state is an optional parameter provided to the developer allowing to identify each callback by registering a custom object. Since callbacks do not occur sequentially therefore object state can be used to enhance the developer experience and make things easy.
Specify WriteConcern as InMemory or Journal. This is an option of choosing extreme performance over data reliability. This is currently out of scope of this article so please see the documentation for more detail.
By default Callbacks and object state are null and the Write Concern is set to InMemory.
Similar to the insert API you have the update API. The callback is called when the operations is registered successful or failure by NosDB server. It also offers the same parameters to that of the Insert API.
List<JSONDocument> newDocs = new List<JSONDocument>();
docs.Add(product1);
docs.Add(product2);
collection.UpdateDocumentsAsync(newDocs, null, null, WriteConcern.InMemory);
Delete API requires keys to delete. Register callbacks when you want to make sure and notified of the operation result.
//documents to delete asynchronously
List<string> docKeys = new List<string> { "key1", "key2" };
collection.DeleteDocumentsAsync(docKeys, null, null, WriteConcern.InMemory);
Other than providing a direct API access to the database, NosDB also supports SQL querying. NosDB provides you the ability to fetch JSON documents and search them based on field values which are defined either as values, arrays or sub-documents. Therefore the NosDB SQL works completely well with embedded documents extending the Standard Query Language (SQL) to accommodate the flexibility that NoSDB provides. NosDB supports the following:
NosDB's recommended mode of transaction is through queries. Therefore, it extends the existing SQL execution methods to query on schema-less JSON documents in an effective manner. For a comprehensive set of supported SQL queries, please check out the SQL Cheat sheet.
For a brief overview, take a look at the following supported types of queries.
ExecuteReader() Method This method is used for the SELECT queries, where the result is returned in a reader as shown below:
string query= "SELECT Product.Name, Product.Count FROM " +
"Products WHERE Name = 'Chai'";
ICollectionReader reader = db.ExecuteReader(query);
while (reader.ReadNext())
{
//return result as JSONDocument
IJSONDocument data = reader.GetDocument();
Product product = new Product();
prodcuct.Name = data.GetString("Name");
prodcuct.Count = data.GetInt32("Count");
//perform operations
}
ExecuteNonQuery() Method This method usually applies to situations where the queries are required to modify the data in some manner and returns the number of rows affected in the result set. The following example demonstrates how to use the INSERT query:
string query =
"INSERT INTO Products" +
"(ProductID, ProductName, Product.Count) " +
"VALUES" +
"('1','Chai, 20)";
long rowsAffected = db.ExecuteNonQuery(query);
ExecuteScalar() Method For aggregate querying, NosDB offers the ExecuteScalar() method. This functionality comes in handy when only the first record of the result set is required or in other cases the query is expected to return a single result, for example Count queries. The following example demonstrates the use of aggregate querying:
string query = "SELECT count(*) FROM Products WHERE Product.Count > 10";
object count = db.ExecuteScalar(query);
//perform operations
NosDB offers DDL support which allows its users to "define" (create, alter, drop) NosDB resources. Moreover, it also supports backup and restoration of the existing databases.
string config = "{\"Database\": \"northwind\"," +
"\"Distribution\":{\"Strategy\":\"HashBased\"}," +
"\"PartitionKey\":"[{\"KeyName\":\"Age\",\"KeyType\":\"String\"}]}";
string query = "CREATE COLLECTION Products " + config;
db.ExecuteNonQuery(query);
Data Control Language (DCL) NosDB also supports the DCL, which enhances data access control with varying permissions which can be granted to or revoked from its users.
string role = "clusteradmin";
string resource = "cluster";
string login = "'JohnDoe'";
string query = "GRANT " + role + " ON " + resource + " TO " + login;
db.ExecuteNonQuery(query);
Since NosDB is a database, therefore its natural you would also want to store images and files along with JSON documents. Although, NosDB JSON has no size limit but it is advisable to keep the document size low and in check for optimal network bandwidth usage. Therefore, to store such binary data in the database, NosDB provides the feature of attachments.
This feature allows you to insert, fetch, replace and delete large files. Querying (through SQL) is also allowed to fetch the relevant attachment. Do note that modifying the contents of the attachments is not possible yet.
When storing binary data into NosDB, you open a stream to your file and provide it to the client API. The client API sends the file over the network in chunks to prevent choking up the network. If encryption is enabled, the file will be first encrypted before sending it over the network. As an example see how to insert and fetch images in NosDB database.
Please see that along with the attachment we can also provide some metadata to describe the attachment itself. This helps in querying the attachments which we'll see later on
AttachmentStore store = db.GetAttachmentStore();
// ID of file to insert
string attachmentId = "Image_1";
// metadata for the attachment
JSONDocument metadata = new JSONDocument();
metadata.Add("fileType", "png");
metadata.Add("info", "Images");
FileStream filestream = new FileStream(@"FilePath", FileMode.Open, FileAccess.Read);
store.InsertAttachment(attachmentId, filestream, metadata);
When you try to fetch an attachment, NosDB opens up an AttachmentStream to the file. You can read the stream and write it to a local file.
// ID of the file to fetch
string attachmentId = "Image_1";
Attachment attachment = store.GetAttachment(attachmentId);
// get attachment metadata only
IJSONDocument metadata = attachment.Metadata;
// perform operations
// get attachment stream
AttachmentStream aStream = attachment.GetStream();
byte[] arr = new byte[aStream.Length];
aStream.Read(arr, 0, (int)aStream.Length);
using (FileStream filestream = new FileStream(@"F:\ReceivedFromServer.png",
FileMode.Create, FileAccess.Write))
{
filestream.Write(arr, 0, arr.Length);
}
aStream.dispose();
// close and dispose all streams
On the other hand when you want to fetch an attachments based on some criteria, you can perform SQL queries on its meta data to fetch the relevant files. For example
// ID of the file to fetch
string attachmentId = "Image_1";
Attachment attachment = store.GetAttachment(attachmentId);
// get attachment metadata only
IJSONDocument metadata = attachment.Metadata;
// perform operations
// get attachment stream
AttachmentStream aStream = attachment.GetStream();
byte[] arr = new byte[aStream.Length];
aStream.Read(arr, 0, (int)aStream.Length);
FileStream filestream;
using (filestream = new FileStream(@"FilePath", FileMode.Create, FileAccess.Write))
{
filestream.Write(arr, 0, arr.Length);
}
aStream.dispose();
// close and dispose all streams
With this, we reach the end of this article. The aim of this overview was to provide a quick walk-through regarding the features that NosDB has to offer. Going into the details of each feature was beyond the scope of this article.
Also Read: Introduction to Server Side API in NoSQL Database