Laden...

[gelöst] ASP MVC + EntityFramework: Connection (closed/ opened) ==> Context Sharing

Erstellt von mbk_chilli vor 12 Jahren Letzter Beitrag vor 12 Jahren 4.781 Views
M
mbk_chilli Themenstarter:in
79 Beiträge seit 2007
vor 12 Jahren
[gelöst] ASP MVC + EntityFramework: Connection (closed/ opened) ==> Context Sharing

verwendetes Datenbanksystem: MS SQL

HI @ all,

leider stehe ich aktuell vor einem kleinen "Halb"-Rätsel.

Ausgangsbasis:
ASP.NET MVC 3, C# 4, LinQ
Via EntityFramework eine MS SQL Datenbank eingebunden.

Folgende Fehlermeldung erhalte ich:
"The connection was not closed. The connection's current state is connecting."

Was die Fehlermeldung nun heißt ist natürlich klar. Aber wie es dazu kommt und wie ich es verhindern kann wäre nun die Frage.

Verschiedenen Objects stelle ich die Datenbankverbindung statisch zur Verfügung. Evtl. nicht der beste Weg und evtl. auch der Weg welcher mich zu dem benannten Problem führt. Allerdings - aktuell - nicht zu vermeiden.

Die verschiedenen Objects rufen sich auch gerne gegenseitig auf - denn jeder ist für eine spezielle Aufgabe zuständig bzw. behandelt verschiedene Tabellen in der Datenbank. Sie verwenden somit ALLE die Gleiche Connection.

Code in der Basisklasse welche die Connection zur Verfügung stellt:

private static DbEntities _Db = null;
        protected DbEntities DbAccess {
            get {
                if (_Db == null) {
                    _Db = new DbEntities();

                    //_Db.Connection.StateChange += new System.Data.StateChangeEventHandler( Connection_StateChange );
                }

                return _Db;
            }
        }

Warum der statische Zugriff für alle Objekte?

  • Wenn ich ein statisches Objekt zur Verfügung stelle arbeiten alle verwendenden Objekte auf dem gleichen ObjectContext. Was durchaus erwünscht ist - für z.B. das Löschen und Hinzufügen von neuen Datenbankobjekten - zeilen!

Leider tritt nun sporadisch folgende Fehlermeldung auf:

The connection was not closed. The connection's current state is connecting.

Vornehmelich bei folgenden Aufruf:


 foreach (DataBase.MyTapes myTape in allTapes) {

Weiß jemand direkt ob sich dieser Fehler wirklich darauf bezieht, dass der Zugriff statisch für alle Objekte zur Verfügung steht. Wobei es zu parallelen Zugriffen kommen kann.

Besonders: Wenn ich Bilddaten über eine MVC Action Abfrage und sehr oft F5 im Browser drücke!

Gibt es eine Möglichkeit dem Datenbankobjekt zu sagen:
Ist ok der Status ist "Connecting" das passt schon! naja o.Ä.!

Für jedwede Hilfe wäre ich sehr dankbar!
Falls jemand mehr infos benötigt poste ich diese gerne jederzeit!

Vg
mbk_chilli

Die drei Tugenden eines Programmierers:
Faulheit, Ungeduld und Hochmut!

M
mbk_chilli Themenstarter:in
79 Beiträge seit 2007
vor 12 Jahren
Nachtrag

Zusatz:

Interessanterweise ließ sich das Problem zwar nicht beheben aber wohl umgehen durch:


//An welcher stelle das Event gesetzt wird: Siehe 1. Post
_Db.Connection.StateChange += new System.Data.StateChangeEventHandler( Connection_StateChange );
 
void Connection_StateChange( object sender, System.Data.StateChangeEventArgs e ) {
            if (e.CurrentState == System.Data.ConnectionState.Closed) {
               _Db.Connection.Open();
            }
        }

Ich denke jedoch nicht, dass dies im Sinne des Betrachters ist und wirklich eher das Problem umgeht anstatt es zu lösen. Von daher der Nachtrag und NICHT die Lösung!

Vg
mbk_chilli

Die drei Tugenden eines Programmierers:
Faulheit, Ungeduld und Hochmut!

16.842 Beiträge seit 2008
vor 12 Jahren

Hallo,

Lösung Deines Problems: jeder Request hat seinen **eigenen **Context.

Deine von Dir genannte Variante funktioniert nicht - wird sie auch nich. Spätestens bei multiplen (Ajax-)Requests zur gleichen Zeit fliegen Dir die Exceptions um die Ohren - und das zu Recht!

Eine Web-Anwendung ist immer mit verschiedenen Threads verbunden - und das Entity Framework ist nicht Thread-sicher. Eine Web-Anwendung darf ++niemals ++an irgendeiner Stelle irgendwo einen Context teilen - NIE!

Es gibt NIEMALS einen Grund, weshalb ein Context hier geteilt werden muss. Wenn man Objekte in einer Webanwendung teilen will (zum Beispiel eine Liste der derzeit aktiven User, die jeder angezeigt bekommen soll), dann benutzt man einen Nested-Singleton, in dem nur die nötigsten Informationen (zB User-IDs, Last-Active-Time) liegen - aber niemals ein Objekt, das evtl. sogar noch einem bestimmten Datenbank-Kontext zugeteilt ist.

Hintergrund: der IIS behandelt jeden Request in einem eignen Thread (Thread.QueueUserWorkItem) und gibt diesen auch so an die Webanwendung weiter. Aufgrund der Tatsache, dass der IIS Thread.QueueUserWorkItem verwendet, darf zudem dies Deine Anwendung nicht tun, da Du sonst wieder ein Lock bekommst, weil die Queue voll läuft.

Besonders: Wenn ich Bilddaten über eine MVC Action Abfrage und sehr oft F5 im Browser drücke!

Durch meine Erklärung(sversuche) sollte Dir die Ursache nun bekannt sein. Je mehr Zugriffe zeitgleich abgearbeitet werden - und wenn dann auch noch lange Aufgaben abgearbeitet werden und der Context über eine gewisse Zeit aktiv genutzt wird, desto mehr Fehler wirst Du kriegen, da die Wahrscheinlichkeit immer größer wird, dass irgendwann der Kontext geteilt wird => Böse.


Normalerweise ist der Aufbau einer MVC und EF Anwendung folgender:

Du hast einen Basis-Controller, von dem alle anderen Controller erben

public class MyBaseController : Controller { /* ... */ }
public class HomeController : MyBaseController { /* ... */ }

In diesem Basis-Controller wird die Verbindung zur Datenbank erstellt, sodasss bei jedem Request ein **eigener **Context geöffnet wird.


public class MyBaseController : Controller
{
   private _myEntityConnection;
   public EntityConnection EntityContext
   {
      get
      {
          if ( myEntityConnection == null)
             myEntityConnection = new EntityConnection( "my connection string" );

          return myEntityConnection;
       } 
    }
}

Mit this.EntityContext greifst Du nun innerhalb einer Action auf die Datenbank zu.

Bei Deiner "Lösung" wird Dir früher oder später die w3wp.exe volllaufen.
Du hälst hier künstlich die Verbindung am Leben, weshalb die Request-Daten nicht vom GC / IIS verworfen werden und Dir den Speicher zu müllen.

Wenn Du jetzt noch an der Datenbankverbindung bist, sprich relativ am Anfang des Aufbaus Deiner Anwendung: machs richtig und nicht so nen murks!

Gruß

M
mbk_chilli Themenstarter:in
79 Beiträge seit 2007
vor 12 Jahren

Hallo Abt,

vielen Dank schon einmal für Deine Antwort.

Bei mir ist der aktuelle Aufbau wie folgt:

BaseController : Controller für jeden Bereich (Benutzer und Admin)
Eigentlicher "x"Controller : BaseController mit den MVC Actions.

Den Datenzugriff habe ich ausserhalb der Controller gehandelt.
namespace ModelAccess.

BaseModelAccess welcher die Datenbank Connection zur Verfügung stellt.
Dann unterteilt in ModelAccess (quasi SELECT), ModelModify (quasi UPDATE / DELETE) und ModelTasks (quasi der Rest oder wenn boolsche Werte gewünscht sind wie z.B. bei "Ist die CD / DVD bereits vorhanden" etc.)

Stand: Datenbankzugriff nicht statisch

Das Problem trat nun auf, dass ich ein Object löschen wollte und mich in ObjectContext y befand jedoch das Object in Contect x registriert gewesen ist.

Da hat mich natürlich .NET getadelt und konnte das Objekt nicht löschen weil sich das Objekt nicht in dem aktuellen Context befindet.
Danach versuchte ich das Objekt zum aktuellen Context hin zu zufügen und das hat .NET ebenfalls nicht gefallen weil das Objekt bereits in einem anderen Context registriert gewesen ist.

Aktueller Stand: Datanbankzugriff statisch.
Hat super funktioniert da sich die Objekte immer im gleichen Context befinden.

Was mich ja nun zu dem neuen Problem geführt hat.

Ich werde aber nun Deine Lösung probieren und den Context im Controller aufbauen - welcher ja dann pro Request neu erstellt wird - und die Connection dann in dem MVC Controller an die ModelAccess Klassen weiterreichen.

So sollte ja dann je Request ein neuer Context verwendet werden und das Löschen und die parallelen Abfragen funktionieren.

Werde berichten ob dies funktioniert hat.

Vg
mbk_chilli

Die drei Tugenden eines Programmierers:
Faulheit, Ungeduld und Hochmut!

16.842 Beiträge seit 2008
vor 12 Jahren

Bei Dir scheint da was ziemlich verwurschtelt zu sein.
Benutzt Du Repositories für eine Schicht-Trennung?

Wenn Dein Datenbank-Zugriff nicht statisch ist ( ich vermute das meinst Du als nicht Shared ) dann kann keine Context-Verletzung stattfinden; außer Du erstellst innerhalb einer aufgerufenen Methode mehrere Contexte, sodass Du hier die Verletzung hast oder nutzt zwischengespeicherte Datenbankobjekte -> evil.

Die Connection sollte nicht in einere anderen Schicht liegen; die liegt immer noch in der Anwendung. Daher auch mein Ratschlag, diese zB direkt im Controller zu erstellen - sprich an EINER zentralen Stelle.

Beachte, dass Du Dein Modify nicht bei ASP MVC anwenden kannst / solltest.
Hier gibt es ein Verfahren, das es in keiner anderen Technologie so gibt: UpdateModel
Zu beachten sei auch, dass Updatemodel einen Bug hat: Asp.Net MVC Model zwischen Actions übergeben

Wenn Du saubere Schichten hast MVC -> Repository -> Entities wird es absolut keine Probleme geben und Du sparst sehr sehr viel Code.

M
mbk_chilli Themenstarter:in
79 Beiträge seit 2007
vor 12 Jahren

Also in der nicht staischen Lösung (vorletzte Lösung):
Wurde je initialisierte ModelAccess Klasse ein Context erstellt.

Welches dann zu Fehlern führte.

Letzte Lösung:
Daher die statische Lösung welche dann zu anderen Problemen führte.

Die aktuelle Lösung:
Basis Controller stellt den Kontext für die ModelAccess Objekt zur Verfügung. Somit pro Request ein Kontext.

Ich habe nach der Implementierung Deiner Lösung alle Problembereiche gecheckt und es scheint sehr gut zu funktionieren.

Für das Modify (UPDATE / DELTE) verwende ich die Entity Objects welche ich bereits besitze (vor den Modify Aktionen hole ich diese aus der Datenbank weil noch einiges Überprüft werden muss) und ändere die Properties bzw. "markiere den EntityState als Deleted" und Speichere dann über die SaveChanges() Methode.

Die drei Tugenden eines Programmierers:
Faulheit, Ungeduld und Hochmut!

16.842 Beiträge seit 2008
vor 12 Jahren

Die saubere Lösung wäre die IValidatableObject-Schnittstelle zu implementieren.

Vorteile:

  • automatische Validierung durch MVC
  • zentrale Validierung ein einer Stelle
  • Sicherheit, dass keine falschen Daten über einen anderen Weg in die Datenbank kommen.
  • Im Gegensatz zur System.ComponentModel.DataAnnotations-Variante können auch logische Abhängigkeiten überprüft werden

Du siehst: MVC und EF arbeiten sehr eng zusammen. Das sollte alles beachtet werden.

Aber Dein eigentliches Problem scheint ja gelöst zu sein.

M
mbk_chilli Themenstarter:in
79 Beiträge seit 2007
vor 12 Jahren

Ist nur die Frage wo ich diese Schnittstelle implementiere.

Das Datenmodel ist ein ADO.NET Entity Data Model.

Als Rückgabe bei Linq - bzw. direkt bei jeder Arbeit damit - verwende ich auch direkt die Datenbankobjekt aus dem namespace DataBase!
Dadurch erübrigt sich das Anlegen gesonderter Datenobjekte welche ansich nur die Gleichen Properties besitzen wie die Datenobjekte in/ aus der Datenbank.

Ich denke aber, dass ich die Schnittstelle bei den Objekten verwende welche ich durch meine ModelBinder erzeuge. Sobald Usereingaben verarbeitet werden habe ich sowieso noch eigene Objekte angelegt um die Eingaben nicht auf die EntityObjects des Data Models "los zu lassen" bevor diese validiert wurden!

Die Validierung der eigenlichen Datenbankobjekte übernimmt ja dann ADO.NET nachdem ich die Usereingaben selber validierte.

Also danke für den Tipp ich werden die Schnittstelle in meinen eigenen Objekten implementieren.

PS: Jo das stimmt, dass eigentliche Problem ist gelöst! Aber es schaded ja nicht zukünftige zu verhindern!

Die drei Tugenden eines Programmierers:
Faulheit, Ungeduld und Hochmut!

16.842 Beiträge seit 2008
vor 12 Jahren

Wir schweifen nun etwas ab; Richtung Grundlagen.

Das Datenmodel ist ein ADO.NET Entity Data Model.

Dein Entity Framework Model erstellt auch nur Klassen via partial
Also kannst Du die Entity-Klassen einfach erweitern, von der IValidatableObject-Schnittstelle erben lassen und die nötigen Member implementieren.

Als Rückgabe bei Linq - bzw. direkt bei jeder Arbeit damit - verwende ich auch direkt die Datenbankobjekt aus dem namespace DataBase!

Ich hoffe, dass Du mir damit sagen willst, dass Du Repositories nutzt. Das tut man nämlich im Zusammenhang mit dem Entity Framework. Spielt aber Prinzipiell keine Rolle.

Die Validierung der eigenlichen Datenbankobjekte übernimmt ja dann ADO.NET nachdem ich die Usereingaben selber validierte.

Käse 😉
Mach das was ich Dir gesagt hab, dann musst nur noch einmal validieren - nich mehr 50 mal. Am besten Du erweiterst Deine Entity um eine IsValid(out List<ValidationResult> errorList):bool Methode, sodass Du es noch einfacher hast.


public class MyEntity : IValidateableObject
{
        public IEnumerable< ValidationResult > Validate( )
        {
            IEnumerable< ValidationResult > list;

            IsValid( out list );

            return list;
        }

        public bool IsValid( out IEnumerable< ValidationResult > resultList )
        {
                // Validierungen hier
        }
}


public ActionResult Edit(MyEntity model)
{
using ( var myEntityRepository = new EntityRepository( this.EntityContext ) ) 
{
	IEnumerable< ValidationResult > errorList;
	if ( ! model.IsValid( out errorList ) )
	{
		 // Es sind Fehler aufgetreten
	} 
	else 
	{
		 UpdateModel<IMyUpdateRules>( myEntityFromDatabase );

		 myEntityRepository.Save( );
	}
}
}

Nun hast aber wirklich genug auf dem Präsentierteller. 😉