Nachdem wir euch im letzten Teil unserer Blog-Serie in unseren Use-Case für virtuelle Entitäten eingeführt haben, möchten wir euch heute die technische Realisierung des zugehörigen Daten Providers vorstellen. Dieser ermöglicht als Bindeglied zwischen Dynamics365 und eurer externen Datenquelle den Austausch der Daten. So gelangen die Tickets unseres fiktiven Webshops aus der Datenbank also als virtuelle Entitäten in Dynamics 365.
OData ist dabei ein offener Standard, der vorgibt, wie eine ReST-API angesprochen wird. Auch die Web API von Dynamics365 implementiert diesen. Eine nahtlose Integration ohne großen Aufwand lässt sich daher über die Registrierung eines OData Providers als virtuelle Datenquelle in Dynamics365 bewerkstelligen.
Wir zeigen euch am Beispiel der Ticket-Entität, wie ihr einen solchen OData-Service entwickelt.
Konfiguration des OData-Service
Unser OData Service wird als Web API realisiert und baut daher auf dem ASP.Net Core Framework von Microsoft mit der aktuellen .Net Version 5.0 auf. Für die Unterstützung des Protokolls kommt das NuGet-Package Microsoft.AspNetCore.OData zum Einsatz, das ihr zusätzlich in Visual Studio eurer Solution hinzufügen müsst. Die Konfiguration findet wie üblich in der Startup-Klasse des Projektes statt.
In der ConfigureServices-Methode werden dem Web-Server die benötigten Middleware-Services hinzugefügt. Neben der Einbindung des Entity Framework Kontextes (dazu gleich mehr) wird hier die zuvor installierte OData Middleware mittels services.AddOData()
aktiviert.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<HttpBasicAuthorizeFilter>();
services.AddDbContext<EventTicketDbContext>(builder =>
{
builder.UseSqlServer(Configuration.GetConnectionString("EventTicket"));
});
services.AddControllers();
services.AddOData();
services.AddApplicationInsightsTelemetry(Configuration["APPINSIGHTS_CONNECTIONSTRING"]);
}
Der nächste Schritt findet in der Configure-Methode statt, die sich ebenfalls in der Startup-Klasse befindet. Hier wird die ASP .Net Anwendung konfiguriert und deren Verhalten zur Laufzeit gesteuert. Mit der Anweisung UseRouting
wird zunächst das Routing aktiviert und schließlich mit dem Aufruf UseEndpoints
die Bereitstellung der OData-Endpunkte konfiguriert. Neben den möglichen OData-Methoden (hier: Select, OderBy, Filter, Count und MaxTop) wird auch das dem Service zugrundeliegende Entity Data Model (EDM) erstellt, das die Basis eines jeden OData Services darstellt.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseEndpoints(builder =>
{
builder.Select()
.OrderBy()
.Filter()
.Count()
.MaxTop(null);
builder.MapODataRoute("odata", "odata", GetEdmModel());
});
}
Das EDM beschreibt im Grunde genommen die abrufbaren Entitäten und deren Struktur samt Schlüssel-Attribut, Felder mit Datentypen, OptionSets, etc. In diesem Fall wird schlicht und ergreifend die Ticket-Klasse als EntitySet hinzugefügt. Der Route-Builder kann die Struktur dann direkt aus der Klasse ermitteln und diese über den Metadaten-Endpunkt zur Verfügung stellen.
private IEdmModel GetEdmModel()
{
var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<Ticket>("Tickets");
return odataBuilder.GetEdmModel();
}
Entity Framework Core
Für die Anbindung der Datenbank kommt das Entity Framework Core (EFCore) als ORM-Framework zum Einsatz. Im Datenbank-Kontext wird hierfür die Ticket-Klasse als DbSet eingebunden, worüber dann Abfragen gegen die SQL-Datenbank ausgeführt werden können.
public class EventTicketDbContext : DbContext
{
public DbSet<Ticket> Ticket { get; set; }
public EventTicketDbContext()
{
}
public EventTicketDbContext(DbContextOptions<EventTicketDbContext> options)
:base(options)
{
}
}
Die Ticket-Klasse selbst besitzt lediglich eine Hand voll Eigenschaften mit relativ simplen Datentypen. Zur Kennzeichnung des Primärschlüssels kommt das Key-Attribut zum Einsatz (klassisches ComponentModel), das sowohl vom OData- als auch Entity-Framework erkannt wird. Damit ist die Konfiguration der Entität vollständig und es kann weitergehen mit der Erstellung des Endpunktes.
public class Ticket
{
[Key] public Guid TicketId { get; set; }
public Guid EventId { get; set; }
public Guid KontaktId { get; set; }
public string Ticketnummer { get; set; }
public decimal Einzelpreis { get; set; }
}
OData-Endpunkte
Damit nun auch die Ticket-Datensätze von außen über die Schnittstelle angefragt werden können, ist es notwendig einen ASP .Net Controller zu erstellen, der die Anfragen entgegen nimmt und verarbeitet.
Eine wichtige Anmerkung an dieser Stelle:
Sämtliche Parameter und Datentypen unterliegen strengen OData-Konventionen. Es muss daher penibel darauf geachtet werden, diese etwa in Sachen Namenskonvention einzuhalten, um später auch einen funktionierenden Service zu erhalten (z.B. muss der Eindeutige Bezeichner in der Get-Methode tatsächlich key heißen).
Der TicketsController besitzt genau zwei Endpunkte für Anfragen. Der erste ermöglicht die Suche nach Tickets mithilfe aller in der Startup-Klasse konfigurierten OData-Methoden. Dies geschieht durch die Rückgabe des DbSets dieser Entität. Damit ist die Implementierung bereits abgeschlossen und das OData-Framework konvertiert Anfragen gegen den Endpunkt direkt in Entity Framework-Abfragen. Dasselbe gilt auch für den zweiten Endpunkt, der das Laden eines einzelnen Tickets anhand dessen Id ermöglicht.
[ServiceFilter(typeof(HttpBasicAuthorizeFilter))]
public class TicketsController : ODataController
{
private readonly EventTicketDbContext _dbContext;
public TicketsController(EventTicketDbContext dbContext)
{
_dbContext = dbContext;
}
[EnableQuery]
public IQueryable<Ticket> Get()
{
return _dbContext.Ticket;
}
[EnableQuery]
public Ticket Get([FromODataUri] Guid key)
{
return _dbContext.Ticket.Single(l => l.TicketId == key);
}
}
Damit die Daten nicht gänzlich öffentlich zugänglich sind, wird mit einem simplen API-Key-Mechanismus eine rudimentäre Authentifizierung implementiert. Der zugehörige ActionFilter wird vor jedem Aufruf eines Endpunktes ausgeführt und überprüft den HTTP-Request auf die Existenz eines entsprechenden Query-Parameters. Sollte dieser nicht ordnungsgemäß mitgeliefert werden, wird die Anfrage mit dem Status Code 401 (Unauthorized) abgelehnt.
public class HttpBasicAuthorizeFilter : IActionFilter
{
private const string ApiKey = "1234567890";
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.Request.Query.TryGetValue("ApiKey", out StringValues apiKey))
{
if (apiKey.ToString().Equals(ApiKey))
{
return;
}
}
context.Result = new UnauthorizedResult();
}
}
Fazit
Das war der technische Teil – geschafft!
Ihr solltet nun einen funktionierenden OData Service haben, den ihr als ASP .NET Core Web Anwendung bereitstellen könnt. Wie dieser in Dynamics365 als virtuelle Datenquelle registriert wird und das System darüber die virtuellen Entitäten erzeugt, zeigen wir euch dann im nächsten Teil.