Archive for the 'ASP.NET' Category

#ASP.NET: Back!

Ja, manchmal geschehen noch Zeichen und Wunder. Eines dieser Zeichen ist die Rückkehr von ASP.extra, meiner vor mehreren Jahren eingestellten Seite rund um ASP, ASP.NET und verwandte Web-Technologien. Eingestellt worden ist sie damals nicht etwa wegen mangelnder Resonanz, sondern ganz im Gegenteil: Wegen zu hoher Resonanz.

Es wurde damals notwendig, die alte Webseite grundlegend zu überarbeiten und neu zu gestalten – was jedoch aus diversen privaten und zeitlichen Gründen unterblieb. Da dieses Blog sich jedoch in den letzten Monaten mehr in Richtung Politik orientiert hat, bin ich in mich gegangen und habe beschlossen, ASP.extra wieder auferstehen zu lassen. Damit gibt es eine klare Trennung: karsan.de für persönliche und politische Dinge, ASP.extra für technologische Sachen.

Optisch hat sich für die alten ASP.extra-Benutzer nicht soooo viel getan (gut, die Navi und der ganze Kram sind jetzt auf der anderen Seite), aber technisch ist die Plattform WordPress statt eines proprietären Systems. Die Vorteile liegen auf der Hand: Die Webseite ist standardkonform, der Weiterentwicklung sind somit keine Grenzen gesetzt und wenn ich ein nettes Plugin finde, baue ich es auch ein.

Momentan fehlt es noch an einem kleinen Detail auf www.aspextra.de – den Inhalten. Die kommen aber im Laufe der Zeit, versprochen. Bookmarks können gesetzt werden. ;-)

#ASP.NET-Snippet: Textdatei nicht ständig neu laden müssen

Die Aufgabenstellung: Es soll der Inhalt einer Textdatei in ein Literal eingebunden werden, um auf diese Art einen einfachen Newsticker zu realisieren.

Das Problem: Der Inhalt wird bei jedem Request neu geladen. Deshalb gibt es potentiell Performance-Probleme.

Die Lösung: Den ASP.NET-Cache verwenden. Hier können die Daten für einen gewissen Zeitraum zwischengespeichert werden und müssen somit nicht bei jedem Request neu geladen werden.

Die Umsetzung:

lock(GetType())
{
   // News aus dem Cache holen
   String content === Cache["news"] as String;
 
   // News gefunden?
   if(null == content)
   {
      // News waren nicht im Cache, also nachladen
      content = File.ReadAllText();
 
      // Unter dem Schlüssel "news" für zehn Minuten in den
      // Cache packen, danach fliegen sie automatisch wieder raus
      Cache.Insert(
         "news", content, null,
          DateTime.Now.AddMinutes(10), TimeSpan.Zero);
   }
 
   // News ausgeben
   .Text = content;
}

Wenn es sein muss, kann zusätzlich noch eine CacheDependency verwendet werden, um das Objekt sofort aus dem Cache zu nehmen, wenn sich die Datei geändert hat.

DOTNET: Weihnachten, schon jetzt

Die Firma DevExpress, vielen sicherlich ein  Begriff aufgrund der Refactor!-PlugIns fürs Visual Studio (VB und ASP.NET), macht allen .NET-Entwicklern ein hübsches, kleines Weihnachtsgeschenk: 60 Controls (WindowsForms und WebForms) für umme.

Was man tun muss? Nix anderes, als sich unter http://www.devexpress.com/60 zu registrieren und anschließend im Download-Center die Komponenten herunter zu laden. Bei der Installation kann man natürlich die Trial-Versionen angewählt lassen, muss es aber nicht. ;-)

Also, frohes Fest!

ASP.NET: Schichtentrennung – Implementierung

Das Schicke an der Schichtentrennung und den damit verbundenen Konzepten, ist, dass die Implementierung später gegen eine andere Implementierung ausgetauscht werden kann. Genau diesen Ansatz macht man sich zu Nutze, wenn man schnell Ergebnisse erzielen möchte, indem man zunächst eine einfache Implementierung erstellt und eben erst später die echte, große, datenbankgestützte Version bereit stellt.

Das vorausgeschickt, ist die folgende Implementierung nur als ein erster Ansatz zu sehen, den Sie durch eine Version ersetzen können, der Ihren eigenen Anforderungen besser entspricht. Die hier vorgestellte Implementierung nutzt nur den Speicher, um die angelegten Kunden vorzuhalten. Technisch macht sie nix anderes, als eine Liste von Kunden im Speicher zu halten. Einfach, aber für einen Test und einen ersten Prototypen sicherlich ausreichen.

Wichtig: Diese Komponente liegt in einem eigenen Projekt mit dem Namen MemoryCustomerManager (genau so wird der Name der Assemblierung heißen).

Der Code ist ziemlich selbsterklärend – hier werden schließlich nur Operationen auf einer Liste von Kunden vorgenommen. Damit alles später zusammen funktioniert, muss diese Implementierung somit lediglich von der Basisklasse CustomerManager erben und die benötigten Methoden implementieren:

using System;
using System.Collections.Generic;
using System.Text;
using BusinessLayer;

namespace MemoryCustomerManager
{
   ///

   /// Implementation of the business layer
   ///

   public class MemoryCustomerManager : CustomerManager
   {

      private static List _customers =
         new List
();

      ///

      /// List of customers
      ///

           
      private static List CustomersList
      {
         get { return _customers; }
      }  

      ///

      /// Returns all customers
      ///

      public override List GetAllCustomers()
      {
         // Sort
         Sort(CustomersList);

         // Return the customers
         return CustomersList;
      }

      ///

      /// Returns a specific customer
      ///

      public override Customer GetCustomer(Guid id)
      {
         // Check all customers
         foreach (Customer cust in CustomersList)
         {
            // Compare the id
            if (cust.Id.Equals(id))
            {
               // Found it!
               return cust;
            }
         }

         // Found nothing
         return null;
      }

      ///

      /// Finds all customers by their names
      ///

      public override List
         FindCustomersByName(string name)
      {
         List
customers =
            new List
();
         string nameLower = name.ToLower();

         // Check every customer
         foreach (Customer cust in CustomersList)
         {
            if (cust.LastName.ToLower()
               .Equals(nameLower))
            {
               customers.Add(cust);
            }
         }

         // Sort the customers
         Sort(customers);

         // Done
         return customers;
      }

      ///

      /// Finds all customers by their email-addresses
      ///

      public override List
         FindCustomersByEMail(string email)
      {
         List
customers =
            new List
();
         string emailLower = email.ToLower();

         // Check every customer
         foreach (Customer cust in CustomersList)
         {
            if (cust.EMail.ToLower().Equals(emailLower))
            {
               customers.Add(cust);
            }
         }

         // Sort the list
         Sort(customers);

         // Return the customers
         return customers;
      }

      ///

      /// Updates a customer
      ///

      public override Customer
         UpdateCustomer(Customer customer)
      {
         // Delete an existing customer
         DeleteCustomer(customer);

         // Add the customer
         CustomersList.Add(customer);

         return customer;
      }

      ///

      /// Deletes a customer
      ///

      public override bool
         DeleteCustomer(Customer customer)
      {
         // Check, whether the customer
         // exists in the list
         Customer existing = null;
         foreach (Customer cust in CustomersList)
         {
            if (cust.Id.Equals(customer.Id))
            {
               existing = cust;
               break;
            }
         }

         // Replace the old customer
         if (null != existing)
         {
            CustomersList.Remove(existing);
            return true;
         }

         return false;
      }

      ///

      /// Sort the customers
      ///

      private void Sort(List customers)
      {
         customers.Sort(new CustomerSorter());
      }
   }
}

Innerhalb der Klasse wird Bezug auf eine Klasse CustomerSorter genommen. Diese hat die Aufgabe, die Liste der Kunden stets alphabetisch sortiert zu halten. Wir implementieren dies mit Hilfe des Interfaces IComparer. Dabei ist lediglich die Methode Compare() zu überschreiben – und diese Überschreibung ist ziemlich simpel, denn tatsächlich geschieht nix anderes, als die Nachnamen der Kunden über deren Funktionalitäten miteinander zu vergleichen und das numerische Ergebnis dieses Vergleiches zurück zu geben. Dabei kann es zu folgenden Rückgaben kommen:

  • Zahl größer als 0: Der Wert der Instanz, auf der verglichen worden ist, ist größer.
  • 0: Beide Werte sind gleich.
  • Zahl kleiner als 0: Der Wert der Instanz, mit der verglichen worden ist, ist größer.

Dementsprechend kann das natürlich auch umgedreht werden, wenn man am Ende des Tages eine absteigende Sortierung wünscht. Wenn die Nachnamen gleich sind, werden die Vornamen miteinander verglichen. Sind auch die gleich, kommen die E-Mail-Adressen an die Reihe.

Lange Rede, kurzer Sinn: Dies ist der Code der CustomerSorter-Klasse:

using System;
using System.Collections.Generic;
using System.Text;
using BusinessLayer;

namespace MemoryCustomerManager
{
   public class CustomerSorter : IComparer
   {
      ///

      /// Compares two customers
      ///

      public int Compare(Customer x, Customer y)
      {
         int result = x.LastName.CompareTo(y.LastName);
        
         // Compare the first names when neccessary
         if (result == 0)
         {
            result = x.FirstName.CompareTo(y.FirstName);
         }

         // Compare the emails when neccessary
         if (result == 0)
         {
            result = x.EMail.CompareTo(y.EMail);
         }

         return result;
      }
   }
}

Damit ist die Implementierung komplett. Im nächsten Teil unserer Serie widmen wir uns dann der Nutzung dieser ganzen Komponenten im Web-Umfeld.

Teil 1. Teil 2. Teil 3.

ASP.NET: Schichtentrennung – Fassade

Schichtentrennung lebt vom Verbergen der Implementierung und davon, möglichst wenig an Informationen über das Erzeugen von Instanzen oder Abhängigkeiten nach draußen gelangen zu lassen. Das Grundprinzip sollte stets sein, so wenig wie möglich fest miteinander zu koppeln.

Aus diesem Grund bedienen wir uns einer Fassadenklasse, die das Instanzieren und Benutzen der Implementierung unserer Basisklasse verbirgt. Somit haben wir eine dedizierte Abgrenzung zur Frontendschicht geschaffen, was es uns im weiteren Verlauf des Lebenszyklus eines Projektes erleichtern würde, auch weitreichendere Änderungen zu implementieren (und sei es, die komplette Basisklasse gegen irgend eine andere Implementierung auszutauschen). Also, Fassade davor, und schon kann man auch mal was ändern, ohne das es weh tun muss.

So sieht die Fassadenklasse aus:

using System;
using System.Collections.Generic;
using System.Text;

using de.ksamaschke.Tools;


namespace BusinessLayer
{
   ///

   /// API for handling customers
   ///

   public class Customers
   {

      private static CustomerManager _manager;

      ///

      /// Reference to the
      /// CustomerManager-implementation
      ///

           
      private static CustomerManager Manager
      {
         get
         {
            // Get the manager
            if (null == _manager)
            {
               try
               {
                  _manager = ManagerLoader
                     .Load();
               }
               catch
               {
                  throw;
               }
            }

            return _manager;
         }
      }

      ///

      /// Returns a list of all customers
      ///

      public static List GetAllCustomers()
      {
         return Manager.GetAllCustomers();
      }

      ///

      /// Returns a specific customer
      ///

      public static Customer GetCustomer(Guid id)
      {
         return Manager.GetCustomer(id);
      }

      ///

      /// Returns a list of customers
      /// identified by their names
      ///

      public static List
         FindCustomersByName(string name)
      {
         return Manager.FindCustomersByName(name);
      }

      ///

      /// Returns a list of customers identified
      /// by their email-addresses
      ///

      public static List
         FindCustomersByEMail(string email)
      {
         return Manager.FindCustomersByEMail(email);
      }

      ///

      /// Updates a customer
      ///

      public static Customer
         UpdateCustomer(Customer customer)
      {
         return Manager.UpdateCustomer(customer);
      }

      ///

      /// Deletes a customer
      ///

      public static bool
         DeleteCustomer(Customer customer)
      {
         return Manager.DeleteCustomer(customer);
      }
   }
}

Diese Fassade ist absichtlich sehr übersichtlich gehalten. Im Wesentlichen stellt sie nur die gleichen Funktionalitäten wie die intern verwendete CustomerManager-Klasse dar. Die konkret verwendete Instanz wird über die Eigenschaft Manager abgerufen, wo sie in Form eines Singletons gehalten wird – somit gibt es nur eine Instanz und die Instanz muss nicht bei jedem Zugriff neu erzeugt werden.

Im nächsten Teil widmen wir uns der konkreten Implementierung einer CustomerManager-Ableitung.

Hier geht es zu Teil 1 und Teil 2.

ASP.NET: Schichtentrennung – Basisklasse und Überlegungen

Bevor wir nun tatsächlich auf das Thema Schichtentrennung intensiver zu sprechen kommen, hier ein paar grundlegende Überlegungen zum Sinn und Zweck von Schichtentrennungen:

Viele Applikationen, leider auch sehr viele Webapplikationen, sind monolithisch aufgebaut. Das bedeutet, dass alles im Frontend stattfindet und die einzelnen Komponenten des Frontends fest miteinander verdrahtet sind. Das führt jedoch zu ein paar grundsätzlichen Problemen:

  • Einzelne Komponenten können nicht gegeneinander ausgetauscht werden
  • Komponenten müssen sehr viel voneinander wissen, um überhaupt sinnvoll interagieren zu können
  • Die Wartbarkeit fällt hinten herunter, denn der selbe Code findet sich an verschiedenen Stellen im Frontend wieder (Redundanz)
  • Änderungen werden zu Geduldsspielen, da die Änderungen an vielen Stellen vorgenommen werden müssen und es Abhängigkeiten gibt
  • Die Testbarkeit einzelner Funktionalitäten ist nicht gegeben

Es gibt potentiell dutzende weiterer Probleme.

Mit all diesen Problemen möchte die Schichtentrennung und die Kapselung von Funktionalitäten aufräumen. Hier geht es in erster Linie einmal darum, verschiedene Ebenen einer Applikation zu identifizieren:

  • Frontend (Webseite, Windows-Client, Fassade, Webdienst, …)
  • Geschäftslogik (Implementierung von Geschäftsprozessen, Verbergung von deren Details vor der Frontend-Logik)
  • Infrastruktur- / Datenlogik (Kapselung des Zugriffs auf Daten)

Dabei gibt es die Regel, dass die einzelnen Schichten stets nur über genau definierte Kommunikationspfade miteinander sprechen:

  • Das Frontend spricht mit der Geschäftslogik
  • Die Geschäftslogik spricht mit der Infrastruktur
  • Die Infrastruktur spricht mit der Geschäftslogik
  • Die Geschäftslogik spricht mit der Infrastruktur
  • Die Infrastruktur spricht nicht direkt mit dem Frontend!
  • Das Frontend spricht nicht direkt mit der Infrastruktur!

Diese genau definierten Kommunikationspfade müssen herausgearbeitet werden. Zu diesem Zweck muss man sich Gedanken über die Abbildung von Geschäftsprozessen (also großflächigeren Vorgängen, etwa einer Benutzerregistrierung samt aller Bedingungen) und Use-Cases (also kleinere, atomarere Schritte, etwa das Abfragen, ob ein Benutzer bereits im System existiert) machen. Geschäftsprozesse bestehen dabei in der Regel aus mehreren Schritten, Use Cases stellen üblicherweise kleinere Schritte dar. Beide können auf Ebene der Geschäftslogik implementiert sein, wobei die Geschäftsprozesse üblicherweise von außen ansprechbar sind und ihrerseits intern die verschiedenen kleinen Schritte (in Methodenform) umsetzen bzw. aufrufen. Als Regel sollte hier gelten: Je weniger ein Client (also eine Komponente, die eine Funktionalität nutzt) von der internen Implementierung einer Funktionalität weiß, desto besser ist es!

Aus diesem Grund wird die Geschäftslogik üblicherweise in Form einer Fassade definiert, die ihrerseits intern die verschiedenen Funktionalitäten (Use Cases) aufruft bzw. auslöst. Diese Fassade sollte aber so gestaltet sein, dass die interne Implementierung auch wieder gelöst werden kann (üblicherweise geschieht dies in Form einer Factory oder eines Providers). Das bedeutet für uns: Es muss eine Basisklasse oder ein Interface implementiert werden, das seinerseits alle notwendigen Funktionalitäten definiert und später implementiert werden kann.

Dies soll im Folgenden anhand einer kleinen Kundenverwaltung demonstriert werden, die ein paar rudimentäre Funktionalitäten (anlegen, ändern, löschen, zurückgeben) definiert. Diese Definition findet anhand einer abstrakten Basisklasse statt. Dieser Entwurfsansatz nennt sich Contract-First-Design, da zunächst der Vertrag (die Basisklasse) definiert wird und erst anschließend die Implementierung dieser Klasse stattfinden kann und muss.

Die Basisklasse sieht dabei wie folgt aus:

using System;
using System.Collections.Generic;
using System.Text;
using de.ksamaschke.Tools;

namespace BusinessLayer
{
   ///

   /// Defines methods for the handling of customers
   ///

   public abstract class CustomerManager : BaseManager
   {
      ///

      /// Returns a list of all customers
      ///

      public abstract List GetAllCustomers();

      ///

      /// Returns a specific customer
      ///

      public abstract Customer GetCustomer(Guid id);

      ///

      /// Returns a list of customers identified
      /// by their names
      ///

      public abstract List
         FindCustomersByName(string name);

      ///

      /// Returns a list of customers identified
      /// by their email-addresses
      ///

      public abstract List
         FindCustomersByEMail(string email);

      ///

      /// Updates a customer
      ///

      public abstract Customer
         UpdateCustomer(Customer customer);

      ///

      /// Deletes a customer
      ///

      public abstract bool
         DeleteCustomer(Customer customer);
   }
}

Verwendet wird hier auch eine Customer-Klasse, die die einzelnen Datensätze repräsentiert:

using System;
using System.Collections.Generic;
using System.Text;

namespace BusinessLayer
{
   public class Customer
   {

      private Guid _id = Guid.NewGuid();

      ///

      /// Id of the customer
      ///

           
      public Guid Id
      {
         get { return _id; }
         set { _id = value; }
      }

      private string _firstname;

      ///

      /// First name of the customer
      ///

           
      public string FirstName
      {
         get { return _firstname; }
         set { _firstname = value; }
      }

      private string _lastname;

      ///

      /// Last name of the customer
      ///

           
      public string LastName
      {
         get { return _lastname; }
         set { _lastname = value; }
      }

      private string _email;

      ///

      /// E-Mail-Address of the customer
      ///

           
      public string EMail
      {
         get { return _email; }
         set { _email = value; }
      }  
   }
}

Im nächsten Teil der Serie zeige ich dann, wie die Fassadenklasse aufgebaut und implementiert sein kann. Erst danach widmen wir uns einer konkreten Implementierung der obigen Basisklasse und dem Einsatz in einem Beispielprojekt.

Zum 1. Teil. Zum 3. Teil.

ASP.NET: Schichtentrennung – Hilfskomponente

So, auf vielfachen Wunsch: Eine kleine Serie zum Thema Schichtentrennung. Bevor wir aber anfangen können, mit Schichten zu arbeiten, benötigen wir einen Mechanismus, der es uns erlaubt, Komponenten anhand von Konfigurationseinstellungen zu laden. Das nutzen wir später, um diverse Entwurfsmuster brauchbar implementieren zu können.

Damit Komponenten geladen werden können, definieren wir zunächst eine Basisklasse BaseManager. Diese Basisklasse verfügt über eine überschreibbare Methode Init(), die von ableitenden Klassen genutzt werden kann, um sich zu initialisieren. Wir lagern also diese spezielle und individuelle Logik auf die einzelnen nachzuladenden Komponenten aus. Die Nutzung dieser Basisklasse ist jedoch optional – Komponenten sollten sich stets auch laden lassen, ohne das diese Basisklasse erweitert wird:

using System;

namespace de.ksamaschke.tools
{
   ///

   /// Base manager implementation
   ///

   public abstract class BaseManager
   {
      ///

      /// Initializes the component
      ///

      public virtual void Init()
      {
         // Intentionally left blank
      }
   }
}

Im nächsten Schritt wird der Lademechanismus definiert. Dieser ist wiederverwendbar gestaltet – er sollte also in jedem Kontext funktionieren können. Somit gibt es hier keine festen Verdrahtungen mit irgendwelchen Klassen, sondern die Instanzen werden per Reflection erzeugt.

Wie aber kommen wir an die benötigten Informationen (Klassenname, Assembly-Name)?

Diese liegen in der Konfigurationsdatei der Applikation (je nach Einsatzzweck entweder die web.config oder die app.config für Client-Applikationen). Der Einfachheit halber tragen wir die Typinformationen im appSettings-Bereich ein (in meinen Projekten gibt es hier üblicherweise einen eigenen Konfigurationsbereich je Komponente, aber das würde hier den Rahmen sprengen). Gefunden werden diese Typinformationen über den Typnamen der beim Aufruf der Methode Load() angegebenen Basiskomponente. Damit schlagen wir gleich zwei Fliegen mit einer Klappe: Das Ding wird wiederverwendbar, da ich keine feste Verdrahtung mit irgendwelchen Klassen habe und wir zwingen uns selbst zum so genannten Contract First-Design, bei dem wir zunächst eine Basisklasse und erst später konkrete Implementierungen bereit stellen.

Der Lademechanismus befindet sich in der Klasse ManagerLoader. Hier werden zunächst die Informationen aus der Konfigurationsdatei ausgelesen und anschließend wird versucht, die Komponente zu instanzieren. War dies erfolgreich, wird geprüft, ob die Komponente von der Basisklasse BaseManager erbt und wenn dem so ist, wird die Init()-Methode eingebunden:

using System;
using System.Configuration;
using System.Runtime.Remoting;

namespace de.ksamaschke.tools
{
   ///

   /// Loads a manager
   ///

   public class ManagerLoader
   {
      ///

      /// Loads a specific manager
      ///

      public static T Load()
      {
         // Get the name of the settings-area
         string settingsName = typeof(T).Name;
        
         // Get the config-value
         string nameAndAssembly =
            ConfigurationManager.AppSettings[
            settingsName];

         // Check the config-value
         if (!String.IsNullOrEmpty(nameAndAssembly))
         {
            // Get the type-name
            string typeName =
               nameAndAssembly.Substring(0,
                  nameAndAssembly.IndexOf(“,”));

            // Get the assembly-name
            string assemblyName =
               nameAndAssembly.Substring(
               nameAndAssembly.IndexOf(“,”) + 1);

            // Try to create an instance
            try
            {
               // Get the instance
               ObjectHandle handle =
                  Activator.CreateInstance(
                     assemblyName, typeName);

               if (null != handle)
               {
                  object result = handle.Unwrap();

                  // Try to initialize
                  if (null != result &&
                      result is BaseManager)
                  {
                     ((BaseManager)result).Init();
                  }

                  if (result is T)
                  {
                     return  (T) result;
                  }
               }
            }
            catch
            {
               // Intentionally left blank
            }
         }
        
         throw new Exception(
            string.Format(
               “Unable to load manager for type {0}”,
               settingsName));
      }
   }
}

Um später mit dieser Klasse Komponenten laden zu können, müssen Sie in der Konfigurationsdatei einen Eintrag anlegen, der dieses Format hat:


  
              value=”Managers.MemoryCustomerManager,
                  CustomerManagerImplementation” />

(Ohne den Zeilenumbruch innerhalb des -Elements)

Damit haben wir die Grundvoraussetzungen geschaffen, um später mit Schichtentrennungen arbeiten zu können. Sie sollten diese Klassen in einem eigenen C#-Projekt ablegen, so dass Sie sie später immer wieder verwenden können.

Zum 2. Teil. Zum 3. Teil.

ASP.NET: Ja, was wollt ihr denn?

Mit meinem letzten Post scheine ich ja einen wunden Punkt getroffen zu haben. Damit der etwas weniger wund wird, bin ich gerne bereit, praxisnahe Beispiele zu entwickeln – aber blind machen? Nö, nix ist.

Also, was wollt ihr? Welche Themen sollen mal ausführlicher behandelt werden? Was muss unbedingt mal erläutert werden? Und wer würde mir dabei ggf. helfen?

Gebt mal Feedback!

ASP.NET: Handarbeit statt DataSource!

Achtung: Polemik, Zuspitzung, Gemeinheit!

Das ASP.NET-Framework bietet diverse datengebundene Controls an, die der Visualisierung von Listen dienen. Die bekanntesten sind Repeater, DataGrid und GridView, gefolgt von einigen Exoten, etwa der DataList. Dazu gibt es wundervolle DataSource-Steuerelemente, die es erlauben, komplett ohne eine einzige Zeile an .NET-Code, Datenbanken an diese Steuerelemente zu binden.

Wenn man sich nun den Spaß macht, mal durch die Foren zu hirschen, dann bekommt man das kalte Grausen, denn die Kombination DataSource-Steuerelement + GridView-Steuerelement beherrscht seit .NET 2.0 quasi die Programmierung im ASP.NET-Umfeld. Was auch letztlich kein Wunder ist, denn sie sind idiotensicher einzusetzen und bringen Ergebnisse in wenigen Sekunden. Mit dabei sind dann gleich Features, wie etwa Paging und Sorting. Geil.

Nur leider ist es totaler Mumpitz und absoluter Schwachsinn, diese Controls extensiv und in Kombination miteinander einzusetzen. Denn sie räumen gründlich auf mit allem, was gemeinhin als Schichtentrennung, Wartbarkeit, Skalierbarkeit oder Qualität bekannt ist.

Die Gründe liegen auf der Hand:

Schichtentrennung ist nicht mehr gegeben, da der Code zum Abrufen von Daten – samt SQL-Statements! – speziell bei Verwendung vom SqlDataSource-Steuerelement in der WebForm selbst liegt. Statt also eine dedizierte Geschäftslogik damit zu beauftragen, Daten zu laden (zu cachen, aufzubereiten, zu analysieren, …), wird hier direkt auf die Datenquelle zugegriffen. Die komplette Verarbeitungslogik wandert dann in irgendwelche Ereignisbehandlungsmethoden im Code-Bereich der WebForm, statt in einer eigenen Verarbeitungsschicht zu liegen, wo sie ggf. auch gegen andere Implementierungen, Weiterentwicklungen oder sonstwas ausgetauscht werden könnte. Einzige akzeptierbare Ausnahme hier: Das ObjectDataSource-Steuerelement, das es erlaubt, gegen statische Methoden von Geschäftsobjekten zu arbeiten. Doof nur, dass mir dann dabei quasi vorgegeben wird, dass ich eben eine statische Methode brauche. Was ist mit meiner Factory (Provider, Builder, Broker, …), die ich ggf. habe?

Wartbarkeit? Gibts nicht mehr. Wie auch? Ein Beispiel: Es gibt zwanzig WebForms in einer Applikation, die auf diese Art arbeiten – also GridView + DataSource-Steuerelement. Jetzt wird das Datenmodell geändert, was im Rahmen von Weiterentwicklungen durchaus vorkommen soll. Das Ergebnis? Ich darf an mindestens zwanzig WebForms Änderungen vornehmen, statt es an nur einer Stelle in meiner Geschäfts- oder Datenlogik zu haben. Klasse. Nicht zu reden von der quasi nicht mehr vorhandenen Lesbarkeit des Codes. Neee, Wartbarkeit gibt es damit nicht.

Die Skalierbarkeit verschwindet ebenfalls hinter dem Horizont, denn ich habe schlicht keine brauchbare Möglichkeit mehr, in Datenhaltungs- und Caching-Prozesse einzugreifen. Geht nicht, ist ja alles inline. Klar, die SqlDataSource kann auch cachen – aber das kann ich im Code über das Application Data Caching viel zielgerichteter und genauer umsetzen. Wie also soll es skalieren? Zumal ich ja nicht mal in der Lage bin, einfach per Konfiguration etwa auf eine Web-Service-basierende Lösung umzustellen – ich müsste es halt überall ändern, wo ich auf die Daten zugreife. Oder eben eine andere Implementierung der Geschäfts- oder Datenhaltungslogik zu verwenden. Klar, es gibt die ObjectDataSource, aber… siehe oben. Dazu kommt: DataGrid und GridView sind zwar mächtig, aber eben auch langsam, denn die ganzen Funktionalitäten müssen ja irgendwo eingebunden werden. Auch, wenn man sie unter Umständen nicht benötigt. Und Tschüss, Skalierbarkeit!

Qualität? ROTFL! Wie denn, wo denn? Wie soll denn mit solch einem Gewurschtel, mit nicht mehr vorhandenen anerkannten Entwurfs- und Design-Patterns noch sowas wie Qualität erzeugt werden? Wenn man auch nur einen kurzen Moment mal nicht nur an die persönliche Bequemlichkeit denkt, dann muss man eigentlich zwangsläufig erkennen, dass man so ganz sicher alles baut, aber keine qualitativ hochwertige Software.

Die Lösung?

Back to the roots! Wenn datengebundene Listensteuerelemente, dann einen Repeater, keine komplett überdimensionierten GridViews und DataGrids. Und aus dem Code heraus die Daten binden. Kontrolle zurück gewinnen! Weiß denn eigentlich überhaupt noch jemand, wie man händisch die Daten binden lässt? Nein? Na, dann hier ein wenig Beispielcode:

protected override void OnPreRender(EventArgs e)
{
   // Factory für den Business-Layer
   BusinessLayer bl = BusinessLayer.GetInstance();

   // Personen laden
   IList persons = bl.GetPersons();

   // Personen binden
   rptPersons.DataSource = persons;

   // Datenbindung
   DataBind();
}

Oh je, sieben Zeilen Code! Wie schrecklich!

Leute, seid mir nicht böse, aber von nix kommt auch nix. Sicher, ich spitze hier zu, aber Webseiten entwickeln hat eben tatsächlich was mit Entwicklung, mit Code, mit Abläufen, mit Architektur, mit Denken zu tun. Ihr müsst das nicht machen, keine Frage – aber genau so werden dann eure Ergebnisse aussehen: Zusammengestückelt, unprofessionell, langsam, nicht pflegbar. You get, what you pay for – kein Einsatz, kein Ergebnis.

Zu den Themen Paging, Sorting und Caching dann ein anderes Mal. Google hilft sicher bis dahin weiter – Stichworte: PagedDataSource, IComparer und Cache.

Update: Ihr wollt, dass man mal was über bestimmte Themen schreibt? Dann sagt es mir.

SICHERHEIT: Schutz vor SQL-Injection

Das klassische Argument für den Einsatz parametrisierter Statements ist der Schutz vor SQL-Injection. SQL-Injection bezeichnet dabei den Vorgang des Einschleusens von fremden SQL-Statements in eigene SQL-Statements.

Beispiel: Die klassische Benutzer-Authentifizierung, bei der Benutzername und Kennwort übergeben werden. Der typische (C#-) Code für das generieren des SQL-Statements sieht dann in etwa so aus:

   // Werte einlesen
   String username = tbLoginName.Text;
   String password = tbPassword.Text;

   // Statement bauen
   String sqlStatement = “SELECT COUNT(1) FROM Users ” +
      “WHERE Username = ‘” + username + “‘ ” +
      “AND Password = ‘” + password + “‘”;

   // Command erzeugen
   DbCommand command = conn.CreateCommand();
   command.CommandText = sqlStatement;

   // Statement ausführen
   int result = (int) command.ExecuteScalar();

Lassen Sie uns mal einige Varianten für Benutzernamen und Kennwörter durchspielen.

Variante #1:

  • Benutzername: foo
  • Kennwort bla
  • SQL-Statement: SELECT COUNT(1) FROM Users
    WHERE Username = ‘foo‘ AND Password = ‘bla

Kein Problem hier, alles gut. Wenn Benutzername und Kennwort passen, dann ist es fein.

Variante #2:

  • Benutzername: ‘ OR 1=1 –
  • Kennwort: Weiß nicht
  • SQL-Statement: SELECT COUNT(1) FROM Users
    WHERE Username = ‘‘ OR 1=1 –‘ AND Password = ‘Weiß nicht

Großes Problem hier. Das SQL-Statement sieht irgendwie krank aus und kann ganz sicher nicht funktionieren. Sicher?! Doch, es funktioniert, denn die beiden aufeinanderfolgenden Bindestriche interpretiert ein SQL-Server beispielsweise als Kommentar – alles, was danach kommt, wird also ignoriert. Das tatsächlich ausgeführte SQL-Statement sieht also so aus:

  • SQL-Statement: SELECT COUNT(1) FROM Users WHERE Username = ” OR 1=1

Ganz gewaltiges Problem jetzt: Die Abfrage selektiert alle Datensätze, deren Username-Spalte entweder leer ist, oder für die 1=1 gilt. Und da 1=1 immer gilt, werden also alle Datensätze selektiert. Die Anmeldung wird jetzt also klappen.

Variante #3:

  • Benutzername: ‘; INSERT INTO Users (‘karsten’, ‘sicher’) –
  • Kennwort: Keine Ahnung
  • SQL-Statement: SELECT COUNT(1) FROM Users
    WHERE Username = ‘‘; INSERT INTO Users (‘karsten’, ‘sicher’) –‘ AND Password = ‘Keine Ahnung

Ebenfalls großes Problem: Nun werden zwei SQL-Statements ausgeführt, denn das Semikolon trennt SQL-Statements voneinander. Alles, was nach dem Kommentar-Zeichen kommt, wird komplett ignoriert. Das eigentliche SQL-Statement sieht also so aus:

  • SQL-Statement: SELECT COUNT(1) FROM Users WHERE Username = ”; INSERT INTO Users (‘karsten’, ‘sicher’)

Super, jetzt haben wir einen zweiten Datensatz in der Tabelle!

Die Lösung

Der Lösungsansatz ist ganz einfach: Parametrisierte SQL-Statements verwenden. Dazu muss der obenstehende Code nur ein wenig abgewandelt werden:

   // Werte einlesen
   String username = tbLoginName.Text;
   String password = tbPassword.Text;

   // Statement bauen
   String sqlStatement = “SELECT COUNT(1) FROM Users ” +
      “WHERE Username = @Username ” +
      “AND Password = @Password”;

   // Command erzeugen
   DbCommand command = conn.CreateCommand();
   command.CommandText = sqlStatement;

   // Parameter anfügen
   DbParameter param = command.CreateParameter();
   param.ParameterName = “@Username”;
   param.Value = username;
   command.Parameters.Add(param);

   param = command.CreateParameter();
   param.ParameterName = “@Password”;
   param.Value = password;
  command.Parameters.Add(param);


   // Statement ausführen
   int result = (int) command.ExecuteScalar();

Nun werden die Daten anders an die Datenbank übergeben – nämlich in zwei Schritten: Zuerst wird das Statement übergeben, anschließend erfolgt die Übergabe der Parameter. Auf Ebene der Datenbank wird nun nicht etwa ein SQL-Statement dynamisch zusammengebaut, sondern es wird wie in Form eines Prozeduraufrufs verarbeitet. Für die Beispiele gilt also:

Variante #1:

  • Benutzername: foo
  • Kennwort bla
  • SQL-Statement: SELECT COUNT(1) FROM Users
    WHERE Username = @Username AND Password = @Password

Kein Problem hier, alles gut. Wenn Benutzername und Kennwort passen, dann ist es fein.

Variante #2:

  • Benutzername: ‘ OR 1=1 –
  • Kennwort: Weiß nicht
  • SQL-Statement: SELECT COUNT(1) FROM Users
    WHERE Username = @Username AND Password = @Password

Kein Problem hier. Wenn es nicht zufällig einen Datensatz mit dem Benutzernamen ‘ OR 1=1 – und dem Kennwort Weiß nicht gibt, dann wird nix gefunden. Und wenn es den Datensatz gibt, ist der Benutzer ordnungsgemäß authentifiziert.

Variante #3:

  • Benutzername: ‘; INSERT INTO Users (‘karsten’, ‘sicher’) –
  • Kennwort: Keine Ahnung
  • SQL-Statement: SELECT COUNT(1) FROM Users
    WHERE Username = @Username AND Password = @Password

Kein Problem hier. Wenn es nicht zufällig einen Datensatz mit demBenutzernamen ‘; INSERT INTO Users (‘karsten’, ‘sicher’) – und dem Kennwort Keine Ahnung gibt, dann wird nix gefunden. Und wenn es den Datensatz gibt, ist der Benutzer ordnungsgemäß authentifiziert.

Also, keine Sicherheitsprobleme bei Verwendung von parametrisierten Statements.

Wer mehr über SQL-Injection wissen möchte, sollte dringend einen Blick in die Wikipedia werfen.

DOTNET: Hotfix-Pack für VS 2008 und VWD erschienen

Die Kollegen in Redmond haben heute Nacht ein Hotfix-Pack für VS 2008 veröffentlicht. Dabei werden diverse Bugs und Fehler adressiert, die speziell im Visual Web Developer aufgetreten sind.

Download hier. Danke hierher.

BOOK: ASP.NET 3.5 mit VB 2008 erschienen

So, gestern ist es rausgekommen, das erste deutsche Buch zu ASP.NET 3.5 – und ich hab daran mitgewirkt. :-)

Erschienen ist das Buch bei Addison-Wesley, Autoren sind Karsten Samaschke (Me), Jürgen Kotz, Andreas Kordwig, Christian Trennhaus, Tobias Hauser und Christian Wenz. Umfang ca. 1184 Seiten.

Der Verlag sagt dazu:

ASP.NET 3.5 baut auf dem erfolgreichen ASP.NET 2.0 auf und integriert das mächtige Ajax-Framework ASP.NET AJAX. Ein-, Umsteiger und fortgeschrittene ASP.NET-Programmierer erfahren hier alles Wesentliche über das .NET Framework und Visual Basic 9. Sie erhalten alle Tricks und Kniffe, die Sie für den erfolgreichen und professionellen Einsatz von ASP.NET benötigen: unterhaltsam, kompetent und ohne Umschweife.

Und der Klappentext liest sich so:

Mit ASP.NET 3.5 ist Microsofts Technologie zur Programmierung dynamischer Webseiten noch leistungsfähiger geworden. Gegenüber ASP.NET 2.0 gibt es zahlreiche Verbesserungen, unter anderem die die integrierte Ajax-Unterstützung und verbesserte Datenbankfunktionalität mit LINQ.

Die Autoren gehen auf alle wesentlichen Bestandteile von ASP.NET 3.5 ein und bieten ausführliche Beschreibungen zur Verarbeitung von Formulareingaben, zum Umgang mit Cookies und Dateien bis hin zum Zugriff auf Datenbanken und XML-Datenquellen sowie Web Services. Außerdem erfahren Sie alles zu den wichtigen Neuerungen wie ASP.NET Ajax, LINQ und der Entwicklungsumgebung Visual Studio 2008. Im Vordergrund stehen die konkreten Anforderungen des Webentwickler-Alltags. Unterhaltsam und anschaulich aufbereitet führt Sie dieses Buch auch zu fortgeschrittenen
Themen wie der dynamischen Generierung von Grafiken, den Web Parts und der Performancesteigerung durch Caching. Ein eigenes Kapitel widmet sich der Erstellung von Rich Internet Applications (RIA) mit Silverlight.


Aus dem Inhalt
  • .NET: Grundlagen, Architektur und Installation
  • HTML Controls, Web Controls und Web Parts
  • Masterseiten, Themes und Skins
  • Security: Benutzer- und Rollenverwaltung
  • ASP.NET AJAX und Silverlight
  • LINQ und Datenbankzugriff mit ADO.NET
  • XML und Web Services
  • Lokalisierung und Inhalte für mobile Endgeräte
  • Debugging
  • Caching
  • Web-Hacking (und Gegenmittel)
  • Spracheinführung in Visual Basic 2008

Auf DVD:
Alle Listings und Beispiele aus dem Buch, .NET Framework 3.5 plus Editor Visual Web Developer 2005 Express Edition.


Zu kaufen ist das Buch u.a. bei den Kollegen von Amazon.

BRAINDUMP: ScriptMethod-Attribut und UseHttpGet

Okay, nach einer Weile hab selbst ich es rausbekommen: Wenn man per ASP.NET AJAX eine Webdienst-Methode in einem JavaScript nutzen möchte, dann sollte man sich nicht nur auf die Dokumentation verlassen. Dort steht nämlich, dass der Webdienst so aussehen sollte:

namespace AJAXSample
{
   ///

   /// Webdienst
   ///

   [WebService(Namespace = "urn:bla"),
      ScriptService]
   public class GoodMorningService : WebService
   {

      ///

      /// Webmethode
      ///

      [WebMethod, ScriptMethod]
      public string GoodMorning()
      {
         return “Guten Morgen!”;
      }
   }
}

Die wirklich wichtige Stelle ist mal gefettet und gekursivt :-) . Das Einbinden kann dann per JavaScript in einer Seite (nach der Registrierung des Services im ScriptManager-Control) so aussehen:

Doof nur, dass das nicht funktioniert. Man erhält immer nur die Info, dass der Webservice nicht definiert sei.

Ein wenig googlen brachte mich dann – mal wieder – zur ASP.NET Zone und siehe, ich fand die Lösung: Beim ScriptMethod-Attribut muss explizit der Zugriff per HTTP-GET erlaubt sein:

[WebMethod, ScriptMethod(UseHttpGet=true)]
public string GoodMorning()
{
   return “Guten Morgen!”;
}

Dann funktioniert es auch.

AUA: …vs. Karsten

Ist das noch frech oder schon unverschämt von diesem Herrn?

  • Sag mal Stefan, wie ist denn die Erfahrung in Eurer Gruppe, ist es möglich mit Karsten sachlich zu reden, oder soll ich das einfach ignorieren. (hier)

Eigentlich zum Lachen, wenn es nicht so lächerlich wäre.

Wer will, kann es gerne hier diskutieren.

AUA: …vs. Java

In der C# vs. PHP-Diskussion ist erfolgreich die Java-Keule rausgeholt worden:

  • Java hat bei den Möglichkeiten “nachgeholt”, es ist wohl evtl. auch eher ein Problem, dass die Java-Entwickler ihre eigene Umgebung teilweise nicht mehr komplett verstehen/ausnutzen und so zu einem Grossteil imperformante (ich spreche gerade eher übers Web) Web-Applikationen herauskommen. (hier)

Das ist einfach nur Müll und Polemik. Muss wohl daran liegen, das Freitag ist.

AUA: Web vs. Client

Oh je, in der C#-Newsgroups gehts ab: Da hat eine gestandene Entwicklerin wohl Stress mit ihrem Chef, weil der (abgesehen davon, dass er letztlich die Weisungsbefugnis hat) eine Applikation auf Basis von Webtechnologien (egal, ob jetzt wirklich C# oder PHP, ob SQL Server, Oracle oder Postgre-SQL) erstellen lassen möchte. Sie möchte das nicht und hat dann unter dem Deckmäntelchen von “C# vs. PHP” versucht, sich argumentativ zu wappnen.

Das mag für sich durchaus zulässig sein bzw. ist schnell zu entlarven. Wenn ich mir dann aber die Kommentare der gestandenen Client-Entwickler-Fraktion (egal, ob C# oder VB) ansehe, dann bekomme ich das kalte Grausen über so viel Unwissenheit oder auch Ignoranz. Hier mal ein paar Auszüge:

  • PHP ist von der Performance her nicht konkurrenzfähig mit vorkompilierten Assemblies aus .NET. PHP selbst wird diesbzgl. sogar überholt von seinen Perl oder Pyton-Kollegen. Auch Erweiterungen wie Zend Optimizer etc. können das Loch da nicht stopfen. (hier)
  • Web-Anwendungen erfordern deutlich erhöhten Aufwand, im Gegensatz zu Windows Forms Anwendungen. (hier)
  • Um eine gewisse Usability zu erreichen, muss in Web-Anwendungen deutlich mehr Aufwand getrieben werden, als zum Beispiel in eine Windows Forms Anwendung. (hier)
  • Die ‘Usability’ sollte IMHO entscheidend sein! Heute akzeptieren leider viel zu viele Anwender noch jeden Unsinn im Web-Browser. Ich hoffe, dass sich dies bald mal bessern wird und die Programmierer+Anwender ‘reifer’ werden. (hier)
  • Heute sind zu viele ‘Kunden’ entweder (noch) inkompetent oder vom Web ‘geblendet’ (hier)
  • Bei ernsthafter Betrachtung fallen aber heute IMHO 99% aller Web-Sites schlicht durch. (hier)
  • Ein Web/Browser ist IMHO brauchbar um zB ganz rasch ein paar diverse Informationsquellen durchzusurfen/filtern. Aber überall wo man länger interaktiv & produktiv mit _arbeiten_ soll, gehören _native_ Apps hin, ohne den Browser-Quatsch drum herum. (hier)
  • Es ist ein Armutszeugins aktueller Informatik sondergleichen, wenn die Leistungsfähigkeit heutiger PCs wegen HTML/Browser usw völlig brach liegt. (hier)

Aua, das tut wirklich weh. Die Diskussion läuft übrigens noch – hier als Webversion, hier als Newsgruppen-Version und wer will kanns auch hier im Form besprechen.

ME TOO: Microsoft veröffentlicht Quellcode vom NET-Framework

Wie jeder andere .NET-Entwickler, der ein Blog bedienen kann, muss auch ich erwähnen: Der Kollege Guthrie hat erklärt, dass der Quälcode Quellkot Quellcode vom .NET-Framework jetzt verfügbar ist.

Musste man ja mal sagen.

ASP.NET: ASP.NET-Controls clientseitig per JavaScript aktivieren oder deaktivieren

Nur mal schnell zum Mitmeißeln: Alle HTML-input-Elemente verfügen über eine per JavaScript ansprechbare disabled-Eigenschaft. Dieser kann ein boolescher Wert zugewiesen werden, der steuert, ob das Element aktiviert ist oder nicht. Das gilt selbstverständlich auch für ASP.NET-Controls, etwa Buttons oder TextBoxen, die ihrerseits in der Ausgabe zu eben diesen HTML-input-Elementen werden.

Dieses Wissen vorausgesetzt, kann beispielsweise ein per default deaktivierter Button durch einfaches Klicken auf eine CheckBox aktiviert werden. Zum Einsatz kommt im Grunde nur die bereits besprochene disabled-Eigenschaft und natürlich die ClientID-Eigenschaft des ASP.NET-Steuerelements:

  
           Enabled=”false” … />

  

DOTNET: Letzter!

Huch, nicht das jemand denkt, ich hätte den .NET 3.5-Start und das VS 2008-Release verpasst. Vor lauter Mac-erei. Nix da. Ich hab nur nix darüber gebloggt, weil man sich dem sowieso nicht entziehen kann.

Aber, und deshalb schreibe ich diesen Beitrag überhaupt, die Kollegen aus Redmond haben wirklich dazu gelernt, denn die Downloads flutschen nur so (ja, liebe CallBoys, sowas geht auch elektronisch): Der komplette knapp 4 GB große Download war bei mir in weniger als 90 Minuten auf der Kiste.

Ich liebe 16 MBit. :-)

BRAINDUMP: Patterns für die Ausgabe von Zeichenketten

Hier findet man eine brauchbare Übersicht über die möglichen Patterns der DateTime.ToString()-Methode.

Nächste Seite »