Yet another OData twitterwall

Ik heb inmiddels al enkele blogposts gewijd aan OData. Het feit dat data zo
eenvoudig uitgewisseld kan worden tussen (onbekende) client en provider is een
groot pluspunt. En Microsoft heeft hierbij handig gebruik gemaakt van diverse al
bestaande standaarden.

Wall

Eén daarvan is Atom Publishing Protocol (AtomPub). Deze is vooral bekend
voor het weergeven van informatie voor feedreaders zoals
hieronder.

rss feed

AtomPub is gewoon het vervoermiddel, de container voor de data. Hierboven staat iets wat wij graag willen zelf lezen  maar er had ook een entiteit in XML notatie in beschreven kunnen zijn.

En daarom is dit protocol zo handig voor OData. Een OData provider spuwt
entiteiten uit, compleet met relaties en eventuele functies, in XML. En die
berichten worden standaard in AtomPud formaat aangeboden. Die feeds zijn niet bepaald vriendelijk om te lezen door alle ruwe XML die er in opgenomen is.

Maar wat als ik nu juist wel een ‘friendly feed’ wil aanbieden? Moet ik
die ruwe OData feed dan eerst door de mangel halen om dezelfde data alsnog
“vriendelijke leesbaar” te kunnen tonen, in een doorsnee feedreader?

Op de Mix van 2010 is gedemonsteerd hoe een OData feed uit twitter
in een feedreader getoond kan worden. Dit was een aardige introductie over het
bouwen van een OData provider zonder Entity framework. En tevens werd
gedemonstreerd hoe we een standaard OData feed direct ‘friendly’ kunnen
maken.

In deze blog wil ik dit kunstje nog eens herhalen. En we zullen tevens
zien dat deze feed eenvoudig is uit te breiden met extra functionaliteit zodat
deze eerst zo statische feed een soort van twitterwall wordt.

Iedere OData service heeft drie standaard onderdelen:

  • 1. Entities : Hoe zien de entiteiten er uit?
  • 2. Context: In welke context worden deze entiteiten als data geinstancieerd?
  • 3. Wcf Data Service: Wat zijn de ‘rechten’ en ‘plichten’ om de data te mogen bekijken

Dit toont goed aan wat OData verwacht van de data om die te kunnen
representeren. Normaal komt dit uit een Entity Framework (Entiteiten uit de
EDMX, de context uit de EDMX) waarna een WCF DataService een instantie van de EF context aanmaakt en aanroept.

Maar met wat geknutsel kunnen we die context ook zelf nabootsen en gelijk wat
pimpen 🙂

Dus laten we eerst eerst de standaard drie stappen doorlopen:

 Stap 1: Entiteiten beschrijven

[DataServiceKey("TweetIdentifier")]
public class Tweet
{
  public string TweetIdentifier { get; set; }
  public string Text { get; set; }
  public string FullName { get; set; }
  public DateTime Published { get; set; }
  public string UserName { get; set; }
}


Hier zien we een Tweet entiteit. Een goede entiteit moet voor OData een
uniek veld bezitten (als soort van primary key). Als er geen veld ID of
[class]Id heet, dan kan een bepaald veld via een DataServiceKey attribuut
toegewezen worden.

 Stap 2. Context
Omdat we geen context via een Entity Framework model laten genereren,
moeten we er zelf een schrijven. Hierbij worden we geholden door OData want die kijkt via reflection alleen  maar naar de aanwezige publieke properties die
IQueryable<> implementeren, en dat is hier een lijst van tweets dus.
Andere vereisten zijn er niet. Eventuele relaties worden dan ook vanzelf
uitgezocht, daar hoeven we niks extra’s voor te doen.

public class TwitterFeedContext
{
  public IQueryable<Tweet> Tweets
  {
    get
    {
      List<Tweet> _Tweets = LoadTweets("Atos Origin");
      return _Tweets.AsQueryable();
    }
  }

  private List<Tweet> LoadTweets(string tag)
  {
    var feed =
      SyndicationFeed.Load(XmlReader.Create(
          "htt p://search.twitter.com/search.atom?q=%23"
          + tag));

    var tweets = new List<Tweet>();
    foreach (SyndicationItem item in feed.Items)
    {
      string tweetLink = (item.Links.Count > 0)
        ? item.Links[0].Uri.OriginalString
        : string.Empty;
      string image = (item.Links.Count > 1)
        ? item.Links[1].Uri.OriginalString
        : string.Empty;
      string authorLink = (item.Authors.Count > 0)
        ? item.Authors[0].Uri
        : string.Empty;

      string text = String.Format(@"<a href='{0}'><img
        src='{1}' alt='' border='0' /></a><br />{2}<br /><a
        href='{3}'>Tweet</a>",
        authorLink,
        image,
        Regex.Replace(item.Title.Text,
          @"http(s)?://([\w+?\.\w+])+([a-zA-Z0-9\~\!\@\#\$\%\^\
                          &amp;\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?",
        "<a href='$&'>Click Here</a>")
        , tweetLink);

      string authorName = (item.Authors.Count > 0)
        ? item.Authors[0].Uri.Substring(19)
        : string.Empty;

      string authorFullname = (item.Authors.Count > 0)
        ? item.Authors[0].Name
        : string.Empty;

      tweets.Add(new Tweet
        {
          TweetIdentifier = item.Id,
          Text = text,
          FullName = authorFullname,
          Published = item.PublishDate.DateTime,
          UserName = authorName
        });
    }
    return tweets;
  }
}

Wat we hierboven dus zien is een simpele context class met slechts één
property die een lijst van Tweets teruggeeft. In de Getter wordt de methode
LoadTweets uitgevoerd. Hierin wordt heel handig twitter benaderd met een tag. De ontvangen tweets worden aan de lijst toegevoegd.

 Stap 3. wcf data service

En dan zijn we al weer bij de laatste stap. We moeten nog even een WCF
Data Service aanbieden. Deze template verwacht een context en we behoren ook aan te geven welke entiteiten beschikbaar worden voor de buitenwereld.

public class TwitterFeedODataService :
                        DataService<TwitterFeedContext>
{
  public static void InitializeService(DataServiceConfiguration config)
  {
    config.SetEntitySetAccessRule("Tweets", EntitySetRights.AllRead);

    // enforce serverside paging
    config.MaxResultsPerCollection = 200;

    config.DataServiceBehavior.MaxProtocolVersion =
      DataServiceProtocolVersion.V2;

  }
...

Hierboven geven we aan de service door dat we de TwitterFeedContext willen
ontsluiten en tevens geven we aan dat de ‘tabel’ Tweets alle leesrechten
vrijgeeft.

Als we bovenstaande uitvoeren dan zal de inhoud van http://localhost:1720/TwitterFeedODataService.svc/Tweets
als een blog zichtbaar worden (Eventueel moet nog even de feedreader in Internet Explorer aangezet worden).

Resultaat zonder actieve feedreader:

OData Ruw

Zie dat we één entry zichtbaar krijgen, maar dat de titel niet ingevuld is en
dat de context uit xml bestaat. Dus het resultaat met een actieve feedreader is
een beetje triest:

OData Ruw maar wel in een feedreader

Gelukkig kan met een eenvoudige extra stap een nette representatie
gemaakt worden.


Extra stap 4: Attributen voor een Friendly feed

We voegen op de entiteit enkele attributen toe om de samenstelling van de
feed om te buigen. Nu zal wel b.v. de titel ingevuld worden.

[EntityPropertyMapping("Text", SyndicationItemProperty.Summary,
 SyndicationTextContentKind.Html, false)]
[EntityPropertyMapping("FullName", SyndicationItemProperty.Title,
 SyndicationTextContentKind.Plaintext, false)]
[EntityPropertyMapping("Published", SyndicationItemProperty.Published,
SyndicationTextContentKind.Plaintext, false)]
[EntityPropertyMapping("UserName", SyndicationItemProperty.AuthorName,
 SyndicationTextContentKind.Plaintext, false)]
[DataServiceKey("TweetIdentifier")]
public class Tweet
{
  ...
}

Hierdoor wordt het resultaat alsnog netjes zichtbaar in een doorsnee
feedreader.

Mix10 resultaat met opmaak

Dit is ook precies wat Mike Flasko op de Mix gedemonstreerd heeft. Ik
heb wel de mapping met de attributen uitgebreid en eventuele URL’s in de tweets direct beschikbaar gemaakt via een Regex zoek-en-vervang.

Erg leuk…

Maar deze feed is helaas hard-coded. De naam van de tag is hier
meegecompileerd als ‘Atos Origin’. Deze kan natuurlijk ook via een config file
aangepast worden maar dan nog is deze te statisch. Het zou mooi zijn als iedere
gebruiker zijn eigen keuze kan maken.

Wat we dus willen is het dynamisch doorgeven van een  gewenste tag via de
url zelf. En als het even kan de bestaande url niet aantasten…

Dit kan en daarvoor is er een simpele laatste extra stap, we moeten een
Service Operation toevoegen.

Extra stap 5: tag dynamisch plaatsen via Service Operation
OData maakt het mogelijk om naast entiteiten ook methodes uit te voeren.
Dit wordt een FunctionImport genoemd.

Wat we doen is een extra methode op de WCF Data Service plaatsten welke
via een URL te benaderen is:

public class TwitterFeedODataService :
    DataService<TwitterFeedContext>
{
  public static void InitializeService(DataServiceConfiguration config)
  {
    ...
    config.SetServiceOperationAccessRule("Tweets", ServiceOperationRights.All);
  }

  [WebGet]
  public IQueryable<Tweet> Tweets(string tag)
  {
    lock (this)
    {
      TwitterFeedContext.Tag = tag;
      var tweets = from item in this.CurrentDataSource.Tweets
        select item;
      return tweets;
    }
  }
}

Deze Tweets methode wordt later beschikbaar als functie in de metadata van de
service.

...
<EntityContainer Name="TwitterFeedContext"
                 m:IsDefaultEntityContainer="true">
  ...
  <FunctionImport Name="Tweets"
                  EntitySet="Tweets"
   ReturnType="Collection(TwitterFeedODataWebApplication.Tweet)"
   m:HttpMethod="GET">
    <Parameter Name="tag" Type="Edm.String" Mode="In" />
  </FunctionImport>
</EntityContainer>
...

De FunctionImports zijn vooral bedoeld om stored procedures uit de database
direct via het Entity Framework te ontsluiten. Maar wij kunnen deze ook
gebruiken om een tag door te geven. Dus moeten we de in de methode opgegeven tag doorgeven aan de context:

public class TwitterFeedContext
{
  public static string Tag { get; set; }
  public IQueryable<Tweet> Tweets
  {
    get
    {
      if (string.IsNullOrWhiteSpace(Tag))
      {
        Tag = "AtoS";
      }

      List<Tweet> _Tweets = LoadTweets(Tag);
      return _Tweets.AsQueryable();        }
    }
    ...

Om de tag te kunnen blijven onthouden, is deze static gemaakt. En omdat het
plaatsen van de tag en het aanroepen van de twitter search uit twee regels
bestaat, heb ik er een lock tussengeplaatst.

Nu is de “Yet Another OData Twitterwall” klaar.

Een dynamische twitter rss feed

Zie hier, een prettig leesbare feed uit een manipuleerbare
twitter-zoekopdracht. Overigens heeft de ServiceOperation ‘toevallig’ dezelfde
naam als de entiteiten die opgeroepen worden. Dit maakt het transparant voor de gemiddelde gebruiker… Odata heeft hier geen enkel probleem mee, dus prima!

Uiteindelijk komt de code er dan zo uit te zien en is het opzetje van
Mike Flasko uitgewerkt tot een leuke demonstratie van OData welke ook nog prima toe te passen is in je eigen werk.

using System;using System.Collections.Generic;using System.Data.Services;
using System.Data.Services.Common;
using System.Linq;
using System.ServiceModel.Syndication;
using System.ServiceModel.Web;
using System.Text.RegularExpressions;
using System.Xml;
namespace TwitterFeedODataWebApplication
{
// 01 - Entity
[EntityPropertyMapping("Text", SyndicationItemProperty.Summary,
  SyndicationTextContentKind.Html, false)]
[EntityPropertyMapping("FullName", SyndicationItemProperty.Title,
 SyndicationTextContentKind.Plaintext, false)]
[EntityPropertyMapping("Published", SyndicationItemProperty.Published,
 SyndicationTextContentKind.Plaintext, false)]
[EntityPropertyMapping("UserName", SyndicationItemProperty.AuthorName,
 SyndicationTextContentKind.Plaintext, false)]
[DataServiceKey("TweetIdentifier")]
public class Tweet
{
  public string TweetIdentifier { get; set; }
  public string Text { get; set; }
  public string FullName { get; set; }
  public DateTime Published { get; set; }
  public string UserName { get; set; }
}
// 02 - Context
public class TwitterFeedContext
{
  public static string Tag { get; set; }
  public IQueryable<Tweet> Tweets
  {
    get
    {
      if (string.IsNullOrWhiteSpace(Tag))
      {
        Tag = "AtoS";
      }
      List<Tweet> _Tweets = LoadTweets(Tag);
      return _Tweets.AsQueryable();
    }
  }

  private List<Tweet> LoadTweets(string tag)
  {
    var feed =
      SyndicationFeed.Load(XmlReader.Create(
           "htt p://search.twitter.com/search.atom?q=%23"
           + tag));
    var tweets = new List<Tweet>();
    foreach (SyndicationItem item in feed.Items)
    {
      string tweetLink = (item.Links.Count > 0)
        ? item.Links[0].Uri.OriginalString
        : string.Empty;

      string image = (item.Links.Count > 1)
        ? item.Links[1].Uri.OriginalString
        : string.Empty;

      string authorLink = (item.Authors.Count > 0)
        ? item.Authors[0].Uri
        : string.Empty;

      string text = String.Format(@"<a href='{0}'><img
       src='{1}' alt='' border='0' /></a><br />{2}<br /><a
       href='{3}'>Tweet</a>",
        authorLink,
        image,
        Regex.Replace(item.Title.Text,
          @"http(s)?://([\w+?\.\w+])+([a-zA-Z0-9\~\!\@\#\$\%\^\
                        &amp;\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?",
        "<a href='$&'>Click Here</a>")
        , tweetLink);

      string authorName = (item.Authors.Count > 0)
        ? item.Authors[0].Uri.Substring(19)
        : string.Empty;

      string authorFullname = (item.Authors.Count > 0)
        ? item.Authors[0].Name
        : string.Empty;

      tweets.Add(new Tweet
         {
            TweetIdentifier = item.Id,
            Text = text,
            FullName = authorFullname,
            Published = item.PublishDate.DateTime,
            UserName = authorName});
      }

      return tweets;
    }
  }
  // 03 - Service

  public class TwitterFeedODataService : DataService<TwitterFeedContext>
  {
    public static void InitializeService(DataServiceConfiguration config)
    {
      config.SetEntitySetAccessRule("Tweets", EntitySetRights.AllRead);

      // enforce serverside paging
      config.MaxResultsPerCollection = 200;
      config.SetServiceOperationAccessRule("Tweets",
            ServiceOperationRights.All);
      config.DataServiceBehavior.MaxProtocolVersion =
            DataServiceProtocolVersion.V2;
    }
    [WebGet]
    public IQueryable<Tweet> Tweets(string tag)
    {
      lock (this)
      {
        TwitterFeedContext.Tag = tag;
        var tweets = from item in this.CurrentDataSource.Tweets
          select item;
        return tweets;
      }
    }
  }
}
Advertenties