Zugriffsreporting für Blogengine.net mit Logparser

Ich vertrete die These, das Suchmaschinen generierter Traffic im abnehmen ist. Also google muss sein Geschäftsmodell auf den Prüfstand stellen. Das tun sie auch, indem sie z.B. Werkzeuge wie Google Analyze zur Verfügung stellen. Damit übermittelt der Benutzer nicht nur Suchdaten, sondern auch Telemetrie Infos zur Nutzung von Websites. Auch andere Dienste wie Url Shortener sind Datenkraken, wie auch Scott Hanselman in seinem Blog anmerkt. Die Alternative einen eigenen Dienst mit Piwik aufzusetzen, hilft mehr Anonymität ins Web zu bringen. Falls der Benutzer im Browser dies unterbindet, bleibt nur noch der Blick in die Logfiles des Webservers. In unserem Fall des Internet Information Servers (kurz IIS), der von  Microsoft gratis mitgeliefert wird.

Für die Logfile Analyse hat Microsoft vor vielen Jahren den Logparser entwickelt. Ein sehr schneller kostenloser Parser. Im Stil von SQL Abfragen kann man durchaus forensische Analysen vornehmen.

Die Frage lautet konkret, wie viele User kommen durch eine Suchmaschine auf einen Blogeintrag, pro Monat.

In diesem Blog erkennt man einen Blogeintrag an der Sequenz /blog/ in der URl. Damit fallen alle direkten Aufrufe von blog.ppedv.de aus der Analyse heraus. In einem Google Ergebnisbild, wird ein suchender (ich liebe das Wort) auch dank der Darstellung vor allem auf den direkten Link clicken.

Stellt sich die  Frage, wie filtert man den Traffic der Spider heraus, Bisher hatten wir im Logparser die Namen der Bots in die Where Bedingung eingeschlossen.  Da man niemals weis, was man nicht weis, können auch unbekannte Spider agieren. Woran erkennt man also den Crawler einer Suchmaschine. Der sollte die Datei Robots.txt abrufen. Ein starkes Indiz für die Beweiskette,. Da die IP Adressen der Crawler endlich und in einem gewissen Maße auch fix sind kann man sich so eine Liste der Adressen anlegen.

   1:  logparser  "SELECT c-ip, cs(User-Agent) AS [UserAgent], COUNT(*) AS [summe]     
   2:  , MIN(TO_DATE(TO_LOCALTIME(TO_TIMESTAMP(date, time)))) AS FirstDate     
   3:  , MAX(TO_DATE(TO_LOCALTIME(TO_TIMESTAMP(date, time)))) AS LastDate 
   4:  INTO C:\LOGPARSER\LOG_RESULT\Robots.log 
   5:  FROM C:\inetpub\logs\LogFiles\W3SVC1\*.log 
   6:  WHERE cs-uri-stem = '/robots.txt' GROUP BY c-ip, UserAgent 
   7:  ORDER BY c-ip, UserAgent " -i:iisw3c -o:CSV

 

Im nächsten Schritt werden die Datensätze aus den Logfile kumuliert per Monat unter Ausschluss der IP Adressen der Suchmaschinen.

   1:  logparser  "select  TO_STRING(date, 'MMMM, yyyy') AS Month,count(c-ip) as blog
   2:   INTO C:\LOGPARSER\LOG_RESULT\zugriffe3d_blog.gif 
   3:  from c:\inetpub\logs\LogFiles\W3SVC1\*.log  
   4:  WHERE cs-uri-stem LIKE '%%/post/%%' 
   5:  and  c-ip not in (select TO_STRING(c-ip) 
   6:  from  C:\LOGPARSER\LOG_RESULT\Robots.log) 
   7:  group by Month " 
   8:  -o:chart -groupSize:1024x768 -i:iisw3c -chartType:ColumnStacked -view:off

 

Für alle interessierten die Zugriffszahlen als 2D Balkendiagramm.

image

Sortieren und Filtern in einer Liste mit Angular.js

Anhand von Praxis bezogenen Anwendungsfällen werden in meinen Angular Blogposts konkrete Funktionen aus der JavaScript Bibliothek demonstriert. In diesem Code Beispiel wird eine Liste, deren Inhalte aus einem ASP.NET Web Api Service stammen, sortiert und gefiltert. Die Daten kommen aus der Northwind SQL Server Datenbank. Am Ende wird eigentlich nur JSON übermittelt. Es geht auch ohne Service.

Ziel ist das ein Suchfeld für Filtern der Daten und die Sortierung durch Click auf den Header zu ändern.

image

Das Angular Module wird mit einem Controller versehen. Dieser enthält den Scope, der als Viewmodel dient und die Daten enthält. Hier die Liste der Kunden, aus der Rückgabe des REST Ajax calls.

   1:  angular.module('kunden2App', [])
   2:  .controller('CRUDController', function ($scope, $http) {
   3:      $scope.customers = [];
   4:      $http.get('api/Customers/').success(function (data) {
   5:          $scope.customers = data;
   6:      })
   7:      .error(function (data) {
   8:          $scope.error = "Fehler " + data.ExceptionMessage;
   9:      });
  10:  });

Am einfachsten lässt sich eine Suche implementieren.  Dem Viewmodel wird einfach per ng-model Direktive einen neue und gebundene Eigenschaft zugewiesen. Hier in Zeile 5 mit search bezeichnet. In ng-repeat wird über das Pipe Symbol getrennt nach der iterationsanweisung (Zeile 11) der Filter Ausdruck angefügt. In diesem Fall wird der von Angular vordefinierte Filter verwendet. Es lassen sich auch eigene Filter mit der Notation .filter in JavaScript erstellen. Die Dokumentation hat dazu ein nettes Beispiel, das Check Icons darstellt.

Andere vordefinierte Filter gib es z.B. für Formatierung von Datumswerten. Dieser wird dann direkt im Binding angegeben.

{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}

Auch die voreingestellte Sortierung wird über den Order By Filter definiert. Aber auch der Benutzer kann die Reihenfolge der Firmennamen ändern indem er auf den Header clickt. Hier werden die Direktiven ng-click und ng-source eingesetzt. Aus erstem wird ein Click Event, das den Wert von reverse toggelt. Dieser Wert wird zur Steuerung des Orderby Statements verwendet. Um ein Icon anzuzeigen wird die Image Source an ein Bild gebunden, das ich downfalse.gif und downtrue.gif genannt habe. Ein kleiner Trick mit geringem Code und großer Wirkung.

   1:   
   2:  <body ng-app="kunden2App">
   3:     <div ng-controller="CRUDController">
   4:         <h2>Customers</h2>
   5:         Name Suchen <input type="text" ng-model="search" />
   6:         <table >
   7:           <tr>
   8:             <th>#</th>
   9:             <th> <a href="" 
ng-click="reverse=!reverse;order('CompanyName', reverse)">Name
<img ng-src="img/down{{reverse}}.gif"></a></th>
  10:           </tr>
  11:           <tr 
ng-repeat="customer in customers | filter:search | orderBy:'CustomerID':reverse" >
  12:               <td>{{ customer.CustomerID }}</td>
  13:               <td>{{ customer.CompanyName }}</td>
  14:            </tr>
  15:         </table>
  16:     </div>
  17:  <script src="Scripts/angular.js"></script>
  18:  <script src="CRUDController.js"></script>
  19:  </body>
  20:   

Am Ende sehr überschaubarer Aufwand.

Paging mit Angular.js

Man hat eine Idee. Wenn grad niemand für eine Diskussion bereit ist, stürzt man sich auf Google (sorry bing) und recherchiert was andere so schreiben. Danach fühlt man sich mit jeder Minute dümmer. Breeze Framework hier, vierhundert Zeilen State of the Art Injection, Factory per Direktive JavaScript Source dort. Unzählige Github Projekte die nach Download nie laufen und dann noch ein paar Nuget Pakete an deren Aktualität man leichte Zweifel hat.

Meine Meinung zu Paging hat sich im Laufe der Zeit mit wachsenden Datenmengen stark gewandelt. Nummerische Paging hinterlässt durchaus einige Sinnfragen. Niemand weis, was sich auf Seite 3 befindet und überspringt Seite 2, niemand blättert bis ans Ende des Google Index. Ich halte ein Forward Only Paging mit wachsender Menge an gleichzeitig dargestellten Daten für die beste Lösung. Oder eine sehr gute Suche.

image

In diesem Projekt wird ein ASP.NET Web Api Service mit Odata Query Syntax seitenweise abgefragt. Schon vorweg: die Schönheit von LINQ sucht man vergeblich. Breeze soll da Abhilfe schaffen. Das ändert nichts daran das weite Teile von Angular mit Zeichenketten operieren, wie man das aus den Anfängen der SQL Sprache kennt. Nachteile unter anderem: keine Intellisense Unterstützung, keine Compilerfehler.

Basis ist ein ASP.NET Web API Odata Service, den ich in einem weiteren Blog Artikel beschrieben habe. Um Seitenweise Ergebnisse zu erhalten kommt folgende ODATA URI Query zum Einsatz

$top=10&$skip=0&$orderby=CustomerName&$inlinecount=allpages

Der Parameter $top liefert immer 10 Datensätze. Mit Skip werden die bereits geladenen übersprungen. In der Regel solle in Paging Szenarien sortiert werden, hier nach CustomerName. Besonderes Highlight ist der Parameter InlineCount der die Anzahl der gesamten Records zurück liefert.

Eine auf Angular basierende SPA Anwendung besteht aus einer HTML5 Datei und einem JavaScript Controller. Die HTML Datei enthält eine Referenz auf die Controller JS und das Angular Framework.

Mit dem Attribut ng-app wird Angular instanziiert. Der Controller wird per ng-controller Direktive zugewiesen.

   1:  <body ng-app="kundenApp">
   2:      <h1>SPA 1</h1>
   3:      <div ng-controller="pagingController">
   4:  ....
   5:   
   6:      <script src="Scripts/angular.js"></script>
   7:      <script src="pagingController.js"></script>
   8:  </body>

 

Ein Controller fungiert ähnlich einem ViewModel und hält den Datencontext ($scope) und die Commands (Events). Ein oder mehrere Controller werden in einem Modul zusammengefasst. Da keine anderen Module eingebunden werden, wird ein leeres Array als Parameter übergeben.

Die Daten werden im Kunden Objekt als Array gehalten. Die Methode LadeRecords wird die ersten 10 Datensätze laden.

   1:  angular.module('kundenApp', [])
   2:   .controller('pagingController', function ($scope, $http) {
   3:      $scope.kunden = [];
   4:      $scope.ladeRecords = function ()
   5:  ....

 

$http ist ein sogenannter Service, der in die Funktion per Parameter eingebunden wird. Per GET wird der REST Service aufgerufen. Die Parameter werden per Querystring übermittelt und als Array der Methode übergeben. Als Page Länge wird hier 10 Datensätze angenommen. Folglich braucht die Paging Logik eine Eigenschaft mit dem Namen Seite.

   1:       $http(
   2:       {
   3:           method: 'GET',
   4:           url: 'http://localhost:1929/odata/CustomersO',
   5:           params: {
   6:               $top: 10,
   7:               $skip: $scope.seite*10,
   8:               $orderby: 'CustomerID',
   9:               $inlinecount: 'allpages'
  10:           }

Das Ergebnis des Ajax Callbacks auf den Service enthält JSON mit einigen zusätzlichen Daten,  wie die Anzahl der Records.

{
  "odata.metadata":"http://localhost:1929/odata/$metadata#CustomersO","odata.count":"92","value":[
    {
      "CustomerID":"ALFKI","CompanyName":"Alfred's Futterkiste Pferd","ContactName":"Alfred","ContactTitle":"Sales Representative","Address":"Martin-Schmeisser-Weg 18","City":"Osnabr\u00fcck","Region":null,"PostalCode":"84489","Country":"Germany","Phone":"030-0074321","Fax":"030-0076545","LastChange":"AAAAAAAAv84="
    },{

Die Rückgabe der Daten werden dann ausgewertet und dem ViewModel, also Scope zugewiesen. Um die neuen Kunden an das bestehende Array anzuhängen wird in JavaScript die Push Methode verwendet., mit ein wenig eigenartiger Syntax (Zeile 2). Die Anzahl wird per odata.Count ausgelesen und letztendlich der Seitenzähler inkrementiert.

   1:   .success(function (data, status) {
   2:          $scope.kunden.push.apply($scope.kunden,data.value);
   3:          $scope.records = data['odata.count'];
   4:          $scope.seite++;

 

Zusammengeführt wird dies in der finalen HTML Seite. Solange keine Daten vorhanden sind, wird eine Wartemeldung angezeigt. Per ng-show Direktive und JavaScript wird diese Meldung automatisch ein, bzw. nachher wieder ausgeblendet.

Mit dem Attribut ng-repeat werden aus dem Viewmodell die Kunden in einer Art for-each durchlaufen. Die Bindungsyntax mit den Doppelgeschweiften Klammern erlaubt den Zugriff auf die Eigenschaften des Kunden Objektes. Fast so wie in WPF oder Silverlight.

Sehr nett ist die durchgeführte Berechnung der Restdatensätze aus den gebundenen Wert Records und der Länge der aktuellen Kundenliste im Browser.

Letztendlich wird noch ein HTML Button mit der Angular Direktive ng-click an eine Methode aus dem Viewmodell gebunden.

   1:  <body ng-app="kundenApp">
   2:      <h1>SPA 1</h1>
   3:      <div ng-controller="pagingController">
   4:          <div ng-show="kunden.length==0">
   5:              Daten werden geladen...
   6:          </div>
   7:          <div ng-repeat="kunde in kunden" >
   8:              {{kunde.CompanyName}}
   9:          </div>
  10:          <div>Noch {{records-kunden.length}}</div>
  11:          <button id="pageButton" ng-click="ladeRecords()"  >mehr..</button>
  12:      </div>
  13:      <script src="Scripts/angular.js"></script>
  14:      <script src="pagingController.js"></script>

 

Auch der JavaScript Code wird nun im pagingController zusammengefasst. Der Eigenschaft ladeRecords wird eine anonymen Methode zugewiesen. Der $http Ajax Service Call erhält eine Success und eine Error Behandlung.

   1:  angular.module('kundenApp', [])
   2:  .controller('pagingController', function ($scope, $http) {
   3:      $scope.kunden = [];
   4:      $scope.ladeRecords = function ()
   5:      {
   6:          $http( {
   7:           method: 'GET',
   8:           url: 'http://localhost:1929/odata/CustomersO',
   9:           params: {
  10:               $top: 10,
  11:               $skip: $scope.seite*10,
  12:               $orderby: 'CustomerID',
  13:               $inlinecount: 'allpages'
  14:           }
  15:       })
  16:      .success(function (data, status) {
  17:          $scope.kunden.push.apply($scope.kunden,data.value);
  18:          $scope.records = data['odata.count'];
  19:          $scope.seite++;
  20:      })  
  21:      .error(function (data, status) {
  22:          alert(data['odata.error'].message.value );
  23:      });    
  24:      };
  25:      $scope.seite = 0;
  26:      $scope.ladeRecords();
  27:  });

 

Wer mehr zu Angular.JS lernen möchte, kann dies auf der ADC X in einem von mir persönlich gehaltenen Workshop tun. Ein kompletter Tag speziell für den WPF, Silverlight Entwickler mit dem Ziel einen Schnelleinstieg in die Welt der SPA’s zu erhalten.

Scroll zum Ende der Seite mit Angular.js

Angular arbeitet ähnlich wie Silverlight mit einem ViewModel. Eine Liste kann so an einem Service gebunden werden, ohne das das UI blockiert wird. Die Daten kommen eben asynchron.

Im Beispiel sind die Daten länger als der Anzeigeplatz im Browser und man möchte automatisch ans Ende der Liste scrollen. Hier ergeben sich mehrere Detailprobleme?

  • Wann ist die Liste geladen und gerendert?
  • Wie komme ich aus dem Viewmodel in den View?

Durchaus enttäuschend ist die Antwort auf Teil 1. Man weis nicht, wann die Liste geladen und gerendert ist. Es gibt kein rendered oder loaded Event. Man kann allerdings mit einer Direktive ein HtML Element erweitern. Von der Funktion erschient mir das ähnlich eines Expression Blend Behaviours. Platt gesprochen erstellt man damit neue HTML Attribute. Angular nennt die Funktion Directive. Quasi alles aus Angular setzt auf dieses System (zb ng-repeat)

Teil 2 dreht sich darum wie scrollt man aus dem Viewmodel ohne in den HTML View JavaScript Code schreiben zu müssen. Dafür bietet Angular eine Methode anchorScroll, die mit einer vorgelagerten Methode Scope das Element wählt und dann den Browser Scroll ausführt.

Die Direktive wird ähnlich wie der Controller im App Modul erzeugt und beim erreichen des letzten Elements ausgeführt.

   1:  angular.module('kundenApp', [])
   2:    .directive('myRepeatFinished', function ($location,$anchorScroll) {
   3:        return function (scope, element, attrs) {
   4:             if (scope.$last) {
   5:                 $location.hash('pageButton');
   6:                 $anchorScroll();
   7:   
   8:            }
   9:        };
  10:    })

Der Name der Direktive sollte nicht mit ng beginnen um Konflikte mit dem Angular Namensraum zu vermeiden. Per Konvention wird bei jedem Wechsel von Klein auf Großbuchstaben ein Bindestrich eingefügt und der gesamte Namen in Lowercase gewandelt.

Im HTML5 Teil wird dann dieses neue HTML Attribut eingefügt und so das DIV um Funktion, CSS oder ein HTML Template erweitert.

   1:   <div ng-repeat="kunde in kunden" my-repeat-finished>
   2:              {{kunde.CompanyName}}
   3:   </div>

 

Dieses Beispiel zeigt zur Laufzeit für einen Sekundenbruchteil das JavaScript Objekt an

image

Dann erst die eigentlichen Werte

image

Vermutlich ist die Methode auf den letzten generierten Eintrag zu setzen einen Tick zu früh.

ASP.NET Web Api 2 und Odata 4

Was die Headline schon vermuten lässt, ganz schön kompliziert, dennoch löst Odata eine Reihe von Problemen im REST Web Service Umfeld. Der Open Data Standard stammt aus der Feder von Microsoft und erlaubt es auf einen Service verschiedene Abfragen durchzuführen. Anders als im “klassischen” REST Ansatz sind damit eine Vielzahl Variabler Querys möglich. Um eine Entität per Rest abzufragen folgt man in der Regel url/servicemethode/id. Mit Odata sieht die gleiche Abfrage so aus url/servicemethode[id]. Im ersten Fall muss allerdings eine spezielle Route und eine Methodenüberladung mit einem Parameter angelegt werden.

Odata ist sehr hilfreich in Sorting, Quering und Paging Szenarien, insofern lohnt sich ein Blick darauf. Leider hat Microsoft die Odata Implementierng im ASP.NET Web API Framework mehrfach und grundlegend geändert. So findet man in den Suchmaschinen meist Code der nicht funktioniert. Auch von mir stammt ein derartiger Blogartikel. Weitestgehend korrekt, aber in manchen Details nicht mehr aktuell.

Basis eines Web Api Service ist der Service Controller, der die notwendigen CRUD Operationen bereit stellt. In VB.NET werden diese Methoden mit dem Präfixen Get usw in Kombination mit dem Controllernamen benannt. Dies ist die Konvention. Mit dem neuen Attribut Routing, kann man in der Namensgebung auch kreativer sein. Sollte man aber ohne Not nicht.

Ganz wesentlich: ein API Controller und ein Odata Controller sind zwei getrennte Welten, sprich Klassen. Die eine erbt von API Controller, die andere von OdataController.

Bevor man sich lange abmüht, empfehle ich per Scaffolding (Gerüstelement) den Odata Controller anzulegen.

image

Dabei wird eine vorhandene Modelklasse ausgewählt. In diesem Projekt wurde diese per Entity Framework aus der Northwind Datenbank generiert.

image

Falls dies ein jungfräuliches Web Projekt ist, wird keine Datenkontextklasse vorhanden sein. Lassen Sie sich bequem generieren indem sie auf das + clicken.

Die Controllerklasse enthält dann u.a. folgenden .NET Code

   1:   Public Class CustomersOController
   2:          Inherits ODataController
   3:   
   4:          Private db As New NorthwindEntities
   5:   
   6:          ' GET: odata/CustomersO
   7:          <EnableQuery>
   8:          Function GetCustomersO() As IQueryable(Of Customers)
   9:              Return db.Customers
  10:          End Function

 

Um den Controller per HTTP auch aufrufen zu können, muss eine Route konfiguriert werden. In den guten alten WCF Zeiten hätte man das in eine Config Datei geschrieben. Heute werden konfigurationen in Code getippt. Im globa.asax Startup Event wird eine Methode RegisterRoutes aufgerufen, die auf eine Klasse in RouteConfig.VB verweist.

Der Code ab Zeile 17 wurde von mir eingefügt. Wirklich intuitiv finden ich den Code nicht. Die Route lauter nun odata und es ist nur der Controller CustomersO eingebunden.

   1:  Public Module WebApiConfig
   2:      Public Sub Register(config As HttpConfiguration)
   3:          ' Web-API-Konfiguration und -Dienste
   4:          ' Web-API für die ausschließliche Verwendung von 
Trägertokenauthentifizierung konfigurieren.
   5:          config.SuppressDefaultHostAuthentication()
   6:          config.Filters.Add(New HostAuthenticationFilter
(OAuthDefaults.AuthenticationType))
   7:   
   8:          ' Web-API-Routen
   9:          config.MapHttpAttributeRoutes()
  10:   
  11:          config.Routes.MapHttpRoute(
  12:              name:="DefaultApi",
  13:              routeTemplate:="api/{controller}/{id}",
  14:              defaults:=New With {.id = RouteParameter.Optional}
  15:          )
  16:   
  17:          Dim builder As ODataModelBuilder = New ODataConventionModelBuilder()
  18:          builder.EntitySet(Of Customers)("CustomersO")
  19:          '       config.Routes.MapODataRoute("ODataRoute", Nothing, builder.GetEdmModel) Obsoloet
  20:   
  21:          config.Routes.MapODataServiceRoute("odata", "odata", builder.GetEdmModel)
  22:          config.EnsureInitialized()
  23:      End Sub

 

Es gibt nun einen Pfad api/ und einen Odata/. Wenn man die ersten 5 Kunden abrufen will, schränkt man dies wie folgt in der URL ein http://localhost:1929/odata/CustomersO?$top=5. Als Ergebnis erhält man JSON Daten

{
  "odata.metadata":"http://localhost:1929/odata/$metadata#CustomersO","value":[
    {
      "CustomerID":"ppedv","CompanyName":"ppedv","ContactName":null,"ContactTitle":null,"Address":null,"City":null,"
Region":null,"PostalCode":null,"Country":null,"Phone":null,"Fax":null,"LastChange":"AAAAAAAAz3M="
    },{

IIS Express und FQDN

Mein aktueller Anwendungsfall einer ASP.NET Web-Anwendung benötigt statt dem üblichen localhost eine echte Domain als Namen. Wer aus Visual Studio eine Website startet, tut dies in der Regel mit IIS Express (früher Cassini Web Develeopment Server).

Dort wird ein zufälliger Port ausgewählt und in die universelle Konfiguration des IIS Express Web Servers in applicationhost.config eingetragen.

image

Den Speicherort der Datei findet man im Screenshot  des IIS Express (Task Bar – Notification Icons- Item- Rechtsklick). Um ein zweites Mapping einzutragen, wird diese Config Datei direkt per Notepad geöffnet. In den Bindings wird eine oder mehrere Bindungen aktiviert.

   1:    <bindings>
   2:  <binding protocol="http" bindingInformation="*:5376:localhost" />
   3:   <binding protocol="http" bindingInformation="*:80:ppedv.localtest.me" />
   4:  </bindings>

Die Adresse localtest.me zeigt immer auf die lokale localhost Adresse 127.0.0.1. und ist damit ein Trick eine echte Internet-Domain zu nutzen, ohne die Hosts-Datei oder schlimmeres zu beanspruchen. Port 80 ist nicht unbedingt erforderlich.

Wenn ein lokaler IIS installiert ist, fängt dieser per Universal Binding alle Domains auf der IP 127.0.0.1 ab. Auch ein Stoppen des www-Publishingdienst löst das Problem nicht. Erst wenn man einen Hostname im IIS Manager vorgibt, ist es anderen Websites möglich ein Binding durchzuführen.

image

Stolperstein #2 ist, dass Binden abseits von localhost auf IIS Express nur möglich ist, wenn IIS Express im Administrator Context läuft. Dazu muss Visual Studio als Administrator (rechte Maustaste runas Administrator) ausgeführt werden.

Im Ergebnis lauscht nun die Web App auf beiden URI

image

Facebook-Login in ASP.NET Webforms

Die Welt bleibt nicht stehen. Das Nutzungsverhalten von Websites ändert sich. Das durchaus bewährte ASP.NET Membership Provider System wird durch ASP.NET Identity abgelöst. Wer heute mit Visual Studio 2013 ein neues Web Projekt anlegt, findet die komplette Benutzerverwaltung voreingerichtet - basierend auf Microsoft.AspNet.Identity.

Das Modul wird in der Datei IdentityConfig aus dem Verzeichnis App_Start hochgefahren. Dort kann man auch die unsäglich strikte Passwort-Policy aufweichen.

   1:     manager.PasswordValidator = New PasswordValidator() With {
   2:            .RequiredLength = 1,
   3:            .RequireNonLetterOrDigit = False,
   4:            .RequireDigit = False,
   5:            .RequireLowercase = False,
   6:            .RequireUppercase = False
   7:          }

 

Die Struktur bzw. die Eigenschaften des Benutzers werden in der Klasse ApplicationUser der Datei IdentityModels vorgegeben. Entsprechend des Code First Paradigmas des Entity Frameworks und der Einstellung aus der Web.Config wird dann die Datenbank automatisch angelegt.

   1:  <connectionStrings>
   2:      <add name="DefaultConnection" 
connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=
|DataDirectory|\aspnet-WebFormsIdentity-20140712075341.mdf;
Initial Catalog=aspnet-WebFormsIdentity-20140712075341;Integrated Security=True"
   3:        providerName="System.Data.SqlClient" />
   4:    </connectionStrings>

 

In der Datei StartUp.Auth.vb sind Methoden für Twitter, Google und Facebook vorkonfiguriert, aber auskommentiert.

   1:   app.UseFacebookAuthentication(
   2:   appId:=ConfigurationManager.AppSettings("FacebookId"),
   3:   appSecret:=ConfigurationManager.AppSettings("FacebookSecret"))

Die Authentication Methode erwartet zwei Parameter, die ID und das Secret. Anders als eine Benutzername-Passwort-Kombination ist diese sozusagen das Login einer Anwendung. Der Benutzer sieht diese Daten nicht und entsprechend macht es Sinn, diese in die Web.Config auch aus dem Code auszulagern.

Im nächsten Schritt muss der Entwickler eine App bei Facebook anlegen, um die Parameter ID und Secret zu generieren. Es wird also ein Facebook-Account benötigt. Einstiegspunkt ist das FB Developer Portal.

Nachdem die App per New App erstellt wurde, können die beiden Parameter aus dem Formular kopiert werden.

image

Wer jetzt loslegt, wird eine Fehlermeldung im Browser erhalten.

Die Anwendungseinstellungen lassen die angegebene URL nicht zu: Eine oder mehrere URLs sind in den Einstellungen der App nicht zugelassen. Sie müssen mit der Website-URL oder der Canvas-URL übereinstimmen, oder die Domain muss Subdomain einer der App-Domains sein.

Dazu muss man wissen, dass eine sogenannte 2-Legs-Authentifzierung auf Basis von OAuth2 zum Einsatz kommt. Im Kern übernimmt ein Provider – hier Facebook – die Anmeldung und reicht dann einen Zugangstoken an die aufrufende Website weiter. Dazu ist es aber nötig, Facebook die URL der aufrufenden Website mitzuteilen.

Die Einstellung wird im Developer Portal von Facebook in den Settings der App vorgenommen.

image

Die hier gewählte lokale URL ist natürlich nicht praxistauglich.

Der Login Dialog der ASP.NET Website erzeugt nun einen zusätzlichen Button für den Facebook-Login auf der rechten Seite.

image

Folgerichtig wird das Passwort in der Tabelle ASPNetUsers auch frei gelassen. Nur ein lokaler Benutzer kann auch ein Passwort besitzen, das per Hash verschlüsselt gespeichert wird.

image

Den zweitbesten per TSQL finden

Das passt ja fast zur Fussball-Weltmeisterschaft. Ein Teilnehmer einer ppedv Schulung schreibt mir:

“Ich habe da eine „knifflige“ Aufgabe in der Firma, wo ich mit meinem bescheidenen SQL-Wissen nach ein paar Stunden nicht mehr weiter komme. Ich kann's natürlich über Umwege lösen, glaube aber dass man das mit einem sql query oder sql query + stored procedure oder function auch zusammenbringt.”

Kurz zusammengefasst: eine SQL Abfrage im Microsoft SQL Server soll den zweiten Wert liefern. Die Frage war mir neu und mein SQL-Wissen auch ein wenig eingerostet - teilweise war es auf dem Stand von SQL 2000. Seit 2005 ist aber RowNum hinzugekommen und seit 2012 auch ein Offset-Kommando, um in Kombination mit z.B. TOP /Group By einfach einen Datensatz zu überspringen. So zunächst mein erster Gedanke.

Die Ausgangstabelle

   1:  CREATE TABLE [dbo].[KickiTest](
   2:      [ID] [int] IDENTITY(1,1) NOT NULL,
   3:      [Ergebnis] [bit] NULL,
   4:      [Datum] [datetime] NULL,
   5:      [Seriennummer] [int] NULL
   6:  ) ON [PRIMARY]

Die Daten

image

Abfrage: Für alle Geräte, deren Ergebnis positiv ist, der zweite Datensatz. Wenn dieser nicht vorhanden ist, wird der erste abgefragt. 

Die Lösung verwendet die generierte Zeilennummer in Kombination mit einem Zähler

   1:  WITH tmp  AS (SELECT ID, Ergebnis, Datum, Seriennummer,
   2:         anzahl= Count(seriennummer) OVER(PARTITION BY seriennummer) ,
   3:          ROW_NUMBER() OVER (PARTITION BY seriennummer 
ORDER BY seriennummer,datum) AS RowNum
   4:          FROM   KickiTest where ergebnis=1) 
   5:   
   6:   
   7:  SELECT ID, Ergebnis, Datum, Seriennummer,rownum,anzahl
   8:  from tmp
   9:  where (anzahl = 1) or (anzahl>1 and rownum=2) 

 

Freie Bilder ganz schön teuer

Nachdem ich gestern einen Blog-Eintrag gelesen habe, der sich mit der Verwendung von Gratis-Fotos auf Webseiten beschäftigt, kann ich nur raten: „Finger weg“.

Die ppedv AG hat seit Anfang 2011 zahlreiche Prozesse laufen, die sich u.a. im Bereich Wettbewerbsrecht abspielen. Dabei geht es pro Klage durchaus auch um hohe sechsstellige Beträge. Beispielhaft sei genannt: die strittige Verwendung von fünf Sternen als Indikator besonderer Qualität. Zeitgleich haben wir regelmäßig auch Verfahren mit eigentlich unbekannten Dritten aufgrund anonymer Hinweise, z.B. mahnte uns die Wettbewerbszentrale München wegen des Schulungskatalogs ab mit nachfolgendem Schlichtungsverfahren. Aktuell verdanken wir genauso einem Tipp Post von Rechtsanwalt Schlösser, der den Fotografen Kleinschmidt vertritt. Konkret haben wir auf einer sehr tiefliegenden Unterseite einen Papagei als etwas größeres Thumbnail abgebildet und damit die Rechte eines Künstlers verletzt.

Aus SEO-Gründen wurde das Bild umbenannt und aus Optimierungsgründen die EXIF-Informationen entfernt. Am Ende hatte es 7kb und war 150 x100 Pixel groß. Das Bild wurde vor über sieben Jahren in einem Foto-Portal erworben. Der Mitarbeiter, den es heute nicht mehr gibt, hat per Kreditkarte ein Credits Paket erworben und daraus unter anderem dieses Bild verwendet. Ohne entsprechendes Hintergrundwissen ist dieses Bild somit für niemanden auffindbar bzw. dem Fotografen zuzuordnen.

Die damaligen Lizenzbedingungen des Portals forderten die Nennung des Portals im Impressum als Quelle. Alles im grünen Bereich und wir haben das soweit auch dokumentiert.

Faktisch erwirbt man mit der Lizenz bestenfalls ein beschränktes Nutzungsrecht. Darüber hinaus hat der Fotograf das Recht als Urheber direkt beim Bild genannt zu werden. Das haben wir, alleine aus optischen Gründen, damals nicht getan und auch rechtlich nicht für nötig gehalten.

Der Anwalt möchte nun rund 1600€ im Vergleichsverfahren. 800 für den Fotograf und 800 für seinen Aufwand. Nicht ohne umfangreich zu erklären, wie schlecht es für uns aussieht. In der Regel gehen solche Verfahren auch negativ für den Beklagten aus. Der Kläger sucht sich ein für seine Ziele in der Rechtsprechung einschlägig bekanntes Gericht aus. Da das Foto im Internet abrufbar ist, ist der Tatort unbestimmt; dies ist bekannt als fliegender Gerichtsstand. Als konkretes Beispiel: Ein Mitglied aus der Community wird im Süden (Traunstein) verklagt, obwohl der Kläger aus dem Osten ist und der Beklagte aus dem Westen. Auch wir sind schon vom identen Gegner in Leipzig, Dresden, Frankfurt, Traunstein, München und Düsseldorf verklagt worden.

Das besonders perfide dabei ist, dass man sich davor nicht schützen kann. Selbst wer ein Foto kauft, kann nie sicher sein, dass es vom genannten Urheber stammt. Der muss nur mit seiner Digitalkamera in den Gerichtssaal spazieren und das Original vorzeigen. Man munkelt, dass es einige Fotografen und Anwälte gibt, die ihr Geschäftsmodell ausschließlich darauf aufbauen. Darüber hinaus gelten hier Aufbewahrung und Nachweisfristen, die sich am Urheberrecht orientieren. In unserem Fall betreiben wir über 60.000 einzelne Seiten, von denen wir nicht wissen, wer jede einzelne erstellt hat. Selbst wenn wir es wüssten und eine Agentur das durchgeführt hätte, befreit das nicht im Geringsten von den Schadensersatzansprüchen.

Mein Tipp: Finger weg von Fotoportalen, egal ob kostenlos oder bezahlt. Free kann ganz schön teuer werden. Am besten nur eigene Fotos verwenden oder einen Fotograf vor Ort beauftragen. Wer weitere Fragen dazu hat, kann mir gerne eine E-Mail senden.

ASP.NET Webforms Scaffolding

Code-Generatoren sehe ich sehr kritisch. Speziell dann, wenn es es sich um einen One-Way-Vorgang handelt. Genau dies bietet ASP.NET Scaffolding für MVC und seit kurzem auch für Webforms. Da dieser Wizard aber doch ein wenig Arbeit sparen kann, werfen wir einen Blick darauf. Aktuell wird nur C# unterstützt. VB.NET ist in Vorbereitung.

Der Webforms Scaffold Generator findet sich auf Github und ist Open Source. Er wird als Erweiterung in Visual Studio 2013 unter dem Namen Web Forms Scaffolding installiert. Dem Artikel liegt die Version 0.1 Beta 2 zugrunde (muss dabei leicht schmunzeln).

Als nächstes erzeugt man ein Model im Visual Studio Web Projekt. Dies kann per Entity Framework aus einer SQL-Datenbank geschehen oder einfach im Stile von Code First durch eine Klasse. Einzig zu beachten ist, dass dies nicht im Root-Verzeichnis, sondern am Besten im Model gespeichert wird. Es kommt sonst zu einem Namespace-Konflikt, da der Generator Model und View mit identem Namen wählt.

Per Dataannotations werden im Model Validierungsregeln deklariert.

   1:  namespace scaff7.Models
   2:  {
   3:      public class person
   4:      {
   5:          [Key]
   6:          public int id { get; set; }
   7:          [Required(ErrorMessage = "Muss was rein")]
   8:          [MaxLength(10, ErrorMessage = "zu lange")]
   9:          public String Name { get; set; }
  10:          [Display(Description = "geburtstag")]
  11:          public DateTime gebDat { get; set; }
  12:      }
  13:  }

Als nächstes wird per Rechtsclick ein Item vom Typ Scaffold hinzugefügt, zu Deutsch “neues Gerüstelement”.

image

Die Vorlage heißt “Webforms Pages using Entity Framework”.  Im kommenden Dialog werden Klasse und der vorhandene DBContext ausgewählt:

image

Mehr ist es nicht. Im Visual Studio Projekt Explorer erscheint ein Unterverzeichnis mit dem Namen der Klasse und einer Datei Default für Listendarstellung und eine Datei Edit für Anzeige und Bearbeiten und eine für den Insert.

image

Dahinter werden DynamicData Templates verwendet, die man nachträglich auch anpassen kann, um seine Designwünsche zu erfüllen. Selbst die Datenbank wird beim ersten Aufruf automatisch erstellt.

Das Layout basiert auf Bootstrap 3.

image

Sehr schick ist, dass auch bereits die Validierung berücksichtigt und visualisiert wird. Dazu werden die Bootstrap CSS-Klassen verwendet wie has-errors, ohne dass man das explizit berücksichtigen muss.

image

Der erzeugte und verwendete HTML5-Code aus den Web Server Controls ist einfach zu verstehen und zu verändern. Natürlich kommt das moderne Model Binding zum Einsatz. Man darf sich nicht allzu viel erwarten, aber für einfache Datenbank Admin Frontends ist Scaffolding eine sehr effiziente Methode. Webforms sind eben noch lange nicht tot.

Wissen aufbauen und Microsoft Surface Pro 3 kassieren

Month List