Azure Cosmos DB Emulator und ASP.NET Core WebAPI
Um eine Anwendung mit Azure Cosmos DB zu entwickeln ist es nicht immer nötig eine Azure Ressource zu buchen. Hier möchte ich zeigen, wie einfach eine ASP.NET Core WebAPI Anwendung mit dem Azure Cosmos DB Emulator entwickelt werden kann.
Zu allererst wird der Emulator benötigt. Dieser ist auf dieser Website zu finden: https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator#installation
Nachdem die Installation erfolgt ist, kann der Emulator gestartet werden. Der Emulator kann dann in der Taskleiste gefunden werden.
Mit der rechten Maustaste lässt sich das Menü öffnen und der Data Explorer öffnen. Dies ist eine lokal gehostete Website, welche Informationen zur Instanz anbietet und einen Explorer für die Daten enthält.
Die Uri und den Primary Key benötigen wir später in der Anwendung, um auf die Instanz zugreifen zu können. Diese Informationen können jederzeit eingesehen werden und müssen nicht notiert werden.
Öffnet man den Explorer aus der Seitenleiste, so findet man eine leere Instanz. Diese müssen wir nun konfigurieren. Dabei ist die Oberfläche nah angelehnt an das Azure Portal.
Dort müssen wir eine Collection anlegen, welche unsere Daten enthält. Dies gelingt uns über den Button „New Collection“.
Ich nenne die Datenbank „data“ und die Collection ebenfalls „data“, da ich nicht plane, mehrere Datenbanken oder Collections für dieses Beispiel zu verwenden. Als Partition Key wähle ich „/doctype“. Die Eigenschaft „doctype“ verwende ich zum Identifizieren von verschiedenen Models.
Nachdem die Collection angelegt wurde, ist der Azure Cosmos DB Emulator einsatzbereit.
Nun widmen wir uns dem WebAPI Projekt. Dieses lege ich mit Visual Studio 2019 als ASP.NET Core Web Application an. Als Typ wähle ich Empty.
Sobald das Projekt erstellt wurde, passe ich die Startup.cs an, damit ich MVC Controller für die RESTful API verwenden kann.
Folgende Zeile füge ich in die ConfigureServices Methode ein, damit MVC verwendet werden kann:
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Nun muss noch MVC in der Request Pipeline verwendet werden:
app.UseMvc();
Die Klasse sieht nun wie folgt aus:
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.TryAddSingleton(CreateDocumentClient);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
}
Nun legen wir den Ordner Controllers an und darin die Datei NotebookController.cs. Diese enthält die Klasse NotebookController und erbt von ControllerBase. Ich definiere einige Funktionen für das REST Prinzip. Die Klasse sieht wie folgt aus:
[Route("[controller]")]
public class NotebookController : ControllerBase
{
[HttpGet]
public async Task<IEnumerable> GetAll(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
[HttpGet("{id:Guid}")]
public async Task Get(Guid id, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
[HttpPost]
public async Task<ActionResult> Create([FromBody] Notebook notebook, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
[HttpDelete("{id:Guid}")]
public async Task Delete(Guid id, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
Nachdem dies fertig ist, können wir das Nuget Paket für Cosmos DB hinzufügen. Diese nennt sich Microsoft.Azure.DocumentDB.Core.
Um auf die Cosmos DB zugreifen zu können, für ich die Sektion CosmosDb in die appsettings.json hinzu:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"CosmosDb": {
"ServiceUri": "https://localhost:8081",
"AuthKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
}
}
Jetzt erstellen wir die Klassen für die Entitäten. Diese bestehen aus einer Basis-Klasse, welche die verpflichtenden Eigenschaften für Cosmos DB enthält (ID und Partition Key) und die Klasse für ein Notebook mit RAM-Riegeln.
public abstract class Entity
{
[JsonProperty("id")]
public Guid Id { get; set; }
[JsonProperty("doctype")]
public abstract string DocumentType { get; }
}
public enum RAMShape
{
DIMM = 0,
SODIMM
}
public enum RAMStandard
{
DDR = 0,
DDR2,
DDR3,
DDR4
}
public class RAM
{
[JsonProperty("manufacturer")]
public string Manufacturer { get; set; }
/**
* Size in GB
*/
[JsonProperty("size")]
public int Size { get; set; }
/**
* Speed in MHz
*/
[JsonProperty("speed")]
public int Speed { get; set; }
[JsonProperty("shape")]
public RAMShape Shape { get; set; }
[JsonProperty("standard")]
public RAMStandard Standard { get; set; }
}
public class Notebook : Entity
{
public override string DocumentType => "notebook";
[JsonProperty("manufacturer")]
public string Manufacturer { get; set; }
[JsonProperty("model")]
public string Model { get; set; }
[JsonProperty("cpu")]
public string CPU { get; set; }
[JsonProperty("ram")]
public IEnumerable RAM { get; set; }
}
Nun muss noch der DocumentClient implementiert werden. Dies möchte ich natürlich mit Dependency Injection erreichen. Hierfür füge ich eine statische Methode hinzu, welche einen client zurückgibt:
private static DocumentClient CreateDocumentClient(IServiceProvider serviceProvider)
{
var configuration = serviceProvider.GetService();
var section = configuration.GetSection("CosmosDb");
var serviceUri = section.GetValue("ServiceUri");
var authKey = section.GetValue("AuthKey");
var client = new DocumentClient(new Uri(serviceUri), authKey);
client.ConnectionPolicy.ConnectionMode = ConnectionMode.Direct;
// see https://azure.microsoft.com/en-us/blog/performance-tips-for-azure-documentdb-part-1-2/
#if DEBUG
client.ConnectionPolicy.ConnectionProtocol = Protocol.Https;
#else
client.ConnectionPolicy.ConnectionProtocol = Protocol.Tcp;
#endif
return client;
}
Diese Funktion nutze ich, um einen Singleton in der ConfigureServices Methode zu erstellen:
services.TryAddSingleton(CreateDocumentClient);
Die neue Startup-Klasse sieht so aus:
public class Startup {
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https:
//go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services) {
services.TryAddSingleton(CreateDocumentClient); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
private static DocumentClient CreateDocumentClient(IServiceProvider serviceProvider) {
var configuration = serviceProvider.GetService();
var section = configuration.GetSection("CosmosDb");
var serviceUri = section.GetValue("ServiceUri");
var authKey = section.GetValue("AuthKey");
var client = new DocumentClient(new Uri(serviceUri), authKey);
client.ConnectionPolicy.ConnectionMode = ConnectionMode.Direct;
// see https:
//azure.microsoft.com/en-us/blog/performance-tips-for-azure-documentdb-part-1-2/
#if DEBUG
client.ConnectionPolicy.ConnectionProtocol = Protocol.Https;
#else
client.ConnectionPolicy.ConnectionProtocol = Protocol.Tcp;
#endif
return client;
}
}
Nachdem nun der DocumentClient über Dependency Injection verfügbar ist, können wir diesen im NotebookController verwenden, um unsere Objekte zu erstellen und abzurufen.
Zuerst müssen wir den DocumentClient über den Konstruktor der Klasse anfordern und dann in einer Klassen-Eigenschaft zwischenspeichern.
private readonly DocumentClient _documentClient;
public NotebookController (DocumentClient documentClient)
{
_documentClient = documentClient;
}
Dann legen wir noch eine private Klassen-Eigenschaft für den Collection-Pfad an.
private Uri CollectionUri => UriFactory.CreateDocumentCollectionUri("data", "data");
Der Einfachheit halber ist dieser hier hart codiert.
Zum Abrufen der Entitäten aus der Cosmos DB nutze ich die Methode DocumentClient.CreateDocumentQuery, zum Erstellen von Entitäten verwende ich DocumentClient.CreateDocumentAsync und zum Löschen von Entitäten verwende ich DocumentClient.DeleteDocumentAsync. Jede der Methoden erwartet den PartitionKey. Die fertige Klasse sieht schließlich so aus:
[Route("[controller]")]
public class NotebookController : ControllerBase
{
private readonly DocumentClient _documentClient;
private Uri CollectionUri => UriFactory.CreateDocumentCollectionUri("data", "data");
public NotebookController (DocumentClient documentClient)
{
_documentClient = documentClient;
}
[HttpGet]
public async Task GetAll(CancellationToken cancellationToken)
{
await _documentClient.OpenAsync(cancellationToken);
var query = _documentClient.CreateDocumentQuery(CollectionUri, new FeedOptions { MaxItemCount = -1, PartitionKey = new PartitionKey("notebook") }).AsDocumentQuery();
return await query.ExecuteNextAsync(cancellationToken);
}
[HttpGet("{id:Guid}")]
public async Task Get(Guid id, CancellationToken cancellationToken)
{
await _documentClient.OpenAsync(cancellationToken);
var query = _documentClient.CreateDocumentQuery(CollectionUri, new FeedOptions { MaxItemCount = -1, PartitionKey = new PartitionKey("notebook") }).Where(_ => _.Id == id).AsDocumentQuery();
var result = await query.ExecuteNextAsync(cancellationToken);
return result.SingleOrDefault();
}
[HttpPost]
public async Task Create([FromBody] Notebook notebook, CancellationToken cancellationToken)
{
await _documentClient.OpenAsync(cancellationToken);
await _documentClient.CreateDocumentAsync(CollectionUri, notebook, cancellationToken: cancellationToken);
var routeData = new Dictionary()
{
{ "id", notebook.Id.ToString() }
};
return Created(Url.Action("Get", routeData), notebook);
}
[HttpDelete("{id:Guid}")]
public async Task Delete(Guid id, CancellationToken cancellationToken)
{
await _documentClient.OpenAsync(cancellationToken);
var documentUri = UriFactory.CreateDocumentUri("data", "data", id.ToString());
await _documentClient.DeleteDocumentAsync(documentUri, new RequestOptions { PartitionKey = new PartitionKey("item") }, cancellationToken);
return NoContent();
}
}
Jetzt wo der Code fertig ist, testen wir den Webservice. Damit nicht immer ein Browser geöffnet wird, passe ich die launchSettings.json an. Dort setze ich die Eigenschaft launchBrowser auf false.
Sobald die Anwendung läuft, starte ich ein Tool zum Ausführen von Web-Requests, in meinem Fall Postman.
Dort führe ich den GetAll-Request aus, um die Verbindung zur Instanz zu prüfen.
Ein leeres Array als Rückgabewert zeigt, dass die Verbindung geklappt hat.
Nun erstellen wir ein Objekt mit dem Create-Request.
Dies scheint auch erfolgreich zu sein, da das Objekt ohne Fehler zurückgegeben wurde.
Dies prüfen wir nochmals mit dem ersten GetAll-Request.
Man sieht, dass das Objekt zurückgegeben wurde.
Dies zeigt, wie schnell man eine Anwendung mit dem Azure Cosmos DB Emulator entwickeln kann.
![Bild von Philip Szalla](https://24882854.fs1.hubspotusercontent-eu1.net/hub/24882854/hubfs/Philip-Szalla.jpg?width=140&height=140&name=Philip-Szalla.jpg)