get-the-solution


Page 1 of 3123

Add Token Authorization to AngularJS and WebApi

By
on Regards: .NET Framework; C#;

Protection and authorization for a Single Web Application and a RESTFul Service

Introduction

There exists some ways to implement an authentication into RESTFul Service. For this article I will focus on the HTTP Token Header technique, because its really easy to implement with the JsonWebTokenHandler.(RESTful Authentication, 22.08.2015, http://stackoverflow.com/questions/319530/restful-authentication?lq=1  ).

To keep it short we focus on the practical aspect. The demonstration contains a Single Web Application with AngularJS which is hosted on an ASP.NET MVC page. The RESTFul Service will be represented by an ASP.NET WebApi project. I want to mention that most of the code was built from code snippets which I researched from other blogs or stackoverflow to reduce time. So the example project consists of several code snippets and some custom coding.

As you know the RESTFul Service is stateless. We need to achieve that the WebApi can correctly identify our current requests to a user. There the token, or precisely the Json Web Token, comes into play. Because the returned data is formatted as JSON the term Json Web Token is more common in that subject.

When sending a valid login request to the WebApi, it will issue a token for the logged in user. The SWA will catch the returned token from the login. From now the SWA will add the token to each call into the HTTP Header. The WebApi will check if there exists a JWT and extract the containing user information. This process is visualized in the graphic below.

Description of the Security token service

After associating a user to the request we can permit or deny the request.

{"iss":"Identity Provider","name":"John Doe","admin":true}

The Json Web Token is a JSON-based open standard which contains the user payload a signature and the following important header information’s (JSON Web Token, https://en.wikipedia.org/wiki/JSON_Web_Token, 22.08.2015).

  • Issuer (iss) : The Service which created the jwt
  • Subject (sub) : The subject regarding the token
  • Audience (aud) : Which application can use the token
  • Expiration time (exp): When expires the token
  • Not before (nbf): Don’t use the token before
  • Issued at (iat): When was the token created
  • JWT ID (jti) : The ID of the token

(“JSON Web Token (JWT)”, https://self-issued.info/docs/draft-ietf-oauth-json-web-token.html, 22.08.2015 )

Frontend – AngularJS

Our AngularJS web application has got an Authentication service which can initiate the login process. It sends the logon and the password to the webapi and saves the returned token. The token will be set to the default Authorization HTTP Header of the httpService (“Setting angularjs $http headers globally“, http://stackoverflow.com/questions/27134840/setting-angularjs-http-headers-globally, 22.08.2015).

In order to prevent the user from navigating to any other page of our application, we add a callback to the angular module. When navigating between pages the callback checks if the user is logged in our Authentication service.

This is done by registering the “$locationChangeStart” event which will be fired on any page navigation. If the user is not logged in we force a redirect to the login page. A logout can be done by refreshing the whole page, the state of the SWA will then be lost.

After a successful login we forward the user from the login- to the home view. The corresponding homeController makes an authenticated webapi request which will then return the current users privileges.

Backend – WebApi

Let’s switch to the WebApi’s PostUser action method. It is used to login a user and issue a security json web token.

First of all we check if the delivered user credentials are correct. Normally this is done by querying the database or another resource. The user and the roles are implemented with the claims-based identity of the Windows Identify Foundation. So if you never heard of claims you should definitely check Andras Nemes blog series about that topic (“Introduction to Claims based security in .NET4.5 with C# Part 4: authorisation with claims”, http://dotnetcodr.com/2013/02/21/introduction-to-claims-based-security-in-net4-5-with-c-part-4-authorisation-with-claims/, 22.08.2015 ).

For creating JSON Web Tokens Microsoft published the JwtSecurityTokenHandler class which can be found in the NuGet Package System.IdentityModel.Tokens.Jwt.

We create the ClaimsIdentity for the user which we wish to log in by adding his claims. After that we can create the token with tokenHandler.CreateToken(issuer: “http://localhost”, audience: “http://localhost”, subject: claimsIdentity, expires: expires, signingCredentials: new X509SigningCredentials(cert));

So as you see the issuer and audience value are equal because we create the token and at the same time consume it. With the expire parameter we can determine how long the token is valid and with the signing credentials we sign our token against manipulation. The returned encoded token is a base64 string.

The Json Web Token will be signed when issuing it with a private key. When a token will be “read”, it will be validated against the public key.

In this project I deposit the private and the public key as files, which is not recommended regarding security issues. In a real world example it would be stored in the certificate manager and for a convenient project start up I skipped that part. By the way, I used the following self-signed certificate tool http://blog.pluralsight.com/selfcert-create-a-self-signed-certificate-interactively-gui-or-programmatically-in-net.

We should not only be able to create tokens, but also to read and “extract” the information from it. This can be done with a DelegatingHandler which intercepts every web request of the WebApi.

Vittorio Bertocci wrote an excellent article http://www.cloudidentity.com/blog/2013/01/09/using-the-jwt-handler-for-implementing-poor-man-s-delegation-actas/on that topic a while ago. The JwtSecurityTokenHandler has changed since the article was released.

What does the TokenValidationHandler do in particular?

First of all it intercept the web request before the concerning action method gets executed. It checks if the request contains a HTTP Header with the key Authorization. If not, the request will be forwarded to the action method. If you wish to protect the action method you can add a claimsauthorization attribute (details about that later).

If there exists a JWT token it will be validated against the TokenValidationParameters and the public key. With ValidateToken the user information from the token will be extracted and set the current user of the HttpContext and Thread. If the validation didn’t fail the request will be forwarded to the action method. Otherwise we abort the current request and return an unauthorized status code.

If for specific action methods certain claims are necessary we can use the ClaimsAuthorization attribute. Before the action method gets executed, it checks if the current HttpContext.User got the required claims.

The order how the web request is processed

Remarks

The solution is configured in a way that you can easily execute the project. Make sure you installed Visual Studio 2013 with the Web Development Tools and the latest updates. Also The Web Essential Tools should be installed with the latest TypeScript Version. You can Download the project from Github.

Press F5 to start the Web Project and navigate to http://localhost:5118/Default/Index/to start the SWA. Then switch to Visual Studio and right click on the WebApi Project and select Debug àStart new Instance. Create some breakpoints and try to login with the user martin in the web application and step through the program to see what’s happening.

Because the web projects are hosted under different domains (http://localhost:5118vs http://localhost:5117) we run into the CORS issue. To solve that I installed the CORS NuGet Package from Microsoft and enabled it with the EnableCors attribute. You may also want to check http://www.asp.net/web-api/overview/security/enabling-cross-origin-requests-in-web-api.

I hope I could show you that it is really is easy to implement an authentication mechanism in AngularJS and ASP.NET WebApi. If you have any questions or suggestion feel free to comment or write me on twitter.

Other used Links:

http://stackoverflow.com/questions/18032038/securing-the-jwt-with-a-x509certificate2-jwtsecuritytokenhandler create jwt token

http://stackoverflow.com/questions/20198124/read-private-key-from-pfx-file read certificate from file

http://stackoverflow.com/questions/52797/how-do-i-get-the-path-of-the-assembly-the-code-is-in get current assemply path


Page 1 of 3123

DLL Hell – Could not load file or assembly System.Web.http 4.0.0.0

By
on Regards: .NET Framework;

Bei einem Erstdeployment kann es zu unliebsamen Überraschungen kommen bei denen nicht sofort die Ursache erkennbar ist.

Besonders tragisch ist es, wenn die Fehlermeldung am Testsystem nicht am Entwicklungssystem reproduzierbar ist. Nach dem Installieren und Aufrufen der Applikation wird vom IIS die Fehlermeldung “Could not load file or assembly System.Web.http 4.0.0.0” ausgegeben.

Die StackOverFlow Antworten brachten keine Lösung, weshalb das Problem genauer anlysiert werden musste.

Das Testsystem war eine Windows Server 2003 Installation und siehe da, im GAC war tatsächlich die Datei nicht vorhanden. Der Global Assembly Cache ist dazu da, um Versionskonflikte zu vermeiden. Ein weiteres Feature vom GAC ist, das die Assemblies dort nativ vorliegen. Während der Installation vom .NET Framework werden die Assemblies prozessoroptimiert kompiliert und sind dann in nativem Code verfügbar. Das heißt die Bibliotheken können um einiges schneller gestartet werden, als die “üblichen” welche in der Common Intermediate Language vorliegen und mit dem JIT ausgewertet werden.

Wie auch immer, nach Recherechen hat sich dann herausgestellt, dass genau diese File und noch ein paar Andere auf Server Installationen nicht ausgeliefert werden (Security Issues).

Also habe ich die Option Copy to Local im Visual Studio auf true gesetzt. Auch das zeigte keinen Effekt. Somit löschte ich die Referenz und kompilierte nochmal das Projekt auf dem Entwicklungssystem. Trotz der Änderung kam am Testsystem nach wie vor die Fehlermeldung. Also die Änderung wieder Rückgäng machen und die Referenz System.Web.http zum Projekt hinzufügen.

Nach dem die Projekt-Referenzen genauer unter die Lupe genommen wurden, fiel eine Third Party Referenz auf, die eine Abhänigkeit auf System.Web.http hatte. Außerdem waren die referenzierten Versionen auf System.Web.http der Lib und der Web Application unterschiedlich. Es wurden deshalb alle Referenzen von System.Web.Http zunächst einmal auf die Version upgedated welche die Third Party Lib verwendete. In der web.config ist auch der Eintrag

 < dependentAssembly>
    < assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" />
    < bindingRedirect oldVersion="0.0.0.0-5.3.0.0" newVersion="5.3.0.0" />
 < / dependent Assembly > 

interessant. Den dieser ist dafür verantwortlich, dass wenn keine geeignete Assembly Version gefunden wird, ein Fallback auf den GAC passiert. Genau das ist auf der Testmaschine passiert. Die Assembly System.Web.http war im bin Folder der Web Application vorhanden, aber aufgrund des web.config Eintrag wurde stattdessen auf den GAC zurückgegriffen. Da auf der Windows Server Installation die File nicht existierte, kam die Fehlermeldung. Auf dem Entwicklungsystem waren beide Assembly Versionen vorhanden, weshalb der Fehler so nicht reproduzierbar war.

Nachdem bei allen Projekten die Referenzen auf die korrekte Assembly Version verwiesen haben kam immer noch die Fehlermeldung System.Web.Http 4.0.0.0 wurde nicht gefunden, obwohl die neue Assembly Version 5.3.0.0 lauten sollte. Nach weiterem suchen, hat sich herausgestellt, dass die Projektdateien im Visual Studio zwei Referenzen auf die gleiche Assembly hatten mit jeweils der unterschiedlichen Assembly Version. In diesem Fall hat die Visual Studio GUI die falsche Information angezeigt. Das Visual Studio Projekt musste also ungeloaded werden und in der xml Ansicht nachbearbeitet um die alten Assembly Verweise zu löschen. Der Fehler könnte von einem auto. TFS merge stammen.

In diesem Fall haben mehrere ungünstige Faktoren zusammengespielt.


Page 1 of 3123

C# Plugins mit AppDomains realisieren

By
on Regards: .NET Framework; C#;

1. Einführung

Im letzten Post konnte man ein einfaches  Beispiel sehen, wie während der Laufzeit „Plugins“ geladen und ausgeführt wurden.

Wir möchten aber die Plug-Ins von unserer Hauptanwendung isolieren und eigene Sicherheitsrichtlinien für diese festlegen. So heißt es z.B. in der MSDN

„Verwenden Sie Anwendungsdomänen, um Aufgaben zu isolieren, die einen Prozess zum Absturz bringen könnten. Wenn der Zustand der AppDomain, in der eine Aufgabe ausgeführt wird, instabil wird, kann die AppDomain ohne Auswirkungen auf den Prozess entladen werden. Dies ist wichtig, wenn ein Prozess über einen längeren Zeitraum ohne Neustart ausgeführt werden muss. Sie können auch Anwendungsdomänen verwenden, um Aufgaben zu isolieren, die keine Daten gemeinsam verwenden sollten.“
(AppDomain-Klasse, 22.08.10, http://msdn.microsoft.com/de-de/library/system.appdomain.aspx)

Ein .Net Prozess enthält mindestens eine AppDomain. Diese  StandardAppDomain wird auch Default AppDomain genannt. Jede AppDomain hat ihren eigenen virtuellen Adressenspeicher. Ein Prozess kann also mehrere AppDomains enthalten.

Da jede AppDomain ihren eigenen virtuellen Adressenspeicher hat, ergeben sich folgende Eigenschaften:

  • AppDomains können nicht auf Objekte von anderen AppDomains direkt zugreifen
  • AppDomains können entladen werden, mit Ausnahme der Default AppDomain
  • AppDomains können unterschiedliche Sicherheitsprivilegien vergeben werden
  • AppDomains können unterschiedlich konfiguriert werden (AppDomainSetup)

Wie man aus der Abbildung entnehmen kann, werden die Assemblies in den AppDomain auch getrennt behandelt. Während der Windows Prozess ausgeführt wird, kann man keine Programm-Dateien, wie z.B. die Plugin.dll ersetzen oder löschen.   Das Löschen der Plugin.dll ist erst möglich, wenn die „AppDomain 2“ entladen wurde.

Um das Verändern der Plugindatei während der Laufzeit zu ermöglichen, gibt es das Feature Shadow Copy.
Dabei wird die zu ladende Assembly in ein temporäres Verzeichnis kopiert. Die Originaldatei kann somit jederzeit ersetzt werden. Siehe dazu auch AppDomain and Shadow Copy (AppDomain and Shadow Copy, 22.08.10, http://blogs.msdn.com/b/junfeng/archive/2004/02/09/69919.aspx )

Bestimmte Grundtypen werden in jeder AppDomain benötigt. Um Ressourcen zu schonen, gibt es eine AppDomain neutrale Zone. In diese wird z.B. die MSCorLib.dll geladen da diese den Typ System.Object, System.Integer usw. enthält. Alle AppDomains können auf diese „Domain-Neutral Assemblies“ zugreifen.
Um auf Objekte von einer AppDomain auf die andere zugreifen zu können, müssen die Objekte MarshalByRefObject implementieren.

2. Sonstige Bemerkungen

Bibliotheken die mit Assemblies.Load geladen werden, „landen“ in der DefaultAppDomain. Diese Dateien werden erst nach dem Beenden des Prozesses wieder freigegeben.
Hier ein Beispielcode:

	 public static void Main(string[] Args)
	 {
	    //Gib die AppDomain zurück in der wir uns befinden
	    System.Console.WriteLine(AppDomain.CurrentDomain);
	 
	    //AppDomain erstellen
	    AppDomain appDomain = AppDomain.CreateDomain("AppDomain #2");
	 
	    //Schauen wir ob es sich um die Default Domain handelt
	    System.Console.WriteLine(AppDomain.CurrentDomain + " " + AppDomain.CurrentDomain.IsDefaultAppDomain());
	 
	    System.Console.WriteLine(appDomain + " " + appDomain.IsDefaultAppDomain());
	 
	    Print(AppDomain.CurrentDomain.GetAssemblies());
	 
	    //In unsere AppDomain lib laden
	    appDomain.DoCallBack(new CrossAppDomainDelegate(delegate()
	        {
	            System.Console.WriteLine(AppDomain.CurrentDomain);
	            Assembly.LoadFrom(@"C:\Users\martin\Plugin.dll");
	        }));
	 
	    Print(appDomain.GetAssemblies());
	}
	static void Print(System.Reflection.Assembly[] Assemblies)
	{
	    int i = 0;
	    foreach (System.Reflection.Assembly assem in Assemblies)
	        System.Console.WriteLine("[{0}] {1}", ++i, assem.GetName().Name);
	}

Die Ausgabe dazu sieht wie folgt aus:

	Die Ausgabe dazu sieht wie folgt aus:
	Name:ConsoleApplication1.vshost.exe
	Keine Kontextrichtlinien vorhanden.
	 
	Name:ConsoleApplication1.vshost.exe
	Keine Kontextrichtlinien vorhanden.
	 True
	Name:AppDomain #2
	Keine Kontextrichtlinien vorhanden.
	 False
	[1] mscorlib
	[2] Microsoft.VisualStudio.HostingProcess.Utilities
	[3] System.Windows.Forms
	[4] Microsoft.VisualStudio.HostingProcess.Utilities.Sync
	[5] System
	[6] Microsoft.VisualStudio.Debugger.Runtime
	[7] mscorlib.resources
	[8] vshost
	[9] System.Core
	[10] System.Xml.Linq
	[11] System.Data.DataSetExtensions
	[12] System.Data
	[13] System.Xml
	[14] WindowsBase
	[15] ConsoleApplication1
	[16] System.Drawing
	[17] Accessibility
	[18] DomainManager
	[19] IPlugin
	 
	Name:AppDomain #2
	Keine Kontextrichtlinien vorhanden.
	 
	[1] mscorlib
	[2] Microsoft.VisualStudio.HostingProcess.Utilities
	[3] ConsoleApplication1
	[4] Plugin

Mit dem AppDomains sind wir  unserem Ziel, einen robusten und flexiblen Plugin-Manager zu schreiben, einiges näher gekommen. Als nächstes gilt es die Hostanwendung vor Exceptions der Plugins zu schützen. Das heißt ein Plugin soll nicht die komplette Anwendung crashen.

Eine Möglichkeit wäre für die Plugins einen Wrapper oder einen Proxy zu erstellen.   Dazu müssen die Plugin-Funktionen mit einem Try/Catch Block versehen werden. Bei einer Exception könnte man auch ein Event auslösen.

Beispiel Code:

        public interface IPlugin
	{
	    public void Execute();
	    public void Connect(object info);
	}    
	public class PluginProxy : System.MarshalByRefObject, IPlugin
	{
	    private IPlugin m_IPlugin;
	 
	    //Other Code for Events etc. 
	    void Init(string assemblyName, string typeName)
	    {
	        m_IPlugin = (IPlugin)Activator.CreateInstance(assemblyName, typeof(IPlugin));
	    }
	    public void Execute()
	    {
	        try
	        {
	            m_IPlugin.Execute();
	        }
	        catch (Exception ex)
	        {
	            //Event feuern
	            this.OnException(ex);
	        }
	    }
	    public void Connect(object info)
	    {
	        try
	        {
	            m_IPlugin.Connect(info);
	        }
	        catch (Exception ex)
	        {
	            //Event feuern
	            this.OnException(ex);
	        }
	    }
	}

Damit kann man schon einige Exceptions abfangen. Allerdings sind wir nicht in der Lage auf interne Ereignisse der Plugins zu reagieren. Das heißt,  wenn ein Plugin einen Timer oder ein FileSystemWatcher-Ereignis abonniert (könnte genauso irgendein anderes Ereignis sein) das eine Exception auslöst, können wir diese nicht abfangen.

Die Host Anwendung wird also abstürzen wenn eines der internen Plugin –Ereignisse eine Exception wirft. Da nützen uns auch die AppDomains nichts, die die Plugins isolieren. Zumindest kenne ich in diesem Zusammenhang keine Lösung, die das Abstürzen der Hostanwendung verhindern würde.  Auch im Internet habe ich dazu nichts gefunden.

Jedoch gibt es einen anderen Lösungsansatz, den ich aufgegriffen habe.

„In den Versionen 1.0 und 1.1 von .NET Framework wird eine unbehandelte Ausnahme, die in einem anderen als dem Hauptanwendungsthread auftritt, von der Laufzeit abgefangen und führt daher nicht zum Beenden der Anwendung. Das UnhandledException-Ereignis kann also ausgelöst werden, ohne dass die Anwendung beendet wird. Ab .NET Framework Version 2.0 wurde diese Option für unbehandelte Ausnahmen in untergeordneten Threads entfernt, da solche stillen Fehler zu einer Verschlechterung der Leistung, beschädigten Daten und Lockups führten, was insgesamt schwer zu beheben war.“ (Ausnahmen in verwalteten Threads, 22.08.10, http://msdn.microsoft.com/de-de/library/ms228965(v=VS.100).aspx)

Die Idee ist also, die Anwendung bei unbehandelten Ausnahmen auf das Verhalten der Common Language Runtime auf Versionen 1.0 und 1.1 zurück zu setzen, damit der Haupt-Thread weiter läuft.

Das macht man indem man mit der Funktion ICLRPolicyManager::SetUnhandledExceptionPolicy die gewünschte Policy setzt. Leider stellt der AppDomainManager keine passende managed Funktion bereit. Deshalb kann man das Runtime verhalten in der Application.config wie folgt setzen.

        <?xml version="1.0" encoding="utf-8" ?>
	<configuration>
	  <runtime>
	    <legacyUnhandledExceptionPolicy enabled="true" />
	  </runtime>
	</configuration>

Dabei muss unbedingt beachtet werden, dass sich der Debugger mit der gesetzten Abwärtskompatibilität falsch verhält.

Um zu sehen ob das Programm auch erwartungsgemäß funktioniert, muss man das kompilierte Programm  manuell, ohne den Debugger bzw. Visual Studio 2008 starten.

Für reine Debugging Zwecke, die nicht die Plugins betreffen, würde ich die Abwärtskompatibilität temporär entfernen.

Ein Beispiel bzgl. der gesetzten Abwärtskompatibilität wird in diesem Blog Eintrag demonstriert. Allerdings finde ich, dass der Autor die Demo unnötig mit seiner eigenen Exception Klasse aufgebläht hat. Wie man das viel einfacher und mit weniger Code machen kann, sieht man in meiner PluginManger bzw. PluginDomain Klasse.

3. AppDomain und Plugin erstellen

Bevor man aber überhaupt die Plugin-Isolation machen kann, muss man sich Gedanken machen wie man die Plugins in Kombination mit den AppDomains handhabt.

Meine Idee ist für jedes Plugin eine separate AppDomain zu erstellen. Das Erstellen der AppDomain für das Plugin soll eine Klasse für uns erledigen die PluginDomain heißt. Es handelt sich dabei um eine generische Klasse, damit man den Plugin-Typ angeben kann. Den Generischen Typ brauchen wir später beim Erstellen der Plugin-Instanz und um die Instanz speichern. Das macht das Arbeiten mit dem Plugin einfacher, da der „Cast“ Vorgang weg fällt.

Die Klasse wird mit dem Konstruktor PluginDomain(string assemblyPath) initialisiert, wobei der Parameter assemblyPath den Pfad des Plugins enthält. Im Konstruktor wird die AppDomain mit Hilfe der Klasse AppDomainSetup konfiguriert und erstellt.

        //Beim erstellen der AppDomain wird die Funktion GetInterfaceTypes aufgerufen
	appDomainSetup.AppDomainInitializer = new AppDomainInitializer(GetInterfaceTypes);
	//Parameter der Funktion übergeben, Pfad des Plugins und nach welchem Interface gesucht werden soll
	appDomainSetup.AppDomainInitializerArguments = new string[] { assemblyPath, typeof(T).ToString() };

Der Eigenschaft AppDomainSetup.AppDomainInitializer  wird der delegate GetInterfaceTypes zu gewiesen. Wenn die AppDomain initialisiert wird, wird die Methode GetInterfaceTypesu in der erstellten AppDomain ausgeführt. Wir wollen in die erstellte AppDomain die Plugin-Assembly laden. Theoretisch könnte man dieses Vorhaben auch mit der Funktion AppDomain.DoCallBack realisieren.

        /// <summary>
	/// Diese Methode sucht in der übgergebenen Datei die erste Klassen die das übergebene
	/// Interface implementieren.
	/// </summary>
	/// <param name="args">Der erste Parameter ist der Datepfad der Datei die nach dem gesuchten 
	/// Interface durchsucht werden soll.
	/// Im zweiten Parameter wird das Interface übergeben.</param>
	private static void GetInterfaceTypes(string[] args)
	{
	    AppDomain appDomain = System.AppDomain.CurrentDomain;
	 
	    //Laden wir die Plugin Assembly in unsere AppDomain
	    System.Reflection.Assembly assembly = System.Reflection.Assembly.LoadFrom(args[0]);
	 
	    String pluginType = String.Empty;
	 
	    foreach (Type type in assembly.GetTypes())
	    {
	        //Check only Public Types
	        if (type.IsPublic)
	        {
	            //Check only not abstract Types
	            if (!type.IsAbstract)
	            {
	                //look up the interface
	                Type typeInterface = type.GetInterface(args[1], true);
	                if (typeInterface != null)
	                {
	                    //return the type which implements our interface
	                    pluginType = type.FullName;
	                }
	            }
	        }
	    }
	 
	    //Daten über die Methode SetData setzen, weil der AppDomainInitializer-Delegate
	    //kein Rückgabewert hat. Mit GetData können die Daten wieder abgeholt werden
	    //http://msdn.microsoft.com/en-us/library/37z40s1c.aspx
	    appDomain.SetData(_GetDataPluginTypes, pluginType);
	}

In der Methode GetInterfaceTypes welche unsere CallBack Funktion ist, durchsuchen wir die Assembly nach dem Typ der unser Plugin-Interface implementiert. Der Vorgang ist ähnlich diesem (siehe GetModul). Denn gefunden Typen wollen wir zurückgeben damit wir eine Instanz vom Plugin erstellen können. Weil der AppDomainInitializer-Delegate kein Rückgabewert besitzt müssen wir mit der Methode AppDomain.SetDataund GetData arbeiten. Mit diesen Methoden kann man Daten zwischen AppDomains transportieren.

Bei diesen Methoden muss man folgendes beachten: Angenommen wir würden das Objekt typeInterface mit SetData in unserer Plugin AppDomain verschicken und in der Default AppDomain wieder mit GetData abholen, hätte das folgenden Effekt. Der Typ typeInterface ist in der Default AppDomain nicht bekannt, da die Plugin-Assembly nur in der Plugin-AppDomain geladen ist. Die Plugin-Assembly wird also auch in die Default AppDomain geladen.

Nach dem die AppDomain erstellt und die GetInterfaceTypes Methode ausgeführt wurde können wir den Plugin-Typ mit AppDomain.GetData() abholen.

Jetzt wo wir wissen welcher Typ unser Plugin implementiert, erstellen wir mit CreateInstanceFromAndUnwrap die Plugin-Instanz. Die Instanz speichern wir in die generische Eigenschaft die logischerweise Instance heißt.

	_Instance = this._appDom.CreateInstanceFromAndUnwrap(_AssemblyPath, pPluginType) as T;

Noch bevor die Instanz erstellt wird, haben wir von der AppDomain das UnhandledException Event angemeldet.

	_appDom.UnhandledException += new UnhandledExceptionEventHandler(AppDom_UnhandledException);

Diese tritt auf, wenn eine Ausnahme in der AppDomain nicht abgefangen wurde. Dieses Event ist dafür gedacht, dass man den Fehler mitloggen kann. Nach dem das Event gefeuert wurde, wird das Programm normalerweise beendet.

Da wir aber bei unserem Programm die Abwärtskompatibilität aktiviert haben, wird das Programm weiter ausgeführt. Wenn eine Exception in einer AppDomain bzw. in einem Plugin auftritt, werden wir dieses Plugin entfernen, in dem wir die AppDomain entladen in dem es sich befindet. Die Default AppDomain kann nicht entladen werden.

Wir werden in der Funktion AppDom_UnhandledException eine Key setzen damit wir im Pluginmanager wissen, welche AppDomain wir entladen müssen. (Wir machen also fast das gleiche wir hier, nur einfacher.)
Über die UnhandledExceptionEventArgs können wir auf das Exception Objekt zugreifen.  In dieses Tragen wir über

        System.AppDomain appDomain = sender as System.AppDomain;
	Exception exception = e.ExceptionObject as Exception;
	 
	exception.Data.Add("PluginDomain", this);

das Plugin ein, das die Exception ausgelöst hat. Der PluginManager hat ebenfalls von alle PluginDomains das UnhandledException Event registriert.

Er führt darin einige Prüfungsroutinen aus, um auch sicher  zu stellen dass die Exception auch von einem unserer Plugins geworfen wurde.

Anschließend versucht er sie zu entladen.

        if (appDomain.Equals(plugindomain.AppDomain) && 
            plugindomain.AppDomain.IsDefaultAppDomain().Equals(false))
	{
	    new Thread(delegate()
	    {
	        try
	        {
	            Console.WriteLine("Unloading the offending application domain...");
	 
	            AppDomain.Unload(appDomain);
	            appDomain = null;
	            plugindomain = null;
	        }
	        catch (System.Threading.ThreadAbortException abortexception)
	        {
	            //(AppDomain..::.Unload beendet Threads mit Thread..::.Abort
	            //http://msdn.microsoft.com/de-de/library/cyayh29d.aspx
	 
	            appDomain = null;
	            plugindomain = null;
	            System.Console.WriteLine(abortexception);
	        }
	        catch (CannotUnloadAppDomainException unloadexception)
	        {
	            System.Console.WriteLine(unloadexception);
	        }
	        catch (Exception ex)
	        {
	            Console.WriteLine(ex.ToString());
	        }
	        finally
	        {
	            OnPluginChangedEvent(new PluginManager.PluginChangedArgs(plugindomain,
	             ChangedAction.Remove));
	        }
	 
	        Console.WriteLine("Unloaded the offending application domain...");
	 
	}).Start();

Beim Entladen gilt folgendes zu beachten.

Wurde eine AppDomain entladen, sind alle darin enthaltenen Typen nicht mehr verwendbar. Ein Zugriff auf einen entladenen Typ würde eine AppDomainUnloadedException auslösen. Die entladene AppDomain gibt alle geladenen Assemblies wieder frei.

Ist in einer AppDomain ein oder noch mehrere Threads aktiv wird eine ThreadAbortException geworfen. Das bedeudet, dass beim „unloaden“ einer AppDomain eine ThreadAbortException auftreten kann. Mehr Informationen siehe dazu in der MSDN. Nachdem entladen der AppDomain macht sich der GC auf, um die Ressourcen von der AppDomain frei zu geben.

Am besten ihr schaut euch noch die Demo an. In der Demo ist ein Plugin vorhanden, das via Timer eine ArgumentNullException wirft. Die Domain wird anschließend entladen. Die aktivierte Abwärtskompatibilität verhindert, dass die Host-Anwendung abstürzt. Über eine Rückmeldung freue ich mich.

Source Code PluginManager


Page 1 of 3123

C# Anwendung Plugin fähig machen

By
on Regards: .NET Framework; C#;

In diesem Beispiel sieht man wie man ein einfaches Plugin-System realisiert. Demonstriert wird das ganze anhand eines „Mathematikprogramms“.
Das Mathematikprogramm lädt zu Beginn alle Plugins. Wenn der Benutzer z.B. die Wurzelfunktion aufruft mit „sqrt(3)“ durchsucht das Mathematikprogramm alle Plugins und schaut nach ob eines der Plugins die Funktion „sqrt“ berechnen kann. Der Pluginname verrät, für welche Mathematikfunktion das Plugin zu ständig ist. Wurde kein zuständiges Plugin gefunden, wird die Meldung „unbekannte Funktion“ ausgegeben.
Gibt der Benutzer als Eingabe z.B. „sqrt(4)“ ein, wird dem Plugin, das die Funktion „sqrt“ implementiert, „sqrt(4)“ übergeben. Somit weiß das Plugin was es überhaupt berechnen soll. Zurück gibt es das berechnete Ergebnis  der Eingabe.
Damit wir wissen wie die Plugins „aussehen“, also wie die Funktionen, Eigenschaften heißen, müssen wir ein Interface für die Plugins definieren. Ansonsten könnte es passieren (wenn verschiedene Entwickler die Plugins erstellen) dass die zu berechnende Funktion einmal „Berechne(String input)“ oder „Calculate(int input)“ heißt.
Wenn jeder das Interface des Plugins implementiert, kann es nicht zu solchen Problemen kommen. Unser Plugin-Interface sieht wie folgt aus.
            /// <summary>
	    /// Definiert das "Aussehen" unseres Plugins
	    /// </summary>
	    public interface IPlugin
	    {
	        /// <summary>
	        /// Name des Plugins
	        /// </summary>
	        String Name { get; set; }
	 
	        /// <summary>
	        /// Beschreibung des Plugins
	        /// </summary>
	        String Description { get; set; }
	 
	        /// <summary>
	        /// Berechnet die Eingabe
	        /// </summary>
	        /// <param name="pInput">Eingabe die ausgewertet werden soll.</param>
	        /// <returns>Gibt das berechnete Ergebnis zurück</returns>
	        String Calc(String pInput);
	    }

Das bedeutet, dass jeder, der unser Interface implementiert zwei Properties “Name” und “Description” definiert und zu guter Letzt eine Funktion “Calc”.

Im Namen speichern wir den Funktionsnamen der Mathematischen Funktion. Als z.B. „sqrt“. In der Funktion Calc berechnen wir den Übergabewert pInput und geben das Ergebnis als String zurück. Natürlich wäre eine andere Mathematische Funktion wie z.B. Modulointeressanter zu implementieren.Als nächstes gilt es die Mathematik Klasse MathCore zu erstellen, welche die Plugins lädt und erstellt.

Als erstes wird die Funktion LoadPlugins aufgerufen. Dieser übergeben wir den Dateipfad in dem sich die Plugins befinden.

Als nächstes gilt es die Mathematik Klasse MathCore zu erstellen, welche die Plugins lädt und erstellt.
Als erstes wird die Funktion LoadPlugins aufgerufen. Dieser übergeben wir den Dateipfad in dem sich die Plugins befinden.

        public void LoadPlugins(String pPluginPath)
	{
	            String[] files = Directory.GetFiles(pPluginPath);
	 
	            foreach (String file in files)
	            {
	                FileInfo fileInfo = new FileInfo(file);
	 
	                if (fileInfo.Extension.Equals(".dll"))
	                {
	                    Dictionary<string, object> dictionary = GetModul(file, typeof(IPlugin.IPlugin));
	                    foreach (var a in dictionary)
	                    {
	                        Plugins.Add(a.Key,a.Value);
	                    }
	 
	                }
	 
	            }
	}

Es werden alle Dateien aus dem Verzeichnis gelesen und Dateien mit der Dateiendung „.dll“ gefiltert. Ist eine Datei eine „.dll“, wird die Funktion GetModul(file, Type) aufgerufen. Diese lädt die Plugins in unsere Instanz hinein. Sehen wir uns dazu diese Funktion genauer an.

        private static Dictionary<string, object> GetModul(string pFileName, Type pTypeInterface)
	{
	            //Hier speichern wir unsere Plugins
	            Dictionary<string, object> interfaceinstances = new Dictionary<string, object>();
	 
	            //Loads an assembly given its file name or path.
	            Assembly assembly = Assembly.LoadFrom(pFileName);
	 
	            // http://msdn.microsoft.com/de-de/library/t0cs7xez.aspx
	            // Assembly Eigenschaften checken
	            foreach (Type type in assembly.GetTypes())
	            {
	                if (type.IsPublic) // Ruft einen Wert ab, der angibt, ob der Type als öffentlich deklariert ist. 
	                {
	                    if (!type.IsAbstract)  //nur Assemblys verwenden die nicht Abstrakt sind
	                    {
	                        // Sucht die Schnittstelle mit dem angegebenen Namen. 
	                        Type typeInterface = type.GetInterface(pTypeInterface.ToString(), true);
	 
	                        //Make sure the interface we want to use actually exists
	                        if (typeInterface != null)
	                        {
	                            try
	                            {
	 
	                                object activedInstance = Activator.CreateInstance(type);
	                                if (activedInstance != null)
	                                {
	                                    interfaceinstances.Add(type.Name, activedInstance);
	                                }
	                            }
	                            catch (Exception exception)
	                            {
	                                System.Diagnostics.Debug.WriteLine(exception);
	                            }
	                        }
	 
	                        typeInterface = null;
	                    }
	                }
	            }
	 
	            assembly = null;
	 
	            return interfaceinstances;
	}

Der Funktion wird der Pfad der Assembly übergeben, die nach Plugins durchsucht werden soll. Zusätzlich muss man den Typ des Plugins übergeben.

Wir laden die Assembly mit Assembly.LoadFrom() damit wir Informationen aus ihr lesen können (Stichwort Reflection). Mit der Funktion GetTypes() erhalten wir alle Typen (Klassen), die in der Assembly gespeichert sind. Als nächstes führen wir ein paar Routineprüfungen durch wie z.B. ob der Typ public ist und nicht abstrakt.

Danach „suchen“ oder besser gesagt holen wir mit der Methode GetInterface() das  Plugin-Interface falls der gerade iterierende Typ das Plugin implementiert hat. Falls kein Interface implementiert wurde ist der Rückgabewert NULL. Deshalb wird als nächstes überprüft ob der Rückgabewert ungleich NULL ist. Haben wir einen Rückgabewert erhalten, erstellen wir mit Activator.CreateInstance eine Instanz. Diese speichern wir dann in unser Dictionary.

So werden die Plugins Schritt für Schritt in unsere Anwendung geladen. Wurden alle Dateien auf Plugins überprüft, kann man mit der Funktion public string Calc(string pInput) Mathematikfunktionen aufrufen bzw. ausführen.

Ruft man _MathCore.Calc(“sqrt(3)”) auf, wird in der Funktion Calc der Input String interpretiert damit man weiß, welche Mathematikfunktion aufgerufen wird. In diesem Fall wäre das „sqrt“. Anschließend wird nach dem passenden Plugin mit der Funktion GetPlugin gesucht. Wurde es gefunden, wird der Input String an das Plugin weitergereicht und die Funktion Calc vom Plugin aufgerufen.

Wurde kein Plugin gefunden, wird “Funktion nicht gefunden”  zurückgegeben.

Wie gesagt handelt es sich hierbei um ein sehr einfaches Plugin-System. Das Beispiel verwendet die Plugins zwar nur in einer Konsolen Applikation, trotzdem sollte es kein Problem sein, das Ganze in WPF oder WinForms umzusetzen.

Bei einem ausgereiften Plugin-System gibt es Features wie z.B., dass die Plugins auf bestimmte Teile des Host System zugreifen können (z.b auf ein Control), dass Plugins wiederum Plugins enthalten können oder, dass die Plugins in einer Art Sandbox (Stichwort AppDomain ) gehostet werden damit kein schädlicher Code ausgeführt werden kann usw. usf.

Seit .NET 2< wurden eine Reihe neuer Techniken mitgeliefert die das Erstellen von Plugins vereinfachen soll.

Anbei ein paar Links:

http://www.mycsharp.de/wbb2/thread.php?threadid=34472
http://www.codeproject.com/KB/cs/pluginsincsharp.aspx

Beispiel Projekt herunterladen


Page 1 of 3123