Laden...

Fragen, Diskussion, Kritik zu Projekt ".NET Applikationsserver"

Erstellt von wdb.lizardking vor 16 Jahren Letzter Beitrag vor 9 Jahren 142.461 Views
699 Beiträge seit 2007
vor 15 Jahren

Hallo Rainbird,

ich bekommen einen Fehler, den ich irgendwie nicht ganz zuordnen kann.

Ich bin beigegangen und habe die ganzen Dateien der API in eine eigene DLL kopiert und verwende die in meinem Programm.
Wenn ich nun einen Zugriff auf den App-Server machen will, dann bekomme ich den Fehler in Anhang.

Hast Du gerade eine Idee, was ich da falsch mache?

Ich kann Dir auch gerne die komplette Solution mal per Mail schicken, wenn Du darin den Fehler schneller finden kannst.

Grüße Stephan

3.728 Beiträge seit 2005
vor 15 Jahren
Lokalisierung

Hallo Stipo,

er findet offenbar keine Ressourcen für die aktuelle Kultur. Die API verwendet Ressourcen zur unterstützung von Mehrsprachigkeit. Die Standardressource ist in Englisch. Für jede weitere Sprache muss eine separate Ressourcendatei angelegt werden. Die Ressourcendateien heißen:

LanguageResource.resx (Standard)
LanguageResource.de.resx (Deutsche Lokalisierung)

Beim kompilieren werden wird die Standardressource in die Assembly eingebettet und für die einzelnen Sprachen werden Setelittenassemblies erzeugt, die bei Bedarf automatisch nachgeladen werden. Für Deutsch liegt die Satelittenassembly im Unterverzeichnis "de". Du musst dieses Unterverzeichnis samt Inhalt deshalb mitkopieren. Es ist aber nicht nötig einen Verweis darauf aufzunehmen. Die CLR muss den "de" Ordner einfach nur finden können.
An irgendeiner Stelle kann das System den "de" Ordner bzw. die Satelittenassmbly darin, nicht finden. Vermutlich fehlt er Serverseitig.

Wenn Du innerhalb der Projektmappe einen Verweis auf die API-Assembly einfügst, werden die Ressourcen automatisch mitkopiert.

699 Beiträge seit 2007
vor 15 Jahren

Hallo Rainbird,

danke erstmal für die Infos. Aber der de Ordner ist mit kopiert und darin ist auch die Satellitenassembly enthalten. Daher kann es dann sicher nicht daran liegen.
Ich denke mal, das ich irgendwo anders einen Fehler gemacht habe.

Muss mir das Kapitel über Localisierung im Buch nochmal vornehmen und meinen Code noch mal genau unter die Lupe nehmen.

Grüße Stephan

S
167 Beiträge seit 2008
vor 15 Jahren

Hallo,
nimmst du z.b. bei der Products Tabelle in den Spalten "Created/Changed"By generell nvarchar oder nur in dem Beispiel weil es keine User/sonstwas Tabelle gibt?

3.728 Beiträge seit 2005
vor 15 Jahren
CreatedBy und ChangedBy

Hallo Sebi,

ich persönlich verwende grundsätzlich nvarchar (also einen String-Typ) und schreibe dort den Windows-Anmeldenamen inklusive Domäne rein. Das tue ich auch in Projekten, die Benutzereinstellungen in einer eigenen Users-Tabelle verwalten. Ich mache das, damit ich auch bei sehr alten Datensätzen und bei importierten Datensätzen sehen kann, wer diese erstellt hat. Würde ich den UserID eintragen, könnte ich niemals einen Benutzer löschen, sondern müsste die quasi "gelöschten" Benutzer immer über ein bit-Feld wegfiltern. Das wäre zwar noch zu verkraften, aber was ist mit Daten, die von einer anderen Applikation kommen, die eine eigene Benutzerverwaltung hat?
Wenn es ein String ist, ist auf jeden Fall nachvollziehbar, wer der Ersteller eines Datensatzes ist.

Anders wäre es z.B., wenn ich eine Verkaufschance einem Vetriebsmitarbeiter zuordnen müsste. Da würde ich die UserID nehmen.

L
45 Beiträge seit 2006
vor 15 Jahren
TableAdapter aus generierter Table entfernen

Hallo Rainbird,

erstmal allen schöne Weihnachten und einen guten Rutsch ins neue Jahr.

Da ich mich gerade auch mit Deinem Beispiel hier beschäftige, habe ich da mal eine Frage?

Wie Du auch in Deinem Block beschreibst, ist ja der TableAdapter das eigentliche Problem beim generieren durch den Designer.
Wenn ich aber nun nach Erzeugung einer Table durch den Designer (Table aus dem Server-Explorer ziehen), den Adapter, die erzeugte Appconfig Datei, sowie die erzeugte Settingsdatei lösche erhalte ich doch meines Erachtens das gleiche Ergebniss wie durch das Eintippen der Table im Designer.

Liege ich da falsch und es wird doch noch mehr generiert?

ps.: ist schon eine weile her, dass Du mir beim Einstieg in die Programierung sehr geholfen hast und ich hatte danach leider keine weiteren Probleme, die nicht auch selber hier lösen konnte und so wollte ich Dir jetzt halt noch einmal für Deine Hilfestellungen und Beiträge hier im Forum danken.

3.728 Beiträge seit 2005
vor 15 Jahren

Wie Du auch in Deinem Block beschreibst, ist ja der TableAdapter das eigentliche Problem beim generieren durch den Designer.

Der Beitrag in meinem Blog bezieht sich allerdings auf Visual Studio 2005. Visual Studio 2008 hat nämlich ein tolles neues Feature. Man kann die TableAdapter nämlich bei Visual Studio 2008 in ein separates Projekt auslagern. Damit werden die TableAdapter für n-Tier-Anwendungen salonfähig. Die DataSet-Klassen kann man dann bequem in einem Contracts-Projekt unterbringen, aber den TableAdapter-Code in den BusinessComponents. Wenn Du Visual Studio 2008 einsetzt, kannst Du allen per Drag&Drop aus dem Server-Explorer generierten Code einfach verwenden ohne irgendwas umbauen zu müssen. Du hast dann eine fertige DAL, da Du den TableAdaptern auch eigene Queries mit Parametern und allem drum und dran zufügen kannst. Seit Visual Studio 2008 sind TableAdapter also cool! Ich sollte das vielleicht mal bloggen 😉.

Liege ich da falsch und es wird doch noch mehr generiert? Es wird noch mehr generiert.

Vielen Dank für Dein positives Feedback! Dir auch ein Gutes Neues Jahr.

Gruß

Rainbird

365 Beiträge seit 2004
vor 14 Jahren

Hi Rainbird,

ich würde das Projekt gerne auf MySQL portieren. Kannst du mir sagen, ob dies prinzipiell mit relativ geringem Aufwand möglich wäre, oder ob grundsätzliche Schwierigkeiten zu erwarten wären. Ich habe gesehen, dass du in der App.config bereits das Ändern des Datenbankproviders vorgesehen hast.

In den SQL Statements, die du im Setup verwendest, finden sich jedoch diverse Befehle, die ich nicht verstehe und die vermutlich auch MS SQL spezifisch sind.

z.B.

(PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)

Von daher weiß ich jetzt nicht, ob eine Portierung auf MySQL überhaupt möglich ist.

Gruß

Christoph

3.728 Beiträge seit 2005
vor 14 Jahren
MySQL Portierung

Hallo bvsn,

das sollte funktionieren. Das SQL-Skript zur Erstellung der Datenbank habe ich vom SQL Management Studio erzeugen lassen, deshalb stehen da so viele Zusatzattribute drin. Vieles davon ist optional und kann weggelassen werden. Du die SQL-Statements (auch die in den Geschäftskomponenten) eben durchsehen und ggf. an den MySQL-Dialekt anpassen (z.B. TOP heißt bei MySql LIMIT etc.). Das stelle ich mir aber nicht schwer vor. So viele Statements sind es ja nicht.

Wo ich mir nicht sicher bin, ist die Unterstützung von System.Transactions (Verteilte Transaktionen) seitens des MySQL-ADO.NET-Providers. Da solltest Du Dich vorher schlau machen.

Die Datenzugriffskomponente arbeitet mit ProviderFactory und kann deshalb sehr einfach auf andere Provider umgestellt werden.

Wenn Windows.Forms unter mono vernünftig funktionieren würde (was es nicht tut), könnte man das Beispielprojekt auch auf mono portieren und dann unter Linux laufen lassen.

365 Beiträge seit 2004
vor 14 Jahren

Hi Rainbird,

das hört sich nach vertretbarem Aufwand an. Ich werde mich daran mal versuchen. Was den mono Client angeht. Das fände ich auch sehr interessant. Allerdings würde ich die Entwicklung dann auch eher mit Gtk# angehen. Windows.Forms ist bei den mono Entwicklern nicht gerade im Fokus.

Gruß

Christoph

3.728 Beiträge seit 2005
vor 14 Jahren
Gtk#

Hallo bvsn,

GTK# habe ich mir auch schon angesehen. Das hat nicht annähernd die Power von Windows.Forms. Es gibt kein vernünftiges Grid-Steuerelement, was für mich ein K.O.-Kriterium darstellt. DataBinding fehlt komplett. Und und und. Vor zehn Jahren hätte mich GTK# vielleicht noch beeindruckt, aber jetzt fühle ich mich da in die GUI-Steinzeit zurückversetzt. Da müssen die Linux-Buben noch ein paar Briketts drauflegen. Java Swing hat da mittlerweile mehr auf dem Kasten (obwohl da auch noch Welten dazwischen sind).

365 Beiträge seit 2004
vor 14 Jahren

Hi Rainbird,

im Grunde stimmt das. Vieles wird sich hoffentlich mit GTK 3 bessern. Was aber das Databinding angeht, ich glaube das ist mittlerweile recht brauchbar. Siehe hierzu unter anderem: http://tirania.org/blog/archive/2008/Mar-26-1.html

Vieles muss man sich leider zusammensuchen oder Steuerelemente selbst schreiben, was natürlich keine triviale Aufgabe ist. Von medsphere gibt es beispielsweise eine Reihe eigener Steuerelemente u.a. ein GridView:

http://medsphere.org/projects/widgets/wiki/GridView

Naja, will jetzt hier nicht den Thread versauen. Du hast schon recht, dass Gtk lange nicht den Komfort von Windows.Forms bietet aber wenn man wirklich das Ziel hat eine Clientanwendung für Linux zu entwickeln, würde ich trotzdem eher auf ein natives Toolkit setzen.

Gruß

Christoph

365 Beiträge seit 2004
vor 14 Jahren

Hi Rainbird,

ich habe das mit MySQL gerade mal ausgetestet. Das klappt soweit eigentlich ganz gut und es ist mir auch bereits möglich Artikel zu speichern. Allerdings kennt MySQL keine GUIDs, weshalb man dort als Workaround für den DB Typ char(36) nehmen muss. Nun ist es aber so, dass unter anderem beim Laden eines Artikels wieder in eine GUID gecastest wird.

Das habe ich so umschiffen können:


//Guid productID = (Guid)((DataRowView)_binding.Current)["ProductID"];
Guid productID = new Guid((((DataRowView)_binding.Current)["ProductID"]).ToString());
                

Momentan habe ich aber noch das Problem, dass ich nicht in die Detailansicht eines Artikels springen kann. Bzw, rein komme ich schon, aber der Artikel wird nicht geladen.

Ich habe bereits versucht dahinter zu steigen, wo genau das Problem liegt, bin aber noch nicht zum Kern durchgedrungen.

Was ich weiß ist folgendes:

In der nachfolgenden Methode wird die ProductID in einer Parameterliste eingefügt, die anschließend im Command benutzt wird. Dieser Parameter wird als DbType.Guid angelegt, was MySQL nicht kennt und vermutlich dazu führt, dass ich keinen Datensatz zurückgeliefert bekomme.


public ArticleMasterDataSet.ProductsDataTable GetProduct(Guid productID)
{ 
        // Parameterliste erstellen
        IList<DbParameter> parameters=new List<DbParameter>();
          parameters.Add (DataAccess.CreateParameter (ProductManager._dbFactory, "@productID", DbType.Guid,ParameterDirection.Input, productID));
    		
    		
        // Tabelle für Ergebnisrückgabe erzeugen
        ArticleMasterDataSet.ProductsDataTable result=new ArticleMasterDataSet.ProductsDataTable();

        // SQL-Code
        string sql="SELECT * FROM Products WHERE ProductID=@productID";
  			
        // Datenbank abfragen
        DataAccess.Query (ProductManager._dbFactory, result, ProductManager._connectionString,sql,parameters);
        	
       // Abfrageergebnisse zurückgeben
       return result;
}

Soweit so gut. Was ich bereits alles probiert habe ist Folgendes:

  • Keinen Parameter zu der Parameterliste hinzufügen und stattdessen

string sql="SELECT * FROM Products WHERE ProductID='" + productID.ToString() +"'";

verwenden.

  • Den Parameter auf DbType.String umstellen und entsprechend die productID als String übergeben.

Beide Workarounds führten dann allerdings zu folgender Exception:

Der für die Deserialisierung eines Objektes mit dem Typ "MySql.Data.Types.MySqlConversionException" erforderliche Konstruktor wurde nicht gefunden.

System.Runtime.Serialization.SerializationException: Der für die Deserialisierung eines Objekts mit dem Typ "MySql.Data.Types.MySqlConversionException" erforderliche Konstruktor wurde nicht gefunden. ---> System.Runtime.Serialization.SerializationException: Der für die Deserialisierung eines Objekts mit dem Typ "MySql.Data.Types.MySqlConversionException" erforderliche Konstruktor wurde nicht gefunden.
bei System.Runtime.Serialization.ObjectManager.GetConstructor(Type t, Type[] ctorParams)
bei System.Runtime.Serialization.ObjectManager.CompleteISerializableObject(Object obj, SerializationInfo info, StreamingContext context)

Die Exception wird innerhalb der Methode DataAccess.Query beim Aufruf von adapter.Fill(table) geworfen.

Hast du hier noch irgendwie einen hint für mich?

Gruß

Christoph

3.728 Beiträge seit 2005
vor 14 Jahren
Guid

Die MySQL-DataAdapter-Implementierung kann den char(36) aus der Datenbank scheinbar nicht automatisch in einen Guid umwandeln (Im DataSet ist der Datentyp der noch auf Guid eingestellt). Wenn Du auch im DataSet den Datentyp auf String umstellst, sollte das gehen.

Konsequenter Weise solltest Du dann alle Parameter und Variaben für IDs von Guid in String umändern.

365 Beiträge seit 2004
vor 14 Jahren

Ok, danke. Da werde ich später noch mal nach schauen. Ich habe noch nie mit typisierten DataSets gearbeitet (wenngleich mir der Nutzen schon bewusst ist) und habe auch kein Visual Studio zur Hand um es einfach zu ändern.

Ich habe aber noch eine andere Frage zu deinem Applikationserver. Ich hoffe das ist ok. Ist mir schon etwas unangenehm, dass ich dich so mit Fragen löcher. 😕 Wie du weißt versuche ich mich gerade zum ersten mal an einer Server/Client Anwendung. Da wird wohl am Ende eine Flasche Wein, Whisky oder Ähnliches fällig 😉

Ich verstehe noch nicht hundertprozentig wo die Instanzen der Service Klassen erzeugt werden. Das die Klasse ServiceFactory ein Proxy Objekt auf das Remote Objekt liefert ist mir klar, aber wo wird das eigentliche Objekt überhaupt instanziert.

Wenn ich mir die Klasse Core ansehe, dann sieht man, dass zuerst die Dienste Locking und Security veröffentlicht werden.


RemotingServices.Marshal(Locking.LockingService.Instance, "Rainbird.AppServer.API.ILockingService");

Das leuchtet mir auch noch hundertprozentig ein. Über LockingService.Instance holst du dir die Instanz und der Marshal Befehl veröffentlicht das Objekt dann.

Aber wie läuft das bei den Business Services?

Ich sehe da dies hier:


 // Alle Dienste durchlaufen
            foreach (WellKnownServiceTypeEntry serviceEntry in RemotingConfiguration.GetRegisteredWellKnownServiceTypes())
            {
                _traceSource.TraceEvent(TraceEventType.Information, 1104, "-> {0}",serviceEntry.ObjectUri);
            }  

Etwas weiter oben steht: RemotingConfiguration.Configure(configFile, true);

Werden denn automatisch alle Einträge der Konfigurationsdatei unter <service> als WellKnownServiceTypeEntry verfügbar und de TraceEvent Befehl erzeugt dann automatisch Objekte und veröffentlicht sie...klingt ziemlich abenteuerlich?! Zumal ich weiter oben noch den Marshal Befehl für die Veröffentlichung der beiden festen Dienste ausgemacht habe...

Kannst du mir noch mal auf die Sprünge helfen?

3.728 Beiträge seit 2005
vor 14 Jahren
Remoting Magic

... und habe auch kein Visual Studio zur Hand um es einfach zu ändern.

Es geht auch ohne Visual Studio ganz einfach. Die Struktur eines typisierten DataSets ist nichts weiter, als ein XSD-Schema. Du musst also nur die XSD-Datei des gewünschten DataSets anpassen und die dann neue DataSet-Klassen generieren lassen. Dazu verwendest Du das Kommandozeilentool XSD.EXE, welches seit .NET 1.0 im Framework-Verzeichnis verfügbar ist. Hilfe zur genauen Anwendung findest Du hier: http://msdn.microsoft.com/de-de/library/x6c1kb0s.aspx.

Aber unter uns, warum verwendest Du nicht Visual C# 2008 Express? Das ist auch kostenlos und enthält den DataSet-Designer sowie jede Menge anderer Dinge, die in SharpDevelop fehlen. Nicht dass ich die Arbeit der SharpDevelop-Community nicht wertschätzen würde, aber es fehlen einfach noch wichtige Dinge, wie z.B. den DataSet-Designer, die Datenbank-Tools oder den WPF-Designer. Die Entwicklungsumgebung ist das wichtigste Handwerkszeug. Da möchte ich stets das Beste haben, was ich mir leisten kann. http://www.microsoft.com/germany/Express/.

Ich habe aber noch eine andere Frage zu deinem Applikationserver. Ich hoffe das ist ok. Ist mir schon etwas unangenehm, dass ich dich so mit Fragen löcher. 😕 Wie du weißt versuche ich mich gerade zum ersten mal an einer Server/Client Anwendung. Da wird wohl am Ende eine Flasche Wein, Whisky oder Ähnliches fällig 😉 Das ist schon in Ordnung.

Wenn ich mir die Klasse Core ansehe, dann sieht man, dass zuerst die Dienste Locking und Security veröffentlicht werden.

Aber wie läuft das bei den Business Services?

Diese beiden Dienste spielen eine Sonderrolle, da sie praktisch zur Infrastruktur des Applikationsservers gehören. Bei diesen beiden Diensten musste ich sicherstellen, dass sie vor allen anderen erzeugt werden. Deshalb habe ich sie selber erzeugt und via Direct Remoting auch manuell veröffentlicht. Bei den anderen Diensten (den Geschäftsdiensten) überlasse ich das der Remoting-Infrastruktur. Diese lädt auch automatisch die nötigen Assemblies nach (Ein Feature, welches ich übrigens bei WCF schmerzlichst vermisse!). Wie Du sehen kannst, sind im Host-Projekt keine Verweise auf die Geschäftsdienst-Projekte vorhanden. Es genügt, den Assembly-Namen bei der Service-Konfiguration in der App.config einzutragen. Remoting lädt die Assembly und deren Abhängigkeiten automatisch und erzeugt je nach Aktivierungstyp Instanzen. Bei Singleton-Aktivierung wird beim ersten Clientzugriff eine einzelne Instanz erzeugt, welche auch alle weiteren Anfragen verarbeitet und solange lebt, bis ihre Lease abgelaufen ist. Singlton aktivierte Dienste können Variablen zwischen zwei Aufrufen speichern, müssen aber threadsicher sein. Bei SingleCall aktivierten Diensten (das sollte der bevorzugte Standard sein) wird für jeden Client-Anfrage eine eigene Instanz erzeugt, welche aber gleich wieder zerstört wird, sobald die aufgerufene Remote-Methode verarbeitet wurde. Die Dienstinstanz lebt also nur für einen Methodenaufruf, skaliert sehr gut und muss nicht threadsicher sein. Du hast also keine Kontrolle, wann eine Instanz erzeugt und wann diese wieder entsorgt wird. Das macht aber gar nichts. Ich will mich damit auch nicht selber rumärgern. Diese Automatik verleiht Remoting eine außerordentliche Robustheit. Wenn es z.B. in einem Geschäftsdienst zu einer unbehandelten Ausnahme kommt, juckt das die anderen Clients nicht die Bohne, da bei jeder Anfrage eine neue Instanz des Dienstes erzeugt wird. Selbst bei Singleton-Aktivierung läuft das System weiter, da Remoting beim nächsten Clientaufruf einfach eine neue Singleton-Instanz erstellt. Wenn also ein Client mal ungültige Daten sendet, die im Geschäftsdienst zu einer Ausnahme führen, merken die anderen Clients davon nichts (Es sei denn es wurden Remote-Events implementiert; Dann würden die anderen Clients plötzlich nicht mehr benachrichtigt!).

Etwas weiter oben steht: RemotingConfiguration.Configure(configFile, true);

Werden denn automatisch alle Einträge der Konfigurationsdatei unter <service> als WellKnownServiceTypeEntry verfügbar und de TraceEvent Befehl erzeugt dann automatisch Objekte und veröffentlicht sie...klingt ziemlich abenteuerlich?! Zumal ich weiter oben noch den Marshal Befehl für die Veröffentlichung der beiden festen Dienste ausgemacht habe...

Kannst du mir noch mal auf die Sprünge helfen?

Das ist viel einfacher, als Du denkst. RemotingConfiguration.Configure parst und verarbeitet die angegebene Konfigurationsdatei (also die App.config). Das bedutetm dass nach Aufruf von Configure der Server bereits voll Einsatzbereit ist und alle in der App.config eingetragenen Dienste automatisch schon als WellKnowServiceTypes registriert sind. Im einfachsten Fall sieht ein Remoting-Host so aus:


public static void Main()
{
    // Remoting-Dienste laden und starten
    RemotingConfiguration.Configure("SomeHost.exe.config", true); 

    // Server solange laufen lassen, bis jemand Enter drückt
    Console.ReadLine();
}

Das reicht völlig aus. Man kann alles in der App.config festlegen. Das sollte man auch. Es sei denn, man hat gute Gründe eine Ausnahme zu machen, wie ich z.B. beim Locking- und beim SecurityService.
Der TraceEvent-Befehl hat mit Remoting überhaupt nichts zu tun. Die Schleife fragt nur die registrierten Dienste ab und schreibst sie mit TraveEvent in die registrierten Protokolle. Standardmäßig auf die Konsole und optional in der App.config zuschaltbar auch ins Windows-Ereignisprotokoll oder in eine Textdatei. Wenn Du Dir die App.config des Servers mal genauer anschaust, wird Du sicher die ganzen Tracing-Einstellungsmöglichkeiten bemerken. Die Textausgabe, die Du im Konsolenfenster des Servers standardmäßig siehst, ist lediglich die Protokollierung, welcher auf die Konsole umgelenkt wurde. Du könntest das auch stattdessen in eine Datei schreiben lassen, oder ins Eventlog, oder auf die Konsole und in eine Datei oder einfach überall hin. Du könntest auch einen eigenen TraceListener schreiben, welcher die Protokolleinträge sammelt und sie Dir jede Stunde per E-Mail schickt. Den Listener müsstest Du dann nur in der App.config eintragen und die Assembly ins Ausführungsverzeichnis des Servers kopieren. Wir arbeiten hier ja komponentenorientiert! Natürlich auch bei Infrastruktur-Komponenten wie der Protokollierung.

Stell Dir vor Dein Kunde ruft an, und sagt, dass die Lagerbuchungen nicht mehr funktionieren, seit der Server neu gestartet worden ist. Du lässt Dir dann die Logdatei schickten und kannst sofort feststellen, ob der Lagerverwaltungsdienst beim korrekt Serverstart registriert wurde, oder nicht. Falls nicht, muss der Fehler in der App.config liegen. Vielen Leuten ist nicht bewusst, dass das .NET Framework ein exzellentes Tracing-System (Tracing = Logging) mitbringt. Du kannst verschiedene Schweregrade von Meldungen oder Fehlern definieren und darüber den Detailgrad der gewünschten Protokollierung einstellen. Außerdem kannst Du Schalter definieren und damit das Tracing für einzelne Module/Bereiche Deiner Anwendung ein- und ausschalten. Komischerweise kennt fast jeder Log4Net (was ein von Java portiertes Tracing-System ist), aber fast niemand das im Framework enthaltene Tracing im Namensraum System.Diagnostics. Und das obwohl es dem Java-Port in nichts nachsteht. Weitere Infos zum Tracing findest Du hier: http://msdn.microsoft.com/de-de/library/system.diagnostics.tracesource(VS.80).aspx

Wenn ich mir die geniale Einfachheit (ohne auf Leistung und Stabilität verzichten zu müssen) von Remoting so ansehe, ist mir auch klar, warum ich mit WCF immer noch nicht nichtig warm geworden bin. Vom Handling her, macht WCF zwei Schritte zurück. Ich will mich nicht selber um das Laden meiner Dienste kümmern und für jeden Typ eine Host-Instanz erstellen, um dessen Start, Ende und ggf. Abbrüche im Fehlerfall ich mich dann auch selber kümmern muss. Ich möchte auch keinen Client-Code von einem Tool generieren lassen und auf dem Client plötzlich mit anderen Typen arbeiten, als auf dem Server. Das macht mit WCF einfach alles keinen Spaß. Da bleibe ich lieber bei Remoting.

365 Beiträge seit 2004
vor 14 Jahren

Aber unter uns, warum verwendest Du nicht Visual C# 2008 Express?

Die Frage ist berechtigt. Was ich an SharpDevelop mag ist, dass es sehr schlank und schnell ist. Ich kann damit auch auf einem schwächeren Rechner problemlos mehrere Projekte parallel spontan öffnen, etwas nachsehen etc. Außerdem ist es Open Source und wie die vielleicht gemerkt hast, bin ich ein großer Open Source Fan und arbeite auch selber hin und wieder an Open Source Projekten mit (http://banshee-project.org/)

Die Funktionen, die du ansprichst habe ich lange nicht vermisst. Ich habe mich erst in den letzten 12 Monaten besonders stark weiterentwickelt. Als ich vor ca. 3 Jahren von VS zu SharpDevelop gewechselt bin, wusste ich noch nicht mal was starke Typisierung bedeutet und wozu ein stark typisiertes DataSet überhaupt gut sein sollte. Heute bin ich glücklicherweise schlauer 😃

Insofern hast du aber vollkommen recht. Mir ist selber schon bewusst geworden, dass ich mich durch den Verzicht auf VS momentan einschränke und mir das Leben unnötig kompliziert mache. Ich hatte die Installation von VS Express ohnehin schon auf meiner ToDo List für die nächsten Tage. Zumindest als Parallel IDE.

Vielen Dank für die detailierten Ausführungen bezüglich dem Remoting. Das erklärt den Code natürlich. Ich hätte nicht gedacht, dass der Aufruf von RemotingConfiguration.Configure einem bereits so viele Aufgaben abnimmt! Daher habe ich verzweifelt gesucht, wo die Instanzen erstellt werden.

Nochmal, vielen vielen Dank!

Gruß

Christoph

365 Beiträge seit 2004
vor 14 Jahren

Hi Rainbird,

ich sehe mir gerade genauer an, wie du das mit den Benutzern gelöst hast. In der Klasse ApplicationServer gibt es ja das CurrentUser Property. Hier wird ein WindowsIdentity Objekt des Users zurückgegeben in dem der aktuelle Thread läuft.

So ruft beispielsweise der LockingService, wenn ein neuer Lock erzeugt wird, den User ab, der den Lock erzeugt und speichert ihn entsprechend in dem LockingInfo Objekt. Soweit so gut.

In dem Zusammenhang sind mir noch einige Dinge unklar. Zum einen, da der LockingService ja als Singleton auf dem Server läuft. Müsste da der Aufruf von ApplicationServer.CurrentUser nicht eigentlich den User zurückliefern unter dem der Server läuft?

Und zum anderen. Ich habe mich mal daran versucht, dass mit dem CurrentUser so nachzubauen. Eigentlich sieht das ja nach einem trivialen Einzeiler aus:


return (WindowsIdentity)Thread.CurrentPrincipal.Identity;

Nun ist es aber so, dass ich hier direkt eine Exception bekomme. Daher meine Frage: Hast du die Identity vorher erst irgendwie an den Thread angehängt (finde aber nix)

Kannst du hierzu noch ein bisschen was erzählen?

Gruß

Christoph

3.728 Beiträge seit 2005
vor 14 Jahren
Remoting Magic

Hallo bvsn,

das ist ganz einfach. Die Remoting-Infrastruktur hängt automatisch die Identität des Aufrufers an den aktuellen Worker-Thread (welcher den entfernten Methodenaufruf abarbeitet). Dazu muss der Schalter "secure" auf Wahr gesetzt und der TcpChannel entsprechend konfiguriert werden. Dies wird über eine Kanaleigenschaft namens "tokenImpersonationLevel" festgelegt, die folgende Werte akzeptiert:*System.Security.Principal.TokenImpersonationLevel.Impersonation *System.Security.Principal.TokenImpersonationLevel.Identification

Identification ist der Standard, welchen auch in verwendet habe. Dabei wird nur die Identität des Aufrufers an den Worker-Thread gehängt, aber der Thread läuft trotzdem unter dem Benutzer des Server-Host-Prozesses. Bei verwenden von Impersonation würde der Worker-Thread tatsächlich die Identität des Aufrufers annehmen und mit dessen Rechten arbeiten. Das möchte man beim Trusted-Server-Model allerdings nicht.

Ich vermute, Du hast versucht die CurrentUser-Eigenschaft lokal (ohne Remoting) zu nutzen. Dann kommt es zu einer Ausnahme, da standardmäßig nur ein leerer GenericPrincipal am Thread hängt, aber kein WindowsPrincipal. Das lässt sich aber mit einer Zeile Code ändern. Du musst nur die Sicherheitsrichtlinie der Anwendungsdomäne auf Windows-Sicherheit einstellen. Dazu fügst Du folgende Zeile in die main-Methode Deiner Anwendung ein:


// Sicherheitsrichtlinie der Anwendungsdomäne auf Windows-Sicherheit einstellen
AppDomain.CurrentDomain.SetPrincipalPolicy(System.Security.Principal.PrincipalPolicy.WindowsPrincipal);

Die Anwendungsdomäne wird nun automatisch an alle neuen Threads den WindowsPrincipal des angemeldeten Windows-Benutzers hängen.

Die Remoting-Sicherheit (falls benötigt) funktioniert trotzdem noch, wenn Du die Sicherheitsrichtlinie nur auf dem Client änderst, aber nicht auf dem Server.

Falls Du die Ausnahme trotz Remoting bekommst, sind Deine TcpChannels nicht richtig konfiguriert.

365 Beiträge seit 2004
vor 14 Jahren

Hallo Rainbird,

gut, ich denke dann weiß ich wo mein Fehler liegt. Ich aktiviere die Instanzen einfach über Activator.GetObject ohne vorher irgendeine clientseitige Remoting Konfiguration vorzunehmen. Das funktioniert offensichtlich, ist dann aber vermutlich absolute Grundkonfiguration.

Bei dir ruft die Anwendung zuerst ApplicationServer.Logon() auf, was dann wiederum ConfigureClientTCPRemotingChannel() aufruft und die entsprechenden Einstellungen setzt. Da ich die Anwendung von grund auf neu schreibe und das Remoting bereits ohne clientseitige Konfiguration funktioniert hat, habe ich dem irgendwie keine Beachtung geschenkt.

Gruß

Christoph

5.299 Beiträge seit 2008
vor 14 Jahren

Hi!

Nach einigen Mühen habichs schließlich auch geschafft, das Teil zum Laufen zu bringen, ich hoffe richtig.

Die letzte Hürde war, dass in den Settings als DataSource immer "Amilo" eingetragen war, wo "SQLServer" hätte stehen müssen - hab ich da iwas übersehen, oder ist das ein Deploying-Problem?

Jedenfalls habich von der .sln eine Kopie gemacht, und starte beide Solutions mit F5, bei der einen ist "ApplicationSever" als StartProjekt eingestellt, bei der anderen (die als 2. zu starten ist) "WindowsClient". So kann ich auf beiden Seiten debuggen.

Also ich hätte gedacht, von wegen Trennung von Business und DAL, dass im Namespace BusinessLogic in der Klasse ProductManager kein SQL-code sein dürfte.



      /// <summary>
      /// Gibt ein bestimmtes Produkt anhand seines Produktschlüssels zurück.
      /// </summary>
      /// <param name="productID">Eindeutiger Produktschlüssel</param>
      /// <returns>Tabelle mit Produktdatensatz</returns>
      public ArticleMasterDataSet.ProductsDataTable GetProduct(Guid productID) {
         // Parameterliste erstellen
         IList<DbParameter> parameters = new List<DbParameter>();
         parameters.Add(DataAccess.CreateParameter(ProductManager._dbFactory, "@productID", DbType.Guid, ParameterDirection.Input, productID));

         // Tabelle für Ergebnisrückgabe erzeugen
         ArticleMasterDataSet.ProductsDataTable result = new ArticleMasterDataSet.ProductsDataTable();

         // SQL-Code
         string sql = "SELECT * FROM Products WHERE ProductID=@productID";

         // Datenbank abfragen
         DataAccess.Query(ProductManager._dbFactory, result, ProductManager._connectionString, sql, parameters);

         // Abfrageergebnisse zurückgeben
         return result;
      }

Weil das SQL ist SQLServer-Dialekt, und bei einer OleDBProviderFactory müsste man den gleich wieder umschreiben, oder?

Ich hätte iwie DatenObjekte erwartet, aber im Namespace BusinessLogic gibts nur diesen ProductManager, der im wesentlichen die Dinge tut, die ich bisher mit typisierten DataAdaptern gemacht hab, und ich dachte auch, typisierte DataAdapter wären DAL.

Ganz penibel würdich sogar das Design fragwürdig finden, weil die Methode heißt GetProduct, aber es wird nicht das einzelne Product zurückgegeben, sondern eine ganze DataTable (wo dann nur im ErfolgsFall genau ein Product drinne ist).

Ich hätte diese eine DataRow als Business-Objekt angesehen, weil deren Properties das Produkt beschreiben.

Dann haben mich noch 2 Sachen im Gui irritiert:
Erstmal kann ich verschiedene Products anlegen, mit derselben ProductNumber - welchen Sinn hat eine Artikelnummer, wenn sie nicht eindeutig ist?

Und dann habich noch nicht rausgefunden, wie man den ist-Stand eines Warenbestandes eingibt.
Im Gui-Code wird iwo die entsprechende Textbox disabled, aber es gibt scheints keine Stelle, wo sie wieder enabled wird.

so weit erstmal, kommt bestimmt noch mehr.

Huppla, jetzt habich meine eingangsFrage wieder gelöscht: Also wirklich wichtig als Layer-Anfänger ist mir die Frage: Welches Projekt ist welchem Layer zugeordnet, und woran erkennt man das?

Ach, ich kann ja schon mal anfangen, dass man sieht, ich geb mir Mühe (bei Gott!):
AppServer: DAL
AppServer.API: auch DAL, aber gewissermaßen abstrahiert. Alle Projecte außer Contract-Projekte haben einen AppServerAPI - Verweis
Die Contract-Projekte enthalten entsprechende Datasets - tätich für Business-Objekte halten, also BL
Die "BusinessLogic"-Projekte irritieren mich halt, wie oben schon erwähnt. Von dem, was da gemacht wird tätich sagen: DAL, aber sie heißen ja ausdrücklich BusinessLogic
Die Services-Projekte sind wohl Konkretisierungen der Contracte -> BL - wobei ich als Contrakte eiglich abstracte Basisklassen bevorzuge, die dann konkret zu überschreiben sind. Da könnte man dann den sich wiederholenden Code (Rollenüberprüfung und sowas) zusammenfassen.
Oder geht das nicht, weil der AppServer nur mit Interfaces diese Proxy-Dinger erstellen kann?

Hmm. Also AppServer + AppServer.API sind schon eine Klasse für sich, also ich mein ein Layer.

das habich auch noch nicht ganz geschnallt - beim AppServer kann man iwie Schnittstellen bestellen, die er selbst nicht kennt, und kriegt die per TCP zugeschickt.
Und beim Client(!!) sind die Konkretisierungen dieser Interfaces, und diese Konkretisierungen können ganz konkret auffm Server herumfummeln - hier DB-Zugriffe ausführen?
Also nochmal kurz: Obwohl Interfaces _und _Konkretisierungen _nur _beim Client implementiert sind, kann man den Code auffm Server ausführen lassen - richtig?

Also da schlaf ich jetzt 'ne Nacht drüber 🙂

Der frühe Apfel fängt den Wurm.

365 Beiträge seit 2004
vor 14 Jahren

Also ich hätte gedacht, von wegen Trennung von Business und DAL, dass im Namespace BusinessLogic in der Klasse ProductManager kein SQL-code sein dürfte.

Hallo ErfinderDesRades,

diesen Punkt finde ich auch etwas unschön. Ich schreibe auch gerade meine eigene Client/Server Anwendung und orientiere mich hier stark an diesem Beispiel. In diesem Punkt allerdings weiche ich ab. Ich habe mir hier deshalb pro Komponente einen DAL geschrieben, der dann Methoden a la "GetProducts(diverse Flags und Filterkriterien)" bereitstellt. Somit abstrahiert mein DAL soweit von der zugrunde liegenden Technik (MySQL, MS SQL, XML, CSV), dass die Business Schicht nichts darüber wissen muss und in keiner Weise von Änderungen am DAL betroffen wäre.

Ich weiß aber auch, dass Rainbird die Ansicht vertritt, dass man sich in der Praxis doch für eine Technik entscheidet und eben nicht alle Stärken einer Technik voll ausspielen kann, wenn man versucht voll und ganz davon zu abstrahieren. Und im Endeffekt ist das bezogen auf das Gesamtbeispiel eher ein Nebenschauplatz. Natürlich kannst du komponentenorientierte DataAccessLayer schreiben, die von der konkreten Technik abstrahieren und den Anwendungsserver trotzdem sonst voll und ganz genauso benutzen, wie er ist.

Gruß

Christoph

3.728 Beiträge seit 2005
vor 14 Jahren
DAL-Verständnis

Also ich hätte gedacht, von wegen Trennung von Business und DAL, dass im Namespace BusinessLogic in der Klasse ProductManager kein SQL-code sein dürfte.

Eine separate Datenzugriffsschicht, wie Du sie Dir vorstellst, macht nur dann Sinn, wenn Du unterschiedliche Datenbanksysteme unterstützen willst/musst. Wenn Du das nicht musst, ist es nur unnötiger Aufwand und erhöht die Komplexität des Codes, wo es nicht sein müsste. Es handelt sich hierbei auch um eine Beispielapplikation, die zeigen soll, wie man mit Remoting verteilte Anwendungen erstellen kann. Wie genau der Datenzugriff implementiert ist spielt dafür keine Rolle. Da Datenzugriff eh das Lieblingsthema für Glaubenskriege unter Entwicklern ist, gibt es dazu jede Menge Beispiele und Artikel im Netz, die sich nur dem Für und Wieder diverser Datenzugriffskonstrukte widmen.

Ob der SQL-Code direkt in der Geschäftslogik steht, oder in einer anderen Klasse ist sowieso fast egal, da in jedem Fall eine Neukompilierung und ein neues Deployment erforderlich ist, wenn sich am SQL-Code bzw. an der DB-Struktur irgendwas ändert. Den SQL-Code aus der Geschäftslogik rauszuhalten wird - meiner Meinung und Erfahrung nach - häufig überbewertet. Da im SQL-Code auch teilweise Berechnungen, Umformatierungen und Entscheidungen stattfinden, gehört er definitiv zur Geschäftslogik und es ist nicht falsch ihn da hinzuschreiben, wo ich ihn hingeschrieben habe.

Weil das SQL ist SQLServer-Dialekt, und bei einer OleDBProviderFactory müsste man den gleich wieder umschreiben, oder?

Ich verwende Provider-Factories, deshalb könnte man den Provider ganz einfach wechseln, ohne die Komponenten anzufassen.

Ich hätte iwie DatenObjekte erwartet, aber im Namespace BusinessLogic gibts nur diesen ProductManager, der im wesentlichen die Dinge tut, die ich bisher mit typisierten DataAdaptern gemacht hab, und ich dachte auch, typisierte DataAdapter wären DAL.

In Produkten gibts eben nicht viel Logik, da Produkte nicht gebucht werden, das sind nur Stammdaten. Schau Dir deshalb man bitte die Lager-Komponente an. Dort ist eine kleine Buchungslogik integriert. DatenObjekte (Typisierte DataSets) findest Du in den Contract-Assemblies. Da Datenobjekte nur "dumme" Strukturen darstellen und nicht die Geschäftslogik enthalten gehören sie auch nicht in die Geschäftslogik. Außerdem müssen sie in einer separaten Assembly stehen, da sie vom Server und vom Client gleichermaßen benötigt werden.

Ganz penibel würdich sogar das Design fragwürdig finden, weil die Methode heißt GetProduct, aber es wird nicht das einzelne Product zurückgegeben, sondern eine ganze DataTable (wo dann nur im ErfolgsFall genau ein Product drinne ist).
Ich hätte diese eine DataRow als Business-Objekt angesehen, weil deren Properties das Produkt beschreiben.

Man kann keine einzelne DataRow serialisieren. Du musst daran denken, dass alle Parameter und Rückgabewerte serialisierbar sein müssen. Du kannst eine verteilte Anwendung nicht genauso entwerfen, wie Du das bei einer 2-Tier Anwendung machen würdest. Mit dem peniblen objektorientierten Ansatz, fällst Du bei verteilten Anwendungen auf die Nase - das ist ein Versprechen!

Erstmal kann ich verschiedene Products anlegen, mit derselben ProductNumber - welchen Sinn hat eine Artikelnummer, wenn sie nicht eindeutig ist?

Ups! Hab ich vergessen Unique zu setzen. Ist vorher noch niemand aufgefallen. Danke für den Hinweis.

Und dann habich noch nicht rausgefunden, wie man den ist-Stand eines Warenbestandes eingibt.Im Gui-Code wird iwo die entsprechende Textbox disabled, aber es gibt scheints keine Stelle, wo sie wieder enabled wird.

Du musst Lagerbuchungen machen, um den Bestand zu verändern. Das ist bei Lagerverwaltung so üblich.

Also wirklich wichtig als Layer-Anfänger ist mir die Frage: Welches Projekt ist welchem Layer zugeordnet, und woran erkennt man das?

Da bin ich jetzt penibel. Es geht hier nicht um Layers, sondern um Tiers! Das ist ein grundlegender Unterschied. Bei Layers werden die Schichten über Klassen und Assemblies getrennt (also nur logische Trennung), bei Tiers werden sie durch Prozesse (EXE-Dateien) getrennt.
Die Deutsche Sprache unterscheidet leider nicht zwischen Layers und Tiers, da sind beides Schichten. Deshalb verwechseln viele Leute eine 3-Tier-Architektur mit einer 3-Layer-Architektur. Das sind zwei Paar Stiefel.
Das Beispiel hat die folgenden 3 Tiers:*Datenschicht: SQL-Server *Geschäftsschicht (Mittelschicht): Applikationsserver *Präsentationsschicht: Windows.Forms-Client

Jede Schicht kann auf einem anderen Computer laufen, deshalb auch "verteilte Anwendung".

Du denkst viel zu freingranular. Vergiss mal die Klassen, denk in Komponenten!

Hmm. Also AppServer + AppServer.API sind schon eine Klasse für sich, also ich mein ein Layer.

das habich auch noch nicht ganz geschnallt - beim AppServer kann man iwie Schnittstellen bestellen, die er selbst nicht kennt, und kriegt die per TCP zugeschickt.

Nein, so ist das nicht. Der AppServer ist ein Stück Infrastruktur - vergleichbar mit ADO.NET, was ein Stück Infrastruktur für den Datenzugriff ist. Der AppServer implementiert die nötige Infrastruktur, um Geschäftskomponenten zu hosten. Zu dieser Infrastruktur gehört auch das Sicherheitssystem und der Dienst für die Sperren. Das ist keine "fachliche" Geschäftslogik.
Die Hauptaufgabe des Applikationsservers ist das bereitstellen von Geschäftskompoenten (Diensten), welche die eigentliche Geschäftslogik kapseln. Per App.config werden Assemblies, die solche Dienste enthalten beim AppServer registriert. Diese lädt er dann beim Start und veröffentlicht sie über TCP/IP laut Konfiguration.
Die Clients können über einen bestimmten URL (ähnlich wie bei einem Webserver) diese Dienste Ansprechen und sich ein Proxy-Objekte für den Zugriff auf die Dienste erzeugen lassen. Dazu muss ein Client nur die Schnittstelle des Dienstes kennen, den er konsumieren will. Die Proxy-Erzeugung erledigt die generische ServiceFactory. Über den Proxy lassen sich die Methoden eines Dienstes aufrufen, die in der öffentlichen Schnittstelle freigegeben sind. Der Aufruf wird serialisiert, übers Netz geschickt, dort deserialisiert und verarbeitet. Beim Rückgabewert geht das Ganze dann Rückwärts.

5.299 Beiträge seit 2008
vor 14 Jahren

Und dann habich noch nicht rausgefunden, wie man den ist-Stand eines Warenbestandes eingibt.Im Gui-Code wird iwo die entsprechende Textbox disabled, aber es gibt scheints keine Stelle, wo sie wieder enabled wird.
Du musst Lagerbuchungen machen, um den Bestand zu verändern. Das ist bei Lagerverwaltung so üblich.

Ich krieg da nix hin. Bei mir gibts das Haupt-Fenster, und da kannich ArtikelStamm oder Läger auswählen.
Bei ArtikelStamm kannich nach Artikeln suchen oder neue anlegen.
Ich kann auch einen Artikel "öffnen" - da krieg ich allerlei Detail-Infos, u.a. eine Tabelle der Läger, die diesen Artikel lagern.
Bei dieser Lager-Tabelle gibts 3 ToolstripButtons: "neuer Bestand", "Bestandsänderung", "Parameter bearbeiten".
Leider geht von diesen dreien nur der erste, und bei dem ist, wie gesagt der Ist-Bestand disabled.

[Edit: Habs jetzt hingekriegt. Glaub, das Problem war, dassich einen Artikel am Wickel hatte, für den kein Lagerbestand eingetragen war]

Über den "Läger"-Dialog (ich glaub übrigens, der Plural von Lager ist Lager) kommich zu garnix, was aussieht, als könne ich da eine LagerBuchung machen. Da kann ich verschiedene Lager-Standorte cruden.

Ansonsten habichsemal verteilt, deine verteilte Anwendung 😉

Habich 3 Solutions von gemacht, damit ich das iwie besser durchblicke, das mit den Tiers. (War mir schon bekannt, dass du zw. Tiers und Layers unterscheidest. Ich hab Tiers einfach für eine Art verschärfte Layers gehalten. Ob und welche logischen Konsequenzen sich aus der Trennung in veschiedene Executeables ergeben, mussich beizeiten mal hirnen)

Hier die Aufteilung (in Klammern die Projekte):*AppServer-Solution: (AppServer, AppServerAPI) *Services-Solution: (AppServerAPI, ArticleMasterContracts, ArticleMasterServices, BL-Articlemaster, InventoryContracts, InventoryServices, BL-Inventory) *Client-Solution: (AppServerAPI, ArticleMasterContracts, ArticleMasterWinGui, InventoryContracts, InventoryWinGui, ClientApplication)

Die Zusammenhänge sind schon recht tricky:
Die Services-Solution muß nur kompiliert werden. Statt einer "Ausführung" greift der PostBuild-Befehl

copy /Y "$(TargetDir)*.*" "$(SolutionDir)AppServer\$(OutDir)"

, mit dem sich die ArticleMasterServices-DLL und die InventoryServices-DLL ins Ausführungsverzeichnis des AppServers kopieren.
Letzterer "kennt" die Services nur durch die App.Config, wo scheinbar


      <service>
        <!-- Geschäftsdienst "Artikelstammverwaltung" -->
        <wellknown mode="SingleCall" type="ArticleMasterServices.ArticleMasterService, ArticleMasterServices" objectUri="ArticleMasterContracts.IArticleMasterService" />
        <!-- Geschäftsdienst "Lagermverwaltung" -->
        <wellknown mode="SingleCall" type="InventoryServices.InventoryService, InventoryServices" objectUri="InventoryContracts.IInventoryService" />
      </service>

die Typ-Angabe einer Service-Klasse mit einer "object-uri" eines Service-Interfaces gemappt wird.
Wenn nun der Client beim AppServer ein IArticleMasterService bestellt, bekommt er ein ArticleMasterService-Objekt, (welches IArticleMasterService implementiert).
(äh, Rainbird - ich hab deine superlangen Projektnamen radikal verkürzt, hoffe aber, es bleibt erkennbar, was mit was gemeint ist)

Die Client interessiert das alles nicht so rasend. Der bestellt per AppServerApi.ServiceFactory ein contractlich festgelegtes Service-Interface, und das kann halt Daten liefern.

So, ich hoffe, das alles richtig geschnackelt zu haben.

Und schichtentheoretisch betrachtet täte gelten:
DAT (Data Access Tier) ist der SQLServer erselbst.
BLT ist die Service-Solution
Gui is Gui
Und der AppServer selber ist Infrastruktur, und dassis nochmal was ganz anners.

Der frühe Apfel fängt den Wurm.

365 Beiträge seit 2004
vor 14 Jahren

Habich 3 Solutions von gemacht, damit ich das iwie besser durchblicke, das mit den Tiers.

Für meinen Geschmack ist das der falsche Ansatz. Wenn du eine Anwendung komponentenorientiert aufbaust, solltest du in erster Linie in Komponenten und in zweiter Linie in Schichten denken. Wenn du das ganze also in einzelne Solutions aufteilen willst, dann würde ich eher die Komponenten (Lagerverwaltung, Artikelverwaltung) in Solutions ausgliedern. Wenn du dir dann eine Komponente wie die Lagerverwaltung genauer ansiehst, besteht die nun wiederum aus mehreren Schichten. Einer Präsentationschicht, einer Businessschicht und ggf. einer Datenschicht (Bei Rainbird gibt es die komponentenorientierte Datenschicht nun gerade nicht).

2.760 Beiträge seit 2006
vor 14 Jahren

Über den "Läger"-Dialog (ich glaub übrigens, der Plural von Lager ist Lager)

😉 Guckst du Duden:

La|ger, das; -s, - u. Läger [spätmhd. lager, unter Anlehnung an "Lage" für mhd. leger, ahd. ...

5.299 Beiträge seit 2008
vor 14 Jahren

Habich 3 Solutions von gemacht, damit ich das iwie besser durchblicke, das mit den Tiers.

Für meinen Geschmack ist das der falsche Ansatz. Wenn du eine Anwendung komponentenorientiert aufbaust, solltest du in erster Linie in Komponenten und in zweiter Linie in Schichten denken. Wenn du das ganze also in einzelne Solutions aufteilen willst, dann würde ich eher die Komponenten (Lagerverwaltung, Artikelverwaltung) in Solutions ausgliedern. Wenn du dir dann eine Komponente wie die Lagerverwaltung genauer ansiehst, besteht die nun wiederum aus mehreren Schichten. Einer Präsentationschicht, einer Businessschicht und ggf. einer Datenschicht (Bei Rainbird gibt es die komponentenorientierte Datenschicht nun gerade nicht).

Versteh ich nicht, Artikelverwaltung und Lagerverwaltung sind hier mittnander im WniGuiClient verquickt.
Nämlich indem man einen Artikel wählt, kann man sich seine Läger ( 😉) anzeigen lassen, und auch den Lagerbestand in einem der Läger ändern.
Das sehe ich nicht, wie auseinanderpflücken.

Wassich auseinandergepflückt habe ist auch "as it is" - am Code habich nix geändert. Habe nur transparent gemacht, welches projekt welche anderen "kennen" muß.

Und da zeigt sich interessanterweise, daß die konkreten Service-Implementationen keiner kennen muß, es reicht, dass sie in AppServer.Config mit den Service-Contracts-Interfaces geappt werdn (und dass die Dlls in sein Arbeitsverzeicchnis kopiert wern).

Nun könnte mans noch weiter treiben, und die Services je in eine eigen Solution packen - die müssen sich untereinander schon mal gar nicht kennen. Aber das kannman auch lassen - ich jedenfalls 😉.

Aber das WinGui aufteilen in Artikelverwaltung und Lagerverwaltung? - da müssteman 'ne Menge neu schreiben, und das wäre eine erhebliche Änderung der Struktur - der User hätte 2 Anwendungen statt einer.

Komponentologisch tätich denken, kann man evtl. noch eine WebClient-Komponente dranmachen, iwas mit ASP.Net.
Und dem stünde dann frei, ob er noch extra Services zufügen möchte, und/oder die bisherigen vorhandenen mit nutzt.

Der frühe Apfel fängt den Wurm.

365 Beiträge seit 2004
vor 14 Jahren

Ich glaube da hast du mich falsch verstanden. Ich möchte auch nix am Code ändern 😃

Aber wenn du das ganze in einzelne Solutions packen möchtest, solltest du dich imho an den Projektmappenordnern orientieren, die Rainbird bereits erstellt hat.

Dann bekommst du:

  • Eine Applikationserver Solution (Appserver, Appserver.API)
  • Eine Artikelkomponenten Solution (theoretisch: BL, GUI, ggf. DAL -> in seinem Beispiel einfach alles unterhalb von Rainbird.Examples.NTier.ArtikelMaster)
  • Eine Lagerkomponenten Solution (analog)
  • Eine Client Solution

Natürlich kannst du auch anfangen und die Komponenten (Artikelverwaltung, Lagerverwaltung) noch weiter in einzelne Solutions zerlegen (den Schichten nach). Ich denke aber nicht, dass das unbedingt sein muss.

5.299 Beiträge seit 2008
vor 14 Jahren

Mir scheint ein Begriffs-Problem.
Eiglich wollte ich ja was über Layer lernen. und hab Layer und Tier nicht groß unterschieden.
Daraufhin erklärt mir Rainbird

Da bin ich jetzt penibel. Es geht hier nicht um Layers, sondern um Tiers! Das ist ein grundlegender Unterschied. Bei Layers werden die Schichten über Klassen und Assemblies getrennt (also nur logische Trennung), bei Tiers werden sie durch Prozesse (EXE-Dateien) getrennt.
Die Deutsche Sprache unterscheidet leider nicht zwischen Layers und Tiers, da sind beides Schichten. Deshalb verwechseln viele Leute eine 3-Tier-Architektur mit einer 3-Layer-Architektur. Das sind zwei Paar Stiefel.
Das Beispiel hat die folgenden 3 Tiers:
Datenschicht: SQL-Server

Geschäftsschicht (Mittelschicht): Applikationsserver

Präsentationsschicht: Windows.Forms-Client

Jede Schicht kann auf einem anderen Computer laufen, deshalb auch "verteilte Anwendung".

Du denkst viel zu freingranular. Vergiss mal die Klassen, denk in Komponenten!

Also denkich, ein Tier ist eine Komponente ist ein Prozess, die können auf verschiedenen Rechnern laufen - verteilte Anwendung.
D.h. für mich: eine Solution, ich mach F5, dann läuft die.

2 Solutions braucht man ja zwangsweise, jedenfalls, wenn man sowohl im AppServer als auch inne ClientApp Haltepunkte setzen will.

Eine Solution Artikelkomponenten, wie du sie vorschlägst, bringt nix - die kann man nicht laufen lassen.

Verwendest du den Begriff "Komponente" iwie anners als Rainbird?

Der frühe Apfel fängt den Wurm.

365 Beiträge seit 2004
vor 14 Jahren

Ich glaube da hast du etwas falsch verstanden. Meiner Definition nach geht der Begriff Tier einfach noch weiter als Layer und meint damit einen Layer, der aber theoretisch auch auf einer anderen Maschine laufen könnte.

Wenn du die Anwendung zu Übungszwecken nach Tiers aufteilen möchtest, kannst du das natürlich machen. Ich wollte eigentlich nur sagen, dass das in der Praxis nicht sinnvoll ist und man die Anwendung eher in Komponenten aufteilen sollte. Eine Komponente besteht dann wiederum aus Schichten. Und dieses Beispiel ist nach meinem Verständnis ganz klar komponentenorientiert aufgebaut.

Eine Solution Artikelkomponenten, wie du sie vorschlägst, bringt nix - die kann man nicht laufen lassen.

Eine Komponente muss nicht zwangläufig für sich alleine lauffähig sein. Wird sie auch in den allermeisten Fällen nicht.

Verwendest du den Begriff "Komponente" iwie anners als Rainbird?

Ich glaube, dass wir hier die gleiche Definition haben. Wobei Rainbird in dem ganzen natürlich deutlich sattelfester ist. Ich hoffe aber im Kern mit seiner Definition übereinzustimmen 😉

3.728 Beiträge seit 2005
vor 14 Jahren
Begriffs-Chaos

Also um das Begriffs-Cahos zu entwirren:

Tiers =! Komponenten

man könnte eher sagen:

Dienste == Komponenten

Folgender BLOG-Eintrag macht das vielleicht klarer: http://yellow-rainbird.de/blogs/rainbird/archive/2008/11/11/dienste-einer-n-tier-anwendung-in-eine-klare-hierarchie-bringen.aspx

Ich unterscheide z.B. zwischen GUI-Komponenten und Geschäftskomponenten. Eine Geschäftskomponente besteht aus Geschäftslogik-Klassen, Service-Klassen (um die Geschäftslogik von der Infrastruktur unabhängig zu halten) und Kontrakten (Schnittstellen und Datenklassen). Was zusammen eine komplette Komponente bildet, ist auf den ersten Blick nicht unbedingt klar, da der Code auf verschiedene Assemblies aufgeteilt ist. Dafür gibt es aber Namensräume, die Typen über Assemblies hinweg zusammenfassen.

GUI-Komponenten sind in den meisten Fällen Forms und UserControls. Idealerweise UserControls + Plug-In-Konfiguration.

Der Begriff Komponente ist naturgemäß ist etwas schwammig.

In wie viele Solutions Du das aufteilst, ist absolut egal. Was zählt ist die Packetierung der Komponenten in Assemblies.

Die GUI ist überigens aus verschidenen UserControls zusammen gebaut. Das Haupt-GUI-Projekt fügt die einzelnen Teile zu einem Ganzen zusammen.

5.299 Beiträge seit 2008
vor 14 Jahren

Also um das Begriffs-Cahos zu entwirren:

Du meinst: das Chaos komplett machen? 😉

Jedenfalls führst du noch (!!) einen Begriff ein - "Dienst", und Komponente scheint mir widersprüchlich verwendet - aber ich soll in Komponenten denken! (das überlegichmirnoch 😉)

Tiers =! Komponenten

Jo, was ist dann nun ein Tier?

Dienste == Komponenten Und was ein Dienst?
Folgender BLOG-Eintrag macht das vielleicht klarer:
>

Sieht mir bischen aus wie ein gelockertes Layer-Modell: Man darf in eine Richtung durch die nachbarschicht hindurch-grabschen.
Gefällt mir glaubich, weil so Methoden, die einen Aufruf nur durchreichen, magichnichso.

Ich unterscheide z.B. zwischen GUI-Komponenten und Geschäftskomponenten. Eine Geschäftskomponente besteht aus Geschäftslogik-Klassen, Service-Klassen (um die Geschäftslogik von der Infrastruktur unabhängig zu halten) und Kontrakten (Schnittstellen und Datenklassen).

Was zusammen eine komplette Komponente bildet, ist auf den ersten Blick nicht unbedingt klar, da der Code auf verschiedene Assemblies aufgeteilt ist. Dafür gibt es aber Namensräume, die Typen über Assemblies hinweg zusammenfassen.

Hmm. Im Application-Server ist ein Projekt genau ein Namensraum. Kein Namespace erstreckt über mehrere Projekte. Oder meinst du die Segmentierung der Namensraum-Namen durch '.'?
Dann wäre bei


Rainbird.AppServer
Rainbird.AppServer.API
Rainbird.Examples.NTier.ClientApplication
Rainbird.Examples.NTier.ArticleMaster.Contracts
Rainbird.Examples.NTier.ArticleMaster.BusinessLogic
Rainbird.Examples.NTier.ArticleMaster.Services
Rainbird.Examples.NTier.ArticleMaster.WindowsGUI
Rainbird.Examples.NTier.Inventory.Contracts
Rainbird.Examples.NTier.Inventory.BusinessLogic
Rainbird.Examples.NTier.Inventory.Services
Rainbird.Examples.NTier.Inventory.WindowsGUI

"Rainbird.Examples.NTier" eine Komponente?
Und "Rainbird.Examples.NTier.ArticleMaster" eine untergeordnete Komponente?

GUI-Komponenten sind in den meisten Fällen Forms und UserControls. Idealerweise UserControls + Plug-In-Konfiguration.

Ein UserControl ist aber doch keine Assembly - aber trotzdem eine Komponente?

Der Begriff Komponente ist naturgemäß ist etwas schwammig.

Du sagst es 😉

In wie viele Solutions Du das aufteilst, ist absolut egal. Was zählt ist die Packetierung der Komponenten in Assemblies.

Kannst du mich mal aufklären, was aus obigem Frame die Tiers sind, die Komponenten und die Dienste? Gilt das "Dienste == Komponenten" in dem Sinne, dass jeder Dienst ein Projekt ist?

Der frühe Apfel fängt den Wurm.

3.728 Beiträge seit 2005
vor 14 Jahren

Jedenfalls führst du noch (!!) einen Begriff ein - "Dienst", und Komponente scheint mir widersprüchlich verwendet - aber ich soll in Komponenten denken! (das überlegichmirnoch 😉)

Der Begriff Dienst wird im Webservice-Umfeld benutzt, um Codeeinheiten eines bestimmten fachlichen Bereichs zu beschreiben, deren Methoden (auch Operationen genannt) über standardisierte Protokolle (z.B: SOAP) von Clients konsumiert werden können.
So ähnlich verhält es sich auch mit meinen Geschäftskomponenten. Ich hatte ja auch nur gesagt "man könnte eher sagen a==b" und nicht "a ist dasselbe wie b".

Jo, was ist dann nun ein Tier?

Tiers sind die grobe Grundeinteilung (im Falle von 3-Tier-Architektur: Präsentationsschicht, Geschäftsschicht, Datenschicht). Wäre Komponente==Tier, dann würde es eine riesige Molloch-Komponente mit dem Namen BusinessLogic geben in der Artikel und Lagerverwaltung gleichermaßen implementiert sind. Das wäre ein monolithischer Ansatz. Ein komponentenorientierter Ansatz ist, wenn die Tiers aus einzelnen Komponenten zusammengesetzt sind, also z.B. 1 x Artikelverwaltrungs-Komponente + 1 x Lagerverwaltungs-Komponente. Genauso ist es im Presentation-Tier (GUI): 1 x Artikelverwaltungs-Froms-und-Controls + 1 x Lagerverwaltungs-Forms-und-Controls. Hinzu kommt die Infrastruktur. Das ist auf der Business-Tier (Geschäftsschicht) der AppServer mit seinen Diensten wie Sicherheit, Logging, etc. und auf der Persentation-Tier (GUI) die MDI-Shell + Glue-Forms (z.B. das Artikel-Formular im GUI-Hauptprojekt , welches nur den Sinn hat, die GUI-Komponenten der Artikel- und Lagerverwaltung miteinander zu integrieren).

Und was ein Dienst?

Eine Klasse + eine Schnittstelle, deren Aufagbe es ist, die eigentliche Geschäftslogik von der Infrastruktur möglichst (ganz wird es sich selten vermeiden lassen) unabhängig zu halten und bestimmte Methoden der Geschäftslogik übers Netzwerk aufrufbar zu machen. Im Dienstcode wird z.B. die Kultur-Information des Aufrufers an den aktuellen Worker-Thread gehängt und es werden die Rechteprüfungen durchgeführt. Ein Dienst integriert ein Stück Geschäftslogik in die Infrastruktur (den AppServer).

Sieht mir bischen aus wie ein gelockertes Layer-Modell: Man darf in eine Richtung durch die nachbarschicht hindurch-grabschen.
Gefällt mir glaubich, weil so Methoden, die einen Aufruf nur durchreichen, magichnichso.

Das hast Du falsch verstanden. Da wird nichts durchgereicht. Wenn Du z.B. die Geschäftslogik schreibst, um Lieferscheine zu verwalten, dann benötigst Du Daten aus dem Artikelstamm und Du musst Lagerbuchungen durchführen. Also muss die Kompnente "Lieferschein-Verwaltung" Methoden der Komponenten Artikelstammverwaltung und Lagerverwaltung aufrufen. Dazu darf sie aber nicht die eigentliche Implementierung (BusinessLogic-Assemblies) dieser beiden anderen Komponenten direkt aufrufen, da sonst Verweise nötig sind. Stattdessen muss die Lieferschein-Verwaltung nur Verweise auf die Kontrakte haben und kann (wie das auch z.B. ein GUI-Client tun würde) sich über die ServiceFactory Proxies für den Zugriff auf die beiden anderen Komponenten über deren Dienste (also der Teil der Geschäftskomponente, der die Geschäftslogik über Remoting konsumierbar macht) erzeugen lassen. Die Kommunikation läuft so entkoppelt ab und es spielt keine Rolle, wo die konsumierten Komponenten laufen. Vielleicht läuft die Artikelstammverwaltung auf einen ganz anderen Applikationsserver? Ein Tier muss nicht komplett auf einem Computer laufen. Man könnte das auch auf verschiedene Computer verteilen. Dazu benötigt man aber Komponenten und muss in Komponenten denken und nicht in Schichten (Tiers). Selbst wenn man die Komponenten nicht auf verschiedene Computer aufteilt, ist der Ansatz sinnvoll, da die Komponenten so sehr autonom sind. Die Architektur zwingt den Entwickler, autonome Komponenten zu schreiben und nicht über Verweise auf die Implementierung alles miteinander zu verwursteln.

Noch autonomer geht nur, wenn man die Komponenten orchestriert, also jede Komponente auch für alles externe eine eigene Schnittstelle hat und die Kommunikation zwischen den Komponenten über Mappings läuft. Das wird dann im Volksmund häufig als SOA bezeichnet. Ist innerhalb einer Anwendung aber meistens zu Aufwändig und der Nutzen ist Fragwürdig (es gibt ja auch noch nicht wirklich Langzeit-Erfahrungen mit SOA, da der Begriff sehr schwammig ist und es ein relativ neuer Hype ist). Außerdem müsste man auch jede Komponente mit der Infrastruktur orchestrieren, was noch aufwändiger und abstrakter ist, da man dann z.B. nicht mehr innerhalb einer Komponente davon ausgehen kann, dass eine Windows-Authentifizierung verwendet wird. Es müsste sogar so sein, dass jede Komponente ihre eigene Datenbank hat, da man sonst nicht beliebige Komponenten kombinieren kann.
Deshalb vergessen wird mal SOA, das klappt so nicht.

Wenn Komponenten nun aber nicht nur von der Presentation-Tier aus konsumiert werden, sondern sich auch gegenseitig konsumieren, muss diese Kommunikation geregelt ablaufen. Es muss festgelegt werden, welche Komponenten welche anderen Komponenten überhaupt aufrufen dürfen. Die Komponente Artikelstammverwaltung darf z.B. niemals die Komponente Lieferschein-Verwaltung aufrufen, aber umgekehrt. Um den Überblick zu behalten, organisiert man die Komponenten in Ebenenen (Achtung! Das sind weder Tiers noch Layers). Diesen Ebenen kann man auch Namen geben, wenn's passt, wie z.B. Stammdatenebene oder Workflowebene. Wie sowas konkret aussehen kann, zeigt das Diagramm im erwähnten Blog-Eintrag.

Hmm. Im Application-Server ist ein Projekt genau ein Namensraum. Kein Namespace erstreckt über mehrere Projekte. Oder meinst du die Segmentierung der Namensraum-Namen durch '.'?
Dann wäre bei

  
> Rainbird.AppServer  
> Rainbird.AppServer.API  
> Rainbird.Examples.NTier.ClientApplication  
> Rainbird.Examples.NTier.ArticleMaster.Contracts  
> Rainbird.Examples.NTier.ArticleMaster.BusinessLogic  
> Rainbird.Examples.NTier.ArticleMaster.Services  
> Rainbird.Examples.NTier.ArticleMaster.WindowsGUI  
> Rainbird.Examples.NTier.Inventory.Contracts  
> Rainbird.Examples.NTier.Inventory.BusinessLogic  
> Rainbird.Examples.NTier.Inventory.Services  
> Rainbird.Examples.NTier.Inventory.WindowsGUI  
> 

"Rainbird.Examples.NTier" eine Komponente?

Und "Rainbird.Examples.NTier.ArticleMaster" eine untergeordnete Komponente?

Rainbird.Examples.NTier ist der Namensraum der kompletten Lösung. Es gibt zwei Module, die aus jeweils einer Geschäftskomponente und einer GUI-Komponente bestehen:

Rainbird.Examples.NTier.ArticleMaster
Rainbird.Examples.NTier.Inventory

Ein UserControl ist aber doch keine Assembly - aber trotzdem eine Komponente?

Ja, da man aus UserControls ähnlich einem Baukastensystem komplette Oberflächen aufbauen kann.

Kannst du mich mal aufklären, was aus obigem Frame die Tiers sind, die Komponenten und die Dienste? Gilt das "Dienste == Komponenten" in dem Sinne, dass jeder Dienst ein Projekt ist?

Tiers kann man nicht direkt auf Assemblies oder Namensräume oder Klassen abbilden. Tiers sind "nur" eine architektonische Planungseinheit.

5.299 Beiträge seit 2008
vor 14 Jahren

Vielen Dank.

Son Satz Definitionen brauchich gelegentlich, sonst habich das Gefühl, mein Denken verkommt zum Mutmaßen.

Aber ich hab anneres Problem:
Habichjetzt auch einen Service dranpatchen wollen, die ServiceFactory macht da sogar mit, nur der Service kann nichts tun.*Die Datenbank um eine Table erweitert *dazu passendes Dataset gebastelt *einen Service geschrieben, inkl. Interface *inne AppServer.app.config einen Eintrag zugefügt


         <!-- Geschäftsdienst "Eckard Spezial" -->
         <wellknown mode="SingleCall" type="EService2.EService, EService" objectUri="EService2.IEService" />


   public partial class EInfoForm : Form {

      private IEService _eService;

      public EInfoForm() {
         InitializeComponent();
         _eService = ServiceFactory<IEService>.CreateProxy();
         _eService.FillTable(eDataset1.ETable);    // <-hier der Fehler
      }
   }

Huch - im Detail hat die Fehlermeldung diese "LOG"-Zeilen - das kennichnochgarnicht:

System.BadImageFormatException was unhandled
  Message=Die Datei oder Assembly "EService" oder eine Abhängigkeit davon wurde nicht gefunden. Die Assembly wird von einer Laufzeit erstellt, die aktueller als die derzeit geladene Laufzeit ist, und kann nicht geladen werden.
  Source=mscorlib
  FileName=EService
  FusionLog==== Zustandsinformationen vor Bindung ===
LOG: Benutzer = Admin-PC\Admin
LOG: DisplayName = EService
 (Partial)
LOG: Appbase = file:///C:/Programming/CodeDownloads/CsDownloads/myCSharp/ApplicationServer/AppServer/bin/Debug/
LOG: Ursprünglicher PrivatePath = NULL
Aufruf von Assembly : (Unknown).
\===
LOG: Diese Bindung startet im default-Load-Kontext.
LOG: Die Anwendungskonfigurationsdatei wird verwendet: C:\Programming\CodeDownloads\CsDownloads\myCSharp\ApplicationServer\AppServer\bin\Debug\AppServer.vshost.exe.Config
LOG: Die Computerkonfigurationsdatei von C:\Windows\Microsoft.NET\Framework\v2.0.50727\config\machine.config wird verwendet.
LOG: Die Richtlinie wird derzeit nicht auf den Verweis angewendet (private, benutzerdefinierte, teilweise oder pfadbasierte Assemblybindung)
LOG: Download von neuem URL file:///C:/Programming/CodeDownloads/CsDownloads/myCSharp/ApplicationServer/AppServer/bin/Debug/EService.DLL.
ERR: Das Setup der Assembly konnte nicht abgeschlossen werden (hr = 0x8013101b). Die Suche wurde beendet.

Jo, das mit den verschieden aktuellen Laufzeiten versteh ich ühaupt nicht, auch finde ich keinen prinzipiellen Unterschied zw. meinem Service und den deinigen.

Der frühe Apfel fängt den Wurm.

365 Beiträge seit 2004
vor 14 Jahren

Hallo ErfinderDesRades,

deine DLL heißt EService.dll und liegt im Verzeichnis des Anwendungsservers?

Gruß

Christoph

5.299 Beiträge seit 2008
vor 14 Jahren

nein, die dll heißt EService2, und die Service-Klasse dadrin heißt EService.
Aber liegt im AppServer-ausführungsverzeichnis, auchso mittm PostBuild gemacht:


copy /Y "$(TargetDir)*.*" "$(SolutionDir)AppServer\$(OutDir)"

Der frühe Apfel fängt den Wurm.

F
10.010 Beiträge seit 2004
vor 14 Jahren

Du solltest evtl mal schauen, was bei type="" (Vollqualifizierter Assemblyname) wirklich hinkommt.

365 Beiträge seit 2004
vor 14 Jahren

Hallo ErfinderDesRades,

tröste dich, ich habe den Fehler auch gemacht! Da gehört der tatsächliche Name der DLL hin 😉

Gruß

Christoph

Edit: Hallo vergessen 😃

5.299 Beiträge seit 2008
vor 14 Jahren

wassichda hingeschrieben habe, habichja gepostet. Vermutlich meinst du, ich solle schauen, was da hinkommen sollte?

Das weißich natürlich nicht, da habichnur sinngemäß von Rainbirds Einträgen abgekupfert.
Aber nicht ganz korrekt abgekupfert - und siehe - jetzt gehtes! (dafür geht was anneres nicht, aber ich probier erstmal selbst)

Aber verstehen tu ich davon kaum was


      <!-- Dienste -->
      <service>
        <!-- Geschäftsdienst "Artikelstammverwaltung" -->
        <wellknown mode="SingleCall" type="ArticleMasterServices.ArticleMasterService, ArticleMasterServices" objectUri="ArticleMasterContracts.IArticleMasterService" />
         <!-- Geschäftsdienst "Lagermverwaltung" -->
         <wellknown mode="SingleCall" type="InventoryServices.InventoryService, InventoryServices" objectUri="InventoryContracts.IInventoryService" />
         <!-- Geschäftsdienst "Eckard Spezial" -->
         <wellknown mode="SingleCall" type="EService2.EService, EService2" objectUri="EService2.IEService" />
      </service>

Wo findet man dokumentiert, welche Tags und welche Attribute welche Auswirkung haben?

Der frühe Apfel fängt den Wurm.

365 Beiträge seit 2004
vor 14 Jahren

Du findest dazu sicherlich in der MSDN Hilfe, wenngleich ich auch auf die Schnelle nix passenden gesehen habe. Ansonsten ist dieser Link ganz hilfreich:

http://www.csharphelp.com/archives2/archive460.html

Außerdem habe ich bereits ähnliche Fragen in diesem Thread gestellt. Rainbird hat darauf bereits sehr detailiert geantwortet. Geh mal auf Seite 3 in diesem Thread und schau nach:

Das ist viel einfacher, als Du denkst. RemotingConfiguration.Configure parst und verarbeitet die angegebene Konfigurationsdatei (also die App.config). Das bedutetm dass nach Aufruf von Configure der Server bereits voll Einsatzbereit ist und alle in der App.config eingetragenen Dienste automatisch schon als WellKnowServiceTypes registriert sind. Im einfachsten Fall sieht ein Remoting-Host so aus[...]

3.728 Beiträge seit 2005
vor 14 Jahren
Doku

Wo findet man dokumentiert, welche Tags und welche Attribute welche Auswirkung haben?

Schaust Du hier: http://msdn.microsoft.com/de-de/library/z415cf9a(VS.80).aspx
http://msdn.microsoft.com/de-de/library/kwdt6w2k(VS.80).aspx

5.299 Beiträge seit 2008
vor 14 Jahren

Hupsalalala!

Jo, da binnich beim Lesen der Seiten wohl statt auf den "nächsten"-Button auf den "letzten"-Button gekommen, und dachte, ich hätte den ganzen Thread schon intus.
Sorry, das eine oder andere hättich dann wohl nicht gefragt, hmm.

@rainbird: Danke für die Doku

So, jetzt habichaber doch noch ein paar dumme Fragen, zu den Geheimnissen der Remoterei.

Erstmal versuchichs mirselbst zu erklären:
Ein Service (Damit meine ich jetzt, was man braucht, um einen neuen Service dranzupatchen) besteht zum einen aus einer Service-Contract-Dll, in der die DatenObjekte implementiert sind.
Die Methoden, die der Service ausführen können soll, sind dort als Interface definiert ("IService" nennichdas mal).
In einer anderen Dll ist die Konkretisierung des Interfaces implementiert (bei Rainbird die "Business-Logic").
Der Client hat die Contract-Dll eingebunden.
Beim Server liegen Contract-Dll, Konkretisierungs-Dll und Abhängigkeiten herum.
Per App.Config werden Contract und Konkretisierung dem AppServer bekannt gemacht (_:::

Wenn der Client zur Laufzeit einen IService bei der ServiceFactory bestellt (mit Activator.GetObject()), bekommt er ein IDingsbums (Proxy), dessen Methoden im Client aufrufbar sind, die aber im Server in der Konkretisierungs-Dll ausgeführt werden - klarer Fall von Hexerei! 😉
Parameter und Rückgabewerte der Methoden dieses Proxies werden beim Methoden-Aufruf serialisiert und wandern durch den Channel (tcp oder ipc, weitere?).

Zu beachten: *Die konkreten ServiceObjekte müssen von MarshalByRefObject erben. *Die Datentypen der Methoden -Parameter und -Rückgabewerte müssen serialisierbar sein. *Parameter und Rückgabewerte sollten leichtgewichtig sein, weil wandern pro Aufruf durch den Channel. *_**:::

Soweit.

Könnte man nun theoretisch sonem AppServer eine neue Konkretisierungs-Dll unterschieben, ohne ihn abzuschalten?
Neue Services hinzupatchen ebenso?

Allerdings nicht mit diesem AppServer, weil der nur zum Start die config auswertet, und das wars dann, odr?
Aber geht immerhin ohne neu-compilieren - aber ist das nun so ein großer Gewinn?

Mühe macht mir auch, einen neuen Service zu entwickeln. Weil die Konkretisierungs-Dll nirgends wirklich eingebunden ist, kann man keine Haltepunkte setzen.
Immerhin erfreulich eine andere Hexerei: Im FehlerFall stoppt der Code in der Fehlerzeile, ob die nun zur AppServerSolution gehört, oder nur drangepatcht ist.
Und statt Haltepunkte kannmanja Debugger.Break() eincoden.

Der frühe Apfel fängt den Wurm.

3.728 Beiträge seit 2005
vor 14 Jahren
Hexerei

Parameter und Rückgabewerte der Methoden dieses Proxies werden beim Methoden-Aufruf serialisiert und wandern durch den Channel (tcp oder ipc, weitere?).

Es gibt noch einen HTTP-Channel, der nützlich ist, wenn man durch Firewalls muss. Remoting ist aber auch erweiterbar. Es gibt z.B. eine Erweiterung für XML-RPC-Kommunikation: http://www.xml-rpc.net/

Parameter und Rückgabewerte sollten leichtgewichtig sein, weil wandern pro Aufruf durch den Channel.
Es ist natürlich gut, wenn sie leichtgewichtig sind, aber trotzdem gilt die Faustregel "Klotzen statt kleckern!". Jeder Methodenaufruf hat Kommunikations-Overhead, deshalb ist es besser, viele Daten an einem Stück zu übertragen, statt bröckchenweise. Der absolute Horror sind statuswahrende Objekte, auf deren Properties remote zugegriffen wird. Gut sind dagegen statuslose Objekte, die ihre Daten am Stück (z.B. als ganze Objekte, DataTables oder DataSets) übertragen.

Könnte man nun theoretisch sonem AppServer eine neue Konkretisierungs-Dll unterschieben, ohne ihn abzuschalten?
Neue Services hinzupatchen ebenso?

Neue Services hinzupatchen sollte relativ einfach machbar sein. Du kannst ja Services auch zur Laufzeit mit Code veröffentlichen.
Im laufenden Betrieb Services updaten ist schon schwieriger. Du müsstest eine zweite AppDomain hochziehen und einen Remoting-Nachrichtendispatcher schreiben, der während des Updates alle Nachrichten auf die zweite AppDomain umleitet. Generell dürftest Du die Dienste nicht in der Standard-AppDomain laufen lassen, da man in der Standard-AppDomain keine Assemblies mehr entladen kann.
Ein weiteres Problem wäre die Kompatiblität. Es ist nicht garantiert, dass der upgedatete Service zum vorherigen kompatibel ist.

Allerdings nicht mit diesem AppServer, weil der nur zum Start die config auswertet, und das wars dann, odr?
Aber geht immerhin ohne neu-compilieren - aber ist das nun so ein großer Gewinn?

Ja, momentan wird die App.config nur beim Start ausgewertet.
Ein großer Gewinn ist es schon, ohne Neukompilierung neue Dienste zu deployen. Du musst nur per XCopy die neuen Assemblies auf den Produktiv-Server kopieren, einen Entrag in der App.config machen und den Applikationsserver-Windowsdienst (in Produktivumgebungen ist es selten eine Konsolenanwendung) neu starten.

Mühe macht mir auch, einen neuen Service zu entwickeln. Weil die Konkretisierungs-Dll nirgends wirklich eingebunden ist, kann man keine Haltepunkte setzen.

Natürlich kann man Haltepunkte setzen. Entweder legt man mehrere Startprojekte für die Mappe fest, wenn man alles lokal hat, oder man Klickt auf Menü Debuggen und wählt dann "An Prozess anhängen ...". So kann man den Debugger an jeden laufenden Windows-Prozess anhängen. Man kann beliebig viele Prozesse gleichzeitig Debuggen in Visual Studio. Selbst wenn der Applikationsserver auf einem anderen Computer läuft, auf dem kein Visual Studio installiert ist, kannst Du trotzdem Haltepunkte verwenden. Du musst auf dem entfernten Server nur den Visual Studio-Remotedebugger starten (das geht auch über eine Netzwerkfreigabe auf dem Entwickler-PC; Einfach die EXE des Remotedebuggers starten). Über den "Prozess anhängen" Dialog kannst Du den lokalen Debugger dann mit der Remotedebugging-Sitzung auf dem entfernten Server verbinden. Du must also auf nichts verzichten.
Den Remotedebugger gibts allerdings erst ab Visual Studio Professional. Aber im .NET Framework SDK ist der kostenlose SDK Debugger (sieht aus wie Visual Studio, kann aber nur Debuggen) enthalten. Damit kannst Du überall dort kostenlos debuggen, wo kein Visual Studio installiert ist.
http://support.microsoft.com/?scid=kb%3Ben-us%3B910448&x=9&y=14

Leider beschäftigen sich viele Entwickler viel zu wenig mit ihrer IDE. 😁

49.485 Beiträge seit 2005
vor 14 Jahren

Hallo zusammen,

in Anbetracht des engen Zusammenhangs zum Applikationsserver-Projekt, sei hier noch auf NotificationService als Erweiterung zu Rainbirds ApplikationServer Beispiel verwiesen.

herbivore

18 Beiträge seit 2009
vor 14 Jahren
Programmatische Veröffentlichung des SecurityService

Hallo Rainbird.
Ich habe zur Zeit deinen ApplicationServer testweise im Einsatz.
Zunächst einmal: fett respect dafür! Echt saubere Arbeit.

Ich hätte folgende Fragen, die ich auch nach längeren Überöungen und Experimentieren nicht selbst lösen konnte:

1.
Mich würde interessieren, wie man es schafft den SecurityService programmatisch zu veröffentlichen/bereitszustellen?

Der folgende Versuch (und Varianten davon) sind gescheitert:



			// Channel-Konfiguration
			System.Collections.IDictionary properties = new System.Collections.Hashtable();
			properties["name"] = "AppServerChannel";
			properties["port"] = 9999;
			properties["secure"] = true;

			// Eigenen BinaryServerFormatterSinkProvider erstellen
			BinaryServerFormatterSinkProvider provider = new BinaryServerFormatterSinkProvider();
			provider.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;	

			// Channel mit vordefinierten Properties erstellen 
			TcpServerChannel tcpChannel = new TcpServerChannel(properties, provider);

			// Channel registrieren
			ChannelServices.RegisterChannel(tcpChannel, true);

			// Sicherheitsdienst veröffentlichen	
			 RemotingConfiguration.RegisterWellKnownServiceType(typeof(SecurityService),
				 "Rainbird.AppServer.API.ISecurityService", WellKnownObjectMode.Singleton);

			 RemotingServices.Marshal(SecurityService.Instance);


Der Client erhält beim Logon den Fehler mit der Meldung "Der angeforderte Dienst wurde nicht gefunden".
Verwende ich wieder die Konfigurationsdatei funktioniert alles einwandfrei.

2.
Wie registrierst du den SecurityService denn überhaupt?
In der Config-Datei konnte ich keine Einträge diesbezüglich finden.

3.
Mir ist aufgefallen, dass trotz der Einstellung "Singleton" für meinen Geschäfts-Dienst, beim ersten Methodenaufruf der Standard-Konstruktor des Service aufgerufen wird, obwohl ich ihn bereits im Server zuvor instanziert habe.
Wenn ich den Dienst ohne Default-Konstruktor implementiere, erhält der Client beim Methodenaufruf eine Exception mit der Meldung, dass kein parameterloser Konstruktor definiert wurde.
Hierfür finde ich keinerlei Erklärung. Die Instanz des Dienstes veröffentliche folgendermaßen:



		RemotingConfiguration.RegisterWellKnownServiceType(typeof(cBusinessService),
				"ServiceContracts.IBusinessService", WellKnownObjectMode.Singleton);

			cBusinessService service = new cBusinessService();
			RemotingServices.Marshal(service);


Für deine Hilfe wäre ich echt dankbar, sofern deine Zeit es zulässt.

Viele Grüße

3.728 Beiträge seit 2005
vor 14 Jahren
Direct Remoting

Hallo Wyatt,

das ist ganz einfach:

Den Kanal konfiguriere ich in der App.config (da sich Port etc. ja ohne Neukompilierung ändern lassen sollen) und den Sicherheitsdienst manuell über Direct Remoting. Du findest Die Codezeile in der Core.cs:


// Sicherheitsdienst veröffentlichen
RemotingServices.Marshal(SecurityService.Instance, "Rainbird.AppServer.API.ISecurityService");

Mehr ist nicht nötig. Marshal veröffentlicht die bestehende Singleton-Instanz unter dem angegebenen URI. Wenn Du keinen URI bei Marshal angibst, erzeugt Remoting einen zufälligen (den Du dann aber nicht weisst). Also entweder RegisterWellknownServiceType ODER Marshal, aber nicht beides zusammen!

Der Unterschied ist, dass Marshal eine bereits erzeugte Instanz veröffentlicht und RegisterWellkownServiceType nur einen Typ (Remoting kümmert sich dann um die Erzeugung einer Instanz). Ich wollte in diesem Fall die Erzeugung der Sicherheitsdienst-Instanz komplett selbst in der Hand haben, darum habe ich diesen Weg gewählt.

Eine normale Singleton-Veröffentlichung hätte es auch getan. Das war einfach nur Kontrollzwang.

Remoting hat für die meisten Probleme ganz einfache und gute Lösungen (im Gegensatz zu WCF).

18 Beiträge seit 2009
vor 14 Jahren

Hi Rainbird!

Danke für deine stets hilf- und lehrreichen Antworten!

Marshal veröffentlicht die bestehende Singleton-Instanz unter dem angegebenen URI. Wenn Du keinen URI bei Marshal angibst, erzeugt Remoting einen zufälligen (den Du dann aber nicht weisst). Also entweder RegisterWellknownServiceType ODER Marshal, aber nicht beides zusammen!

Diesen Unterschied kannte ich nicht.
Damit ist meine Frage (3) auch gelöst. Das Problem war somit, dass ich den Typ (per RegisterWellKnownServiceType) UND die Instanz (per Marshal) veröffentlicht hatte. Somit erhielt der Client eine neue Instanz des veröffentlichten Typs. Meine gemarshallte Instanz wurde nicht an den Client geliefert.
Das "Problem" zu meiner Frage (1) konnte ich nun auch lösen:
In der Config-Datei kann man ja den Namen der Applikation angeben. Diesen muss man ja im URI der Application-Server Methode mitgeben, also


ApplicationServer.Logon("tcp://localhost:9999/<ApplicationName>/");

Wenn ich aber Remoting programmatisch konfiguriere, kann ich ja keinen Applikationsnamen angeben. Somit musste ich einfach den Namen aus dem URI entfernen. Sounds easy, but hard to find 😭

1.
Gibt es eine Möglichkeit den Applikationsnamen auch bei der programmatischen Konfiguration einzustellen?
[Edit 19:43 Uhr] Selbstohrfeig 😁


RemotingConfiguration.ApplicationName = "MyAppServer";         // Server
ApplicationServer.Logon("tcp://localhost:9999/"MyAppServer/"); // Client

Es funktioniert jetzt mit der programmatischen Konfiguration alles top, bis auf:
Wenn ich aber auf Server-Seite "ApplicationServer.IsInRole(role)" verwende, kehrt der Aufruf nicht zurück, es wird aber auch keine Exception geworfen.
Verwende ich wieder die Config-Datei, bleibt die Methode "IsInRole()" nicht stehen.

Ich habe es debuggt und festgestellt, dass hier:


        public static bool IsInRole(string roleName)
        { 
            // Wenn momentan eine Sitzung besteht ...
            if (HasSession)
            {
                // Verbindung zum Sicherheitsdienst des Anmeldungs-Applikationsservers herstellen
                ISecurityService securityServiceProxy = (ISecurityService)Activator.GetObject(typeof(ISecurityService), ServerURL + typeof(ISecurityService).FullName);
				
                // Berechtigungsprüfung durchführen
               return securityServiceProxy.IsInRole(roleName);
            }
            // Falsch zurückgeben
            return false;
        }

bei "securityServiceProxy.IsInRole" der Aufruf nicht zurückkehrt. Der Aufruf komm im Debug-Modus garnicht erst im Security Service an. Finde den Grund nur leider nicht. Ist das nicht seltsam, zumahl es von Client-Seite aus funktioniert? Es funktioniert ja seltsamerweise auch auf Server-Seite, sobald ich wieder die Config-Datei verwende.
Haste du vielleicht spontan ne Idee dazu?

Besten Dank im Voraus!

3.728 Beiträge seit 2005
vor 14 Jahren

bei "securityServiceProxy.IsInRole" der Aufruf nicht zurückkehrt. Der Aufruf komm im Debug-Modus garnicht erst im Security Service an. Finde den Grund nur leider nicht. Ist das nicht seltsam, zumahl es von Client-Seite aus funktioniert? Es funktioniert ja seltsamerweise auch auf Server-Seite, sobald ich wieder die Config-Datei verwende.
Haste du vielleicht spontan ne Idee dazu?

Schau Dir mal den Code der Eigenschaft ApplicationServer.SeverURL an. Der URL wird aus dem Aufrufkontext bezogen. Der URL des Applikationsservers an dem sich ein Benutzer anmeldet ist Teil der Sitzungsinformationen, die im Aufrufkontext (Siehe MSDN: CallContext-Klasse) abgelegt werden (das passiert in ApplicationServer.Logon). Die Daten im Aufrufkontext werden automatisch und stillschweigend bei JEDEM Remoting-Aufruf übertragen und dort in den Datenslots des Arbeitsthreads abgelegt (Man kann an einen Thread einfach irgendwelche Daten hängen. Siehe MSDN: Lokaler Threadspeicher).

Warum der ganze Zirkus?

Wenn ein Client einen Service A aufruft und dieser einen anderen Service B aufruft, passiert der Aufruf von Service B nicht im Benutzerkontext des Aufrufers von Service A (also dem Client-Benutzer), sondern mit dem Benutzerkonto unter dem der Thread von Service A läuft. Und das ist das Dienstkonto des Applikationsservers. Service B würde z.B. statt 'DOMÄNE1\MaxMustermann' plötzlich 'DOMÄNE1\AppServerKonto' als aufrufender Benutzer gemeldet bekommen. Das ist aber doof, da die Rollenprüfung eigentlich auf den ursprünglichen Aurufer der Aufrufkette erfolgen soll.
Der Aufrufkontext löst dieses Problem, da die Daten im Threadspeicher abgelegt udn dort auch wieder herausgenommen werden. Daten im CallContext werden also über die KOMPLETTE AUFRUFKETTE durchgereicht, egal wie tief die Kette verschachtelt ist oder über wie viele Server kommuniziert wird.
Statt die Identität des Aufrufers direkt zu prüfen, wird die Identität der Sitzung geprüft, deren Sitzungsschlüssel im Aufrufkontext abgelegt ist.

Wenn der Client keine Windows.Forms-Anwendung sondern eine ASP.NET-Webanwendung ist, muss man den Aufrufkontext zusätzlich noch mit dem ASP.NET-Sitzungsstatus synchronisieren (ApplicationServer.LoadContextDataFromWebSessionState und ApplicationServer.SaveContextDataInWebSessionState). Bei einer Windows.Forms-Anwendung braucht man das nicht, da der GUI-Thread die ganze Zeit am Leben bleibt und die Daten in seinem Threadspeicher nicht vergisst.

Wenn Du einen Dienstaufruf aus einem manuell erzeugten Thread machst, wirst Du standardmäßig eine Ausnahme bekommen, in der sich der Server beschwert, dass Du nicht angemeldet bist. Neue Thread heißt auch neuer - und damit leerer -Threadspeicher. Also Logon aufrufen oder Daten im Aufrufkontext in den Threadspeicher der zusätzlichen Threads replizieren.
Bisher hat das scheinbar noch keiner gemacht. Da wir das Thema aber gerade sowieso haben, wollte ich es gleich erwähnt haben.

Ursache Deines Problems im Speziellen: Du hast versucht eine Rollenprüfung durchzuführen, ohne den Benutzer vorher mit Logon anzumelden.

Dieses Verhalten ist so gewollt. Ohne Login geht gar nix. Das ist ein Sicherheitsaspekt. 8)
Ich habe sehr auf Sicherheit geachtet. Mir ist es bisher nicht gelungen, meinen eigenen Sicherheitsdienst auszutricksen.

Es macht ja auch keinen Sinn, die Rolle des Applikationsserver-Dienstkontos zu prüfen. Es soll ja geprüft werden, ob ein Client-Benutzer eine bestimmte Dienstmethode aufrufen darf oder nicht.

18 Beiträge seit 2009
vor 14 Jahren

Hey Rainbird.

Wow, jede Menge brauchbarer Info und das auch noch um die Uhrzeit. Ein einfaches DANKE, reicht da schon fast nicht mehr aus: DICKES DANKE =)

Die Sache mit dem CallContext hatte ich schon soweit verstanden, wenn auch nicht in diesem Detailgrad. Advanced .NET Remoting (Ingo Rammer) verwendet den CallContext ebenfalls, um bspw. Logging-Einstellungen zu übertragen.

Ursache Deines Problems im Speziellen: Du hast versucht eine Rollenprüfung durchzuführen, ohne den Benutzer vorher mit Logon anzumelden.

Nein, es muss wohl an etwas anderem liegen. Mein Client ruft Logon auf:


				cApplicationServerManager.LogonFail += new EventHandler<SessionEventArgs>(cApplicationServerManager_LogonFail);

				// Log on to the Remote Application Server
				cApplicationServerManager.Logon("tcp://localhost:9999/MyAppServer/");

				// If we have a valid SessionId to the Remote Business Server
				if (cApplicationServerManager.HasSession)
				{
					// Connect to the Business Service
					IBusinessService businessService = ServiceFactory<IBusinessService>.CreateProxy();

                     IArticle[] articles = businessService.GetArticles();
                     // ...
				}

Der Aufruf landet auch wunderbar im Service. Hier rufe ich dann ApplicationServer.IsInRole() auf. Darin wird die Property "ServerUrl" aufgerufen, die auch noch erfolgreich den URL aus dem CallContext extrahiert. Dann wird (alles auf Serverseite) eine Remote-Verbindung zum SecurityService aufgebaut und SecurityService.IsInRole() aufgerufen. Dieser Aufruf bleibt ohne Exception einfach stehen.
Verwende ich allerdings die Config-Datei zur Konfiguration des Server-Channels, funktioniert es einwandfrei.

Ich finde aber den entscheidenden Unterschied nicht:

Programmatische Konfiguraion meines Server-Channels:


			RemotingConfiguration.ApplicationName = "MyAppServer";
			RemotingConfiguration.CustomErrorsMode = CustomErrorsModes.Off;

			// Eigenen BinaryServerFormatterSinkProvider erstellen
			BinaryServerFormatterSinkProvider provider = new BinaryServerFormatterSinkProvider();
			provider.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;			

			// Channel mit vordefinierten Properties erstellen 
			TcpServerChannel tcpChannel = new TcpServerChannel("AppServerChannel", 9999, provider);
			tcpChannel.IsSecured = true;

			// Channel registrieren
			ChannelServices.RegisterChannel(tcpChannel, true);

Wenn ich die Config-Datei verwende geht aber alles. Was fehlt bei der programmatischen Konfiguration? Mir fällt nichts auf.
Gruß

3.728 Beiträge seit 2005
vor 14 Jahren
TcpChannel

Hallo Wyatt,

Du darfst keinen TcpServerChannel verwenden sondern musst einen TcpChannel verwenden. Sonst kann der Server nicht selber wieder Client sein. Clientseitig solltest Du auch den TcpChannel verwenden (Falls der Client in einem Szenario mal kurz Server spielen muss).

Es kann auch sinnvoll sein serverintern über einen zusätzlichen IpcChannel zu kommunizieren. Ist schneller und muss nicht über den TCP-Stack.

P.S.: Super Buch! Allerdings teile ich z.B. die Auffassung nicht, daß Remoting bevorzugt im IIS gehostet werden sollte.