HowTo: Membership in Klassenbibliotheken / DLLs

In diesem HowTo habe ich das ASP.NET Membership System mal kurz vorstellt. Ich hab in einem Projekt nun das Membership System eingesetzt und auch seine großen Schattenseiten kennengelernt.

Der erste große Kritikpunkt:
Da entwirft man eine 3-Tier Architektur und am Ende hängt einer der wichtigsten Teile (das Usersystem) mit in der WebApp – das ist mehr als unschön.

Mein Wunsch: Das Usersystem soll mit im Backend verschwinden.
Eine grobe Skizze (wobei man den “Service” noch in andere Schichten unterteilen kann):

image
Die “App.Config” sollte den ConnectionString speichern und auch die Membership-Konfiguration übernehmen.
Ich sage hier bewusst “sollte”, da ich es leider nicht ganz so perfekt hinbekommen hab.

Allerdings erst mal Schritt für Schritt: Das Membership-System muss in eine DLL rein.

Grundsätzlicher Aufbau:

image

  • DllMembership.Lib” ist unser Service in dem wir unseren “MembershipService” haben.
  • DllMembership.Web” ist eine gewöhnliche ASP.NET Website.
  • DllMembership.Test” ist unser UnitTest-Projekt

Schritt 1: Membership-System in der DLL Konfigurieren

Als erstes müssen wir in der App.Config folgende Konfiguration erstellen:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="ASPNETDBConnectionString" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename='C:\Users\rmu\Documents\Visual Studio 2008\Projects\Blogposts\DllMembership\DllMembership.Lib\DB\ASPNETDB.MDF';Integrated Security=True;User Instance=True"
     providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.web>
    <membership>
      <providers>
        <remove name="AspNetSqlMembershipProvider"/>
        <add name="AspNetSqlMembershipProvider"
             type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
             connectionStringName="ASPNETDBConnectionString"
             enablePasswordRetrieval="false"
             enablePasswordReset="true"
             requiresQuestionAndAnswer="false"
             applicationName="/"
             requiresUniqueEmail="true"
             passwordFormat="Hashed"
             maxInvalidPasswordAttempts="12"
             minRequiredPasswordLength="1"
             minRequiredNonalphanumericCharacters="0"
             passwordAttemptWindow="10"
             passwordStrengthRegularExpression="" />
      </providers>
    </membership>
  </system.web>
</configuration>

Die Data Source muss natürlich entsprechend des DB Speicherortes ausgewechselt werden.
Wichtiger Hinweise: Ich verwende dieselbe DB wie aus dem anderen Blogpost.
Ganz wichtig: Damit die App.Config auch angenommen wird, müssen die Properties richtig gesetzt sein:

image

Jetzt fügen wir noch die “System.Web” Referenz hinzu:

image

Schritt 2: Service schreiben

Jetzt implementieren wir unseren sehr einfachen Service:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Security;

namespace DllMembership.Lib
{
    public class MembershipService
    {
        public IList<User> GetUsers()
        {
            MembershipUserCollection col = Membership.GetAllUsers();
            List<User> returnList = new List<User>();
            foreach (MembershipUser user in col)
            {
                User u = new User();
                u.Name = user.UserName;
                u.Email = user.Email;
                returnList.Add(u);
            }
            return returnList;
        }

        public User Login(string username, string password)
        {
            User returnUser = new User();

            if (Membership.ValidateUser(username, password))
            {
                returnUser.IsLoggedIn = true;
                returnUser.Name = username;
                returnUser.Password = password;
                return returnUser;
            }
            else
            {
                returnUser.IsLoggedIn = false;
                return returnUser;
            }
        }

        public User GetUser(string username)
        {
            MembershipUser user = Membership.GetUser(username);
            User returnUser = new User();
            returnUser.Name = user.UserName;
            returnUser.Email = user.Email;
            return returnUser;
        }
    }
}

Dieser Service gibt ein “User” Objekt zurück (im Prinzip findet ein Mapping zwischen dem MembershipUser und unserem User statt) :

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

namespace DllMembership.Lib
{
    public class User
    {
        public bool IsLoggedIn { get; set; }
        public string Name { get; set; }
        public string Password { get; set; }
        public string Email { get; set; }
    }
}

Diese “User” Klasse fungiert ebenfalls als “Result” für unsere Service-Aufrufe. Klappt zum Beispiel die Login-Methode nicht, wird einfach die “IsLoggedIn” Property auf false gesetzt.

Schritt 2.5: Unit Tests

Ich habe 3 Unit-Test Methoden geschrieben, welche die grobe Funktionalität testen. Das klappt soweit.

Schritt 3: Das Web-Projekt
Damit unser Nutzer während einer Session auch eingeloggt bleibt, müssen wir noch die Form-Authentication aktivieren.
Hier müssen wir noch den Cookie “manuell” setzen. Dazu habe ich eine AppHelper mir erstellt:

namespace DllMembership.Web
{
    public static class AppUtil
    {
        public static User GetActiveUser()
        {
        if(HttpContext.Current.User.Identity.IsAuthenticated == false)
            {
                return new User() { IsLoggedIn = false };
            }
            else
            {
                MembershipService service = new MembershipService();
                User returnValue = service.GetUser(HttpContext.Current.User.Identity.Name);
                returnValue.IsLoggedIn = true;
                return returnValue;
            }
        }

        public static User Login(string username, string password)
        {
            MembershipService service = new MembershipService();
            User returnValue = service.Login(username, password);
            if (returnValue.IsLoggedIn)
            {
                FormsAuthentication.SetAuthCookie(returnValue.Name, true);
                returnValue.IsLoggedIn = true;
            }
            else
            {
                returnValue.IsLoggedIn = false;
            }

            return returnValue;
        }
    }
}

Hierbei gibt es zwei Methoden “GetActiveUser” und “Login”.
Zum “Login”:
Diese Methode übergibt die Parameter zum Service und wenn der Login erfolgreich war, wird dieser über ein Cookie über die Session hinweg authentifiziert.
Die “GetActiveUser”:
Diese Methode schaut einfach, ob der User im HttpContext authentifiziert ist, wenn nicht, gibt es keinen angemeldeten Nutzer, ansonsten wird der aktuelle Nutzer geladen.

Schritt 4: Ausgabe des Usernamen auf der Website

            DllMembership.Lib.User returnUser = AppUtil.GetActiveUser();
            if (returnUser.IsLoggedIn == false)
            {
                this.UserName.Text = "unangemeldet";
            }
            else
            {
                this.UserName.Text = returnUser.Name;
            }

Dadurch können wir leicht prüfen, ob jemand angemeldet ist, oder nicht.

Das Problem dabei
Leider geht die Lösung so wie ich sie hier gepostet habe, nicht ganz, denn man muss leider in der Web.Config die Membership Konfiguration und den ConnectionString noch extra angeben.
Das “Witzige” an der Geschichte: Die Unit-Tests laufen. Sobald dies aber auf der Webseite genutzt wird, überschreiben wohl die Web.Config Einstellungen die App.Config Einstellung – ihr könnt es gerne selber ausprobieren.
Ich finde das etwas unschön, aber verschmerzbar (bzw. fällt mir nix anderes ein).
Wenn jemand eine Lösung weiß, dann her damit :)

Fazit
Das ist nur ein “Prototyp”, ich habe längst nicht alles fertig mir ausgedacht und würde vielleicht noch extra Properties einbauen und den Service umbasteln. Allerdings sollte dies der erste Schritt sein um zu zeigen, wie man das Membership dahin packt, wo es hingehört: In eine andere Schicht.

[ Download Source Code ]
* In den *.config muss der ConnectionString angepasst werden
** Anmeldedaten stehen in der ReadMe.txt in WebApp Ordner


Kick It auf dotnet-kicks.de
Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf dem Laufenden bleiben willst, abonniere unseren RSS Feed oder folge uns auf Twitter.

About the author

Written by Robert Mühsig

Robert Mühsig (@robert0muehsig) ist Webentwickler und beschäftigt sich mit Web-Frameworks (vor allem dem ASP.NET MVC Framework) und scheut sich auch nicht vor Javascript. Ansonsten bloggt er über all jene Probleme, die ihm über den Weg laufen. Seit 2008 ist er Microsoft MVP für ASP.NET und er arbeitet bei der T-Systems Multimedia Solutions GmbH in Dresden. Treffen kann man ihn online via Twitter (@robert0muehsig) oder dieser Seite oder bei der .NET User Group Dresden.

3 Responses

  1. Ich persönlich habe sehr gute Erfahrung damit gemacht, das Problem genau andersherum zu lösen. Die Verwaltung der Benutzer ist bei mir ein normaler Teil des Domain Models und weiß nichts von ASP.NET und den Membership Providern. Um das ganze dann Membership-kompatibel zu machen schreibe ich einen extrem einfachen MembershipProvider, der die Aufrufe lediglich an den Service Layer weiterleitet und zwischen MembershipUser und “meinem” User aus der Domain vermittelt.

    Etwa:
    public class MyMembershipProvider : MembershipProvider {
    IMemberService service;

    public override void Initialize(string name,
    NameValueCollection config){
    service = IocContainer.Resolve();
    }

    public abstract MembershipUser GetUser( string username, bool userIsOnline) {
    User user = service.GetUserByUsername(username, userIsOnline);
    return ToMembershipUser(user);
    }

    private MembershipUser ToMembershipUser(User user){
    return new MembershipUser(….);
    }

    // usw.
    }

    Die Service Instanz hole ich mir mittels ObjectLocator Pattern, wobei mein ObjectLocator ein IoC Container ist (das ASP.NET Provider Model unterstützt ja leider kein IoC von hause aus, sonst würde ich den Service als Dependency injizieren lassen). Letzterer konfiguriert mir den Service wie benötigt, sodass sich der MembershipProvider hier nicht drum kümmern muss.

    Auf diese Weise habe ich volle Kontrolle über mein Domain Model und bin nicht abhängig vom ASP.NET MembershipProvider. Das macht das unit testen deutlich einfacher. Es entfällt auch die mMn unschöne Notwendigkeit, der DLL eine Konfigurationsdatei zu geben.

    Reply
  2. Das ist natürlich auch eine geniale Idee. Ich schau mal und versuch auch mal nach dieser Methode das zu basteln.
    Danke für den Tipp :)

    Reply

Comment on this post

Letzte Posts

  • Carriage Return / Neue Zeile in Textareas

    Eine kleine Aufgabe: Jede neue Textzeile (Carriage Return/Wenn man Enter drückt ) in einer Textarea soll ein Element in einer Auflistung sein – wie mach ich das jetzt am einfachsten? Eigentlich ein grundlegendes Element im Web und der Nutzer macht bewusst Absätze – daher wäre es nur gerecht, wenn man das auch entsprechend würdigt. Kleine ...

  • image.png
    Doom, Quake, Wolfenstein & co. Source Code auf GitHub

    id Software, die Macher von Doom, Quake, Wolfenstein & co., stellen regelmäßig ihre älteren Spieltitle als Open Source zur Verfügung. Das Ganze runterzuladen fand ich bisher immer recht mühselig, allerdings gibt es seit kurzer Zeit die Sourcen auch auf GitHub. Darunter Spiele wie Doom 3, Quake 3, Wolfenstein für iOS. Wer also schon immer mal ...

  • image.png
    Twitter Bootstrap 2.0 released & “Release Präsentation”

    Wie bereits vom Twitter Bootstrap Team angekündigt wurde offiziel die Version 2.0 des UI Toolskits “Twitter Bootstrap” veröffentlich. Zudem wurden die Slides, welche bei der Release Party gezeigt wurden auch veröffentlicht: Downloads finden sich auf der Twitter Bootstrap Seite auf GitHub. Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf ...

  • image.png
    Javascript zu Dart Translator

    Dart, Google Javascript Alternative, wurde vor ein paar Monaten vorgestellt und die Webentwickler Szene ist noch etwas gespalten, ob Dart nun überflüssig ist oder einfach nur cool und längst überfällig ist. Um die Sprache näher zu erläutern hat Google die grundlegenden Javascript Basics nach Dart übersetzt. Das Ergebnis ist der “Translator”. Der Name mag momentan ...

  • Twitter Bootstrap 2.0–“Beta”

    Twitter Bootstrap, ein UI-Toolkit für Web-Applikationen von Twitter, erscheint (wie bereits berichtet) demnächst in der Version 2.0. Der offizielle Release ist am 31. Januar, allerdings beginnt jetzt laut Mark Otto (einer der Hauptentwickler von Twitter Bootstrap) die intensive Test-Phase. Das heisst, das es nun offiziel auch die 2.0 Dokumentation online gibt. Im Vergleich zur aktuellen ...

Support us!

Facebook