Wer meinen Einträge die letzten Wochen verfolgt, wird bemerkt haben, das ich mich der Service Schicht angenommen habe. Ich bin mit ASP.NET Web Api nicht sonderlich glücklich, weil zu komplex und teilweise unfertig. Die (ADO.NET) WCF Dataservices sind wesentlich weiter, sehen für mich aber nicht zukunftsfähig aus. Also habe ich mir überlegt einen schmutzigen Service Prototypen zu bauen.
Meine Anforderungen
- Sicherheit mit ASP.NET Membership und Basic Authentifizierung
- REST in Reinform- Hateoas- also mindestens die Möglichkeit LINK einzubauen
- Nur Read
- Odata Query Syntax Support
- Einfach zu verstehen
und ich habe ein Ergebnis mit dem ich ganz leidlich zufrieden bin. Manchmal schreibe ich den Code so richtig von Hand komplett selber und manchmal habe ich eine Bibliothek gefunden die fast perfekt passt. Ich habe mir auch Sourcen von diversen Code Portalen reingezogen habe mich dann aber immer dagegen entschieden, weil mir der Aufwand zu hoch schien das an meine Bedürfnisse anzupassen.
Über das Security Konzept schreibe ich einen eigenen Blog Artikel.
HATEOAS
Ich hatte entdeckt, das man mit dem JsonSerializer quasi mit einer Zeile Code aus einer ASPX Webform Seite eine REST Service machen kann.
Ich bin mir sicher, das es einige gibt die mich für so was hassen.
Damit kann ich auch mein Ziel nicht erreichen. Also wechsle ich in der Webform einfach in das Codebehind. Der HTML Part in der ASPX Seite wird komplett entfernt. Der Namensraum Newtonsoft muss natürlich importiert werden bzw. das Paket per Nuget geladen werden.
1: Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
2: Dim nw = New NorthwindEntities1
3: Dim json As String =
JsonConvert.SerializeObject(nw, Formatting.Indented, New LinkJsonConverter(GetType(Customers)))
4: Response.Write(json)
5: End Sub
Also nun kann der Benutzer die ASPX Seite aufrufen und erhält die Daten im Json Format. Wer obigen Code ausführt wird eine Fehlermeldung erhalten, weil die Klasse LinkJsonConverter erst noch implementiert werden muss. Zweck dieser Klasse ist es, beim konvertieren [Link] einzufügen um den Hypermedia Ansprüchen aus HATEOAS zu genügen. Jedenfalls kann so aus einer Liste von Customers der Empfänger erkennen, wie er einen einzelnen Eintrag abrufen kann.
Für die Art und Weise wie man in den Json Text die Verlinkung einbaut, gibt es keinen Standard, aber eine Menge Leute die glauben einen definieren zu können (HAL, Siren, Collection+Json) Ist auch nicht wichtig. Ich kann in den Json Serializer eingreifen und damit meinen eigenen Standard schaffen. Dafür gibt's eine vom JsonConverter geerbte Klasse, die im Event Write immer für jeden neuen Kunden ein Link Element anfügt.
1: Public Class LinkJsonConverter
2: Inherits JsonConverter
3: Private ReadOnly _types As Type()
4:
5: Public Sub New(ParamArray types As Type())
6: _types = types
7: End Sub
8:
9: Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
10: Dim t As JToken = JToken.FromObject(value)
11: If t.Type <> JTokenType.Object Then
12: t.WriteTo(writer)
13: Else
14: Dim o As JObject = DirectCast(t, JObject)
15: Dim Link = o.Properties().Select(Function(p) p.Name)
16: o.AddFirst(New JProperty("Link", HttpContext.Current.Request.Path +
"?Customer_ID=" + o.Item("Customer_ID").ToString))
17: o.WriteTo(writer)
18: End If
19: End Sub
20:
21: Public Overrides Function ReadJson(reader As JsonReader, objectType As Type,
existingValue As Object, serializer As JsonSerializer) As Object
22:
23: End Function
24:
25: Public Overrides ReadOnly Property CanRead() As Boolean
26: Get
27: Return False
28: End Get
29: End Property
30:
31: Public Overrides Function CanConvert(objectType As Type) As Boolean
32: Return _types.Any(Function(t) t = objectType)
33: End Function
34: End Class
Wie soll nun so ein Link aufgebaut sein? Relativ, Absolut? Per /ID oder (id) oder ganz anders? Ihre Entscheidung. In diesem Fall per Querystring.
Man muss nicht unbedingt einen JsonConverter schreiben. Alternativen sind z.B. zwischen Objekte die die passenden Attribute und Werte halten oder per Attribut (<JsonProperty()>) im Modell. Im ersteren Fall würde dann eine Klasse JsconCustomer geschaffen die ein Link Property Zusätzlich besitzt. Dies füllt man per LINQ und serialisiert das dann direkt wie hier schon gezeigt.
Als nächstes geht es um das Problem aus einem Odata ähnlichen Querstring ein LINQ Kommando zu machen. Ich habe dafür die ganz wunderbare Library gefunden LINQ2REST. Sie stammt von Jacob Reimers als Source oder Nuget. Dies hängt sich mit einer Extension Method Filter in LINQ rein
1: Imports Linq2Rest
2: ...
3:
4: filterdNW = nw.Customers.Filter(Request.Params)
5: Dim json As String = JsonConvert.SerializeObject(filterdNW, Formatting.Indented,
New LinkJsonConverter(GetType(Customers)))
6: Response.Write(json)
Damit kann zb folgendes erfolgreich $top oder $filter verwenden um die Ergebnismenge einzuschränken. Das hat mich wirklich begeistert.
Es gibt allerdings noch ein kleines Problem. Ich habe mir in den Kopf gesetzt einen Bypass verwenden zu können um auf einen Entität zugreifen zu können. Orientiert habe ich mich an dem Hypermedia Link customers.aspx?Customer_ID=ALFKI. Der steht laut meiner Definition so in den Json Daten und sollte für den Client auch aufrufbar sein. Darüberhinaus habe ich mir Überlegt das bei Relationen dieses Szenario flexibler sein sollte, so das ich alle Felder auswählen kann. Gefunden habe ich eine Lösung mit Dynamic LINQ die schon rund 5 Jahre alt ist und in der Tat auch per Nuget installiert werden kann (System.Linq.Dynamic). Sogar VB und C# Quellcode findet sich im Blog von Scott Guthrie, so das man die Klasse auch als Datei einbinden kann. Es wird die Where Methode überladen.
Kurz gesagt kann man damit aus einem Text direkt LINQ generieren lassen.
1: Dim nw = New NorthwindEntities1
2: Dim filterdNW
3: If Request.Params.ToString.Contains("$") Then
4: filterdNW = nw.Customers.Filter(Request.Params)
5: ElseIf Request.QueryString.Count = 0 Then
6: filterdNW = nw.Customers
7: Else
8: Dim q = Request.Params.Item(0)
9: Dim k = Request.Params.Keys(0)
10: Dim l = k + "= @0"
11: filterdNW = nw.Customers.Where(l, q).FirstOrDefault()
12: End If
13: Dim json As String = JsonConvert.SerializeObject(filterdNW, Formatting.Indented,
New LinkJsonConverter(GetType(Customers)))
14: Response.Write(json)
Letztendlich habe ich das Ziel erreicht. Der Code ist einfach und hängt nicht von Manipulationen der IIS Request Pipline ab. Damit kann ich auch in bestehende Web Anwendungen REST Schnittstellen integrieren.
Die Lösung ist nicht perfekt und auch nicht fertig. Für mich eine Frage ist, warum ich noch über LINQ gehen muss. Eigentlich könnte man ja auch Odata2SQL direkt wählen. Aber das wird sich auch noch klären