Data storage
Azure DevOps Services | Azure DevOps Server 2022 - Azure DevOps Server 2019
Azure DevOps extensions can store user preferences and complex data structures directly on Microsoft-provided infrastructure, which ensures your user's data is secure and backed up just like other organization and project data. It also means for simple data storage needs, you, as the extension provider, aren't required to set up, manage, or pay for third-party data storage services.
There are two methods to engage with the data storage service: through REST APIs or via a client service provided by Microsoft, which is part of the VSS SDK. We advise extension developers to utilize the provided client service APIs, as they offer a user-friendly encapsulation of the REST APIs.
Note
Looking for Azure DevOps REST APIs? See the latest Azure DevOps REST API reference.
For information about .NET client libraries, see .NET client libraries for Azure DevOps.
What you can store
The service is designed to let you store and manage two different types of data:
- Settings: simple key-value settings (like user preferences)
- Documents: collections of similar complex objects (documents)
A collection is as an indexed container for documents. A document is a JSON blob that belongs to a collection. Other than a few reserved property names, you control and manage the schema of these documents.
How you can scope data
Settings and document collections can be scoped to either the:
- Project Collection: shared by all users of the project collection to which the extension is installed
- User: a single user of a project collection to which the extension is installed
Settings storage
The two main methods for managing settings are getValue()
and setValue()
:
getValue()
accepts a string key (along with other options like scope) and returns an IPromise. The resolved value of this promise is the value associated with the provided key.setValue()
accepts a string key, a value, and other options such as scope, and returns an IPromise. The resolved value of this promise is the updated value of the setting.
Here's an example of how to set a value:
private async initializeState(): Promise<void> {
await SDK.ready();
const accessToken = await SDK.getAccessToken();
const extDataService = await SDK.getService<IExtensionDataService>(CommonServiceIds.ExtensionDataService);
this._dataManager = await extDataService.getExtensionDataManager(SDK.getExtensionContext().id, accessToken);
this._dataManager.getValue<string>("test-id").then((data) => {
this.setState({
dataText: data,
persistedText: data,
ready: true
});
}, () => {
this.setState({
dataText: "",
ready: true
});
});
}
Here's an example of how to retrieve a setting value:
// Get data service
SDK.getService(SDK.getContributionId()).then(function(dataService) {
// Get value in user scope
dataService.getValue("userScopedKey", {scopeType: "User"}).then(function(value) {
console.log("User scoped key value is " + value);
});
});
If scopeType
isn't specified, the settings are stored at the project collection level and they're accessible to all users in that project collection using the extension.
Here's an example of how to set a setting value at the project collection level:
// Get data service
SDK.getService(SDK.getContributionId()).then(function(dataService) {
// Set value (default is project collection scope)
dataService.setValue("someKey", "abcd-efgh").then(function(value) {
console.log("Key value is " + value);
});
});
Data (collections of documents) storage
To handle more complex data beyond key-value pairs, you can utilize the concept of documents to execute CRUD operations on your extension's data. A document is a JSON blob, enhanced with two special properties: ID and __etag
. If they're crucial to an extension's data model, IDs can be user-defined, or if left unspecified, the system generates them. These IDs must be unique within a specific collection. Since a collection refers to a particular scope and instance of an extension, it implies that the same document ID can be reused across different collections.
The following document operations are available:
- Get a document
- Create a document
- Set a document (create or update)
- Update a document
- Delete a document
There's also a single operation that can be performed on a collection: Get all documents
Get a document by ID
Obtaining a document from a collection using its identifier is straightforward, like the following example:
// Acquire data service
SDK.getService(SDK.getContributionId()).then(function(dataService) {
// Retrieve document by id
dataService.getDocument("MyCollection", "MyDocumentId").then(function(doc) {
// Assuming document has a property named foo
console.log("Doc foo: " + doc.foo);
});
});
This operation tries to fetch a document with the ID "MyDocumentId" from the "MyCollection" collection. In the absence of a provided scope, the service defaults to using the collection scoped to the entire instance of this extension. If either this collection or a document with the specified ID doesn't exist, a 404 error is returned, which the extension should handle. The returned document is a JSON object that includes all its properties, along with the special ID and __etag
properties utilized by the data storage service.
// Get data service
SDK.getService(SDK.getContributionId()).then(function(dataService) {
// Get document by id
dataService.getDocument("MyCollection", "MyDocumentId").then(function(doc) {
// Assuming document has a property named foo
console.log("Doc foo: " + doc.foo);
});
});
This call attempts to retrieve a document with the ID "MyDocumentId," from the collection "MyCollection." Since no scope is provided, the collection that the service uses gets scoped to the default of the entire instance of this extension. If this collection doesn't exist or a document with that ID doesn't exist, then a 404 gets returned, which the extension should handle. The document that is returned is a JSON object containing all of its own properties, in addition to the special ID and __etag
properties used by the data storage service.
Create a document
To create a new document, perform a call like the following example:
// Get data service
SDK.getService(SDK.getContributionId()).then(function(dataService) {
// Prepare document first
var newDoc = {
fullScreen: false,
screenWidth: 500
};
dataService.createDocument("MyCollection", newDoc).then(function(doc) {
// Even if no ID was passed to createDocument, one gets generated
console.log("Doc id: " + doc.id);
});
});
If the collection with the name and scope provided, doesn't yet exist, it gets created dynamically before the document itself is created.
If the document provided contains an id
property, that value gets used as the unique ID for the document. Please note that the provided id
should be limited to 50 characters. If that field doesn't exist, a GUID gets generated by the service and included on the document that is returned when the promise is resolved.
If another document in the collection already exists with the same ID as the one provided on the document, the operation fails. If the desired behavior is create a new document if the ID doesn't exist, but modify the existing document if it does, then the setDocument()
method should be used.
Set a document (update or create)
The setDocument()
function carries out an "upsert" operation - it modifies an existing document if its ID is present and matches a document in the collection. If the ID is absent or doesn't correspond to any document in the collection, then a new document is added to the collection.
// Get data service
SDK.getService(SDK.getContributionId()).then(function(dataService) {
// Prepare document first
var myDoc = {
id: 1,
fullScreen: false,
screenWidth: 500
};
dataService.setDocument("MyCollection", myDoc).then(function(doc) {
console.log("Doc id: " + doc.id);
});
});
Update a document
The updateDocument
function requires that the document being altered already resides in the collection. An exception is thrown if no ID is supplied or if the provided ID doesn't correspond to any document in the collection.
Here's an example of how update is used:
// Get data service
SDK.getService(SDK.getContributionId()).then(function(dataService) {
var collection = "MyCollection";
var docId = "1234-4567-8910";
// Get document first
dataService.getDocument(collection, docId, { scopeType: "User" }).then(function(doc) {
// Update the document
doc.name = "John Doe";
dataService.updateDocument(collection, doc, { scopeType: "User" }).then(function(d) {
// Check the new version
console.log("Doc version: " + d.__etag);
});
});
});
Delete a document
This function deletes the document with the provided ID from the provided collection. If the collection doesn't exist or the document doesn't exist, a 404 gets returned.
Here's an example usage:
// Get data service
SDK.getService(SDK.getContributionId()).then(function(dataService) {
var docId = "1234-4567-8910";
// Delete document
dataService.deleteDocument("MyCollection", docId).then(function() {
console.log("Doc deleted");
});
});
Get all documents in a collection
The following example retrieves all documents from the "MyCollection" collection using the data service, and then logs the number of documents to the console:
// Get data service
SDK.getService(SDK.getContributionId()).then(function(dataService) {
// Get all document under the collection
dataService.getDocuments("MyCollection").then(function(docs) {
console.log("There are " + docs.length + " in the collection.");
});
});
This call retrieves all documents in a scoped collection, with a limit of 100,000 documents. If the collection is nonexistent, it returns a 404 error.
Advanced
How settings get stored
This call encapsulates the setDocument
client method, supplying it with multiple pieces of data. As stated before, settings are internally saved as documents. Therefore, a basic document is dynamically generated, where the document's ID is the key given in the setValue()
method. The document has two more properties. The value
property holds the value passed to the method, and the revision
property is set to -1
. While the revision
property is elaborated more in the "Working with Documents" section, in the context of settings, setting revision
to -1
in the document signifies that we're not concerned with the versioning of this settings document.
Because settings are stored as documents, we need to provide a collection name, indicating where to store the document. To keep things simple, when working with the setValue()
/getValue()
methods, the collection name is always the special name $settings
. The previous call issues a PUT Request at the following endpoint:
GET _apis/ExtensionManagement/InstalledExtensions/{publisherName}/{extensionName}/Data/Scopes/User/Me/Collections/%24settings/Documents
The request payload is like the following example:
{
"id": "myKey",
"__etag": -1,
"value": "myValue"
}
REST APIs
Assuming this snippet is executed after the value is set, you should see an alert message containing the text "The value is myValue." The getValue method is again a wrapper around the REST APIs, issuing a GET request to the following endpoint:
GET _apis/ExtensionManagement/InstalledExtensions/{publisherName}/{extensionName}/Data/Scopes/User/Me/Collections/%24settings/Documents/myKey
etags
The __etag
field is used by the Data Storage Service for document concurrency management. Before an update is saved, the service verifies that the __etag
of the currently stored document matches the __etag
of the updated document. If they match, the __etag
is incremented and the updated document is returned to the caller. If they don't match, it indicates that the document to be updated is outdated, and an exception is thrown. The extension writer is responsible for handling this exception gracefully, either by retrieving the latest __etag
of the document, merging the changes, and retrying the update, or by notifying the user.
For some types of documents, the level of concurrency provided might not be necessary, and a last-in-wins model might be more suitable. In such cases, while editing the document, input -1 as the __etag
value to signify this functionality. The previously mentioned settings service employs this model for storing settings and preferences.