Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Jakub Binkowski - dot or not

Blog programisty C#

opublikowano 31 lipca 2008 01:01
Przygody z Policy Injection Application Block

Policy Injection Application Block w skrócie to biblioteka pozwalająca na wstrzyknięcie kodu opakowującego wywołania metod. Dzięki temu za pomocą jednego atrybutu lub odpowiedniego wpisu w pliku konfiguracyjnym, możemy "nakazać", aby np. czas wykonania metody był mierzony i logowany. Oczywiście, różnych takich zastosowań możemy wyobrazić sobie bardzo wiele. Tyleż samo widać korzyści z zastosowania tego podejścia. Programowanie aspektowe (inaczej AOP - aspect oriented programming), o którym właśnie mowa, pozwala ograniczyć ilość powtarzalnego kodu, tak aby programista mógł skoncentrować na implementowanej funkcjonalności, a architekt mógł zaimplementować raz a dobrze kluczowe kwestie (logowanie, obsługa wyjątków, transakcje, itp.).

Pierwsze zauroczenie...

Moją przygodę na poważnie z PIAB zacząłem jakiś czas temu, kiedy postanowiłem wykorzystać ten blok w jednym z projektów. Spośród istniejących rozwiązań wybrałem właśnie blok z Enterprise Library, kierując się przy tym po części instynktem, a po części zaufaniem do technologii sygnowanych przez samą grupę Patterns & Practices z Microsoft. Początki były bardzo miłe - zalety programowania aspektowego są oczywiste. Ponadto, wraz z PIAB dostajemy zestaw gotowych aspektów (a właściwie "call handlers"), integrujących blok z resztą biblioteki. Tak więc wstrzyknięcie kodu do logowania, obsługi wyjątków, walidacji, autoryzacji, itd. uzyskujemy dodając w najprostszym przypadku jeden atrybut.

Wykorzystanie samego PIAB jest proste i nie wymaga żadnych skomplikowanych czynności. Jeżeli planujemy dodawać aspekty tylko poprzez atrybuty, to nawet nie potrzebujemy dodatkowych wpisów w pliku konfiguracyjnym. Wystarczy tylko w specjalny sposób "opakować" obiekt, w który chcemy wstrzyknąć dodatkową funkcjonalność. Oto prosty przykład pokazujący, w jaki sposób uzyskać walidację parametrów metody:

public interface ILogic
{
    [ValidationCallHandler]
    void SayMyName([NotNullValidator]string name);
}

public class Logic : ILogic
{
    public void SayMyName(string name)
    {
        Console.WriteLine(name);
    }
}
Listing 1. Przykład zastosowania PIAB.

Atrybut ValidationCallHandlerAttribute powoduje, iż przed uruchomieniem właściwego kodu metody uruchamiana jest walidacja parametrów. Atrybut NotNullValidator pochodzi już z Validation Application Block, a jego działanie jest chyba oczywiste.

Aby PIAB zadziałało potrzeba jeszcze opakować obiekt klasy Logic:

ILogic logic = PolicyInjection.Wrap<ILogic>(new Logic());

Oczywiście teraz po wywołaniu kodu:

logic.SayMyName(null);

dostaniemy wyjątek ArgumentValidationException, choć nigdzie jawnie nie dokonywaliśmy sprawdzania ani wyrzucania wyjątku. Wygodne, prawda?

...a po upływie miesiąca miodowego...

Zawsze po miłych początkach, nadchodzą pierwsze rozczarowania. Tak samo było, niestety, z moim "romansem" z PIAB. Zanim jednak przejdę do rzeczy, warto zajrzeć pod maskę i zobaczyć jak PIAB działa.

Otóż, podstawowym wymaganiem, by móc wstrzykiwać polityki do klasy jest aby:

  • implementowała ona interfejs przez który będziemy się do niej odwoływać lub
  • dziedziczyła po MarshalByRefObject.

Wymagania te wynikają stad, iż PIAB wewnętrznie wykorzystuje mechanizmy z .NET Remoting i magię CLR, która pozwala utworzyć transparentne proxy dla interfejsów lub właśnie typów dziedziczących po MarshalByRefObject. Owe transparentne proxy przekazują wywołanie do rzeczywistego proxy, którego zadaniem jest utworzenie ciągu odpowiednich call handlers (aspektów). Ostatnim na liście będzie zawsze kod dokonujący rzeczywistego wywołania docelowej metody. Poniższy diagram UML prezentuje jak to w uproszczeniu wygląda:

piab 
Diagram 1. Schematyczna struktura klas przy przechwytywaniu wywołań metod przez PIAB.

Jak widać ciąg wywołań będzie następujący:

  1. Użytkownik wywołuje metodę na transparent proxy.
  2. Dane o wywołaniu zostają zebrane (wartości argumentów, metoda, itp.) i trafiają do metody Invoke() klasy InterceptingRealProxy.
  3. Informacje o wywołaniu oraz delegat wywołujący następny call handler trafiają do pierwszego handlera. Handler może opakować swoją funkcjonalnością dalsze wywołanie.
  4. Wywołanie trafia stopniowo do wszystkich handlerów, aż na samym końcu pojawia się kod, który przez refleksję wywołuje docelową metodę. Wygląda on mniej więcej tak:
       1: try
       2: {
       3:     object returnValue = call.MethodBase.Invoke(/*..*/);
       4:     return /*...*/; //obiekt z informacją o powodzeniu i rezultatem
       5: }
       6: catch (TargetInvocationException ex)
       7: {
       8:     return /*..*/; //obiekt z informacją o wyjątku ex.InnerException
       9: }
    Listing 2. Wywołanie metody docelowej przez infrastrukturę PIAB.

Warto zwrócić uwagę na linijki 6-9, gdyż sygnalizują one mój pierwszy, na razie drobny problem.

Wyjątek wyrzucany dwa razy

Oto pierwsza niespodzianka - wywołując poniższy kod z Visual Studio:

public class Logic : MarshalByRefObject
{
    [ValidationCallHandler]
    public void SayMyName([NotNullValidator]string name)
    {
        Console.WriteLine(PrepareName(name));
    }

    private string PrepareName(string name)
    {
        throw new NotImplementedException();
    }
}

public class Program
{
    private static void Main(string[] argv)
    {
        var logic = PolicyInjection.Create<Logic>();
        logic.SayMyName("Kuba");
    }
}
Listing 3. Podróż wyjątku.

dostajemy następujący efekt:

image

Wygląda to, jakby wyjątek powstał gdzieś pomiędzy wywołaniem, a samym ciałem metody (co jest poniekąd prawdą). Spodziewalibyśmy się okienka dokładnie w miejscu wyrzucenia, czyli w metodzie PrepareName(). Na szczęście jest jeszcze stack trace, który zawiera cały stos wraz z miejscem postania wyjątku. Niby nic, ale każdy mniej doświadczony programista zgłosi się do nas, uznając, że jest to błąd w architekturze.

Z czego takie zachowanie wynika? Warto prześledzieć, co dzieje się z wyjątkiem:

  1. Wyjątek jest rzucany w metodzie PrepareName().
  2. Jako, że metoda SayMyName została wywołana za pomocą infrastruktury PIAB (poprzez refleksję - MethodBase.Invoke()), więc zostanie on opakowany przez TargetInvocationException. Ten zostanie przechwycony (listing 2, linijki 6-9), oryginalny wyjątek zostanie wyłuskany i przekazany jako element wyniku działania funkcji do wcześniejszych handlerów.
  3. Gdy wróci przez wszystkie handlery, to zostaje ponownie wyrzucony.

Stąd właśnie inne miejsce wyrzucenia wyjątku, wskazywane przez okienko "unhandled exception".

Prawdziwe problemy - środowisko hostowane, czyli partial trust...

Swojego czasu musiałem umieścić aplikację na jednym z większych polskich serwisów prowadzących hosting Windows. I tutaj zaczęły się prawdziwe schody. Problem polegał na tym, że w takich środowiskach poziom zabezpieczeń prawie nigdy nie jest ustawiony na Full Trust, a Enterprise Library nie zawsze jest dostępny z GAC (tak było w moim przypadku). Od razu zaznaczam, że problemem nie był fakt, że dopiero wersja 4.0 ma atrybut AllowPartiallyTrustedCallersAttribute (tzw. APTCA), a więc wersje wcześniejsze trzeba przekompilować bez podpisu cyfrowego, aby w ogóle się uruchomiły.

Problem polega na tym, że PIAB od środka wykorzystuje własne rzeczywiste proxy - InterceptingRealProxy, które dziedziczy po System.Runtime.Remoting.Proxies.RealProxy, która to klasa ma jako jeden z atrybutów:

[SecurityPermission(SecurityAction.InheritanceDemand, Flags=SecurityPermissionFlag.Infrastructure)]

Oznacza to, że aby dziedziczyć po RealProxy, assembly musi mieć uprawnienie do wpinania się do infrastruktury .NET Remoting. Jak nie trudno zgadnąć, we współdzielonym środowisku hostowanym zwykle takiego uprawnienia nie dostaniemy, a więc próba utworzenia proxy skończy się... SecurityException. Możemy poprosić administratora o nadanie tego uprawnienia, choć jest mało prawdopodobne, że się zgodzi.

Sytuacja na szczęście nie jest patowa - istnieje jeszcze jedno wyjście. PIAB pozwala na napisanie i podpięcie własnego Policy Injectora, który już z infrastruktury .NET Remoting korzystać nie musi. Na takie wyjście ja się zdecydowałem - postanowiłem napisać mechanizm, który będzie dynamicznie generował obiekty proxy implementujące wskazany interfejs i dalej wywołujące całą infrastrukturę Policy Injection (oczywiście takie podejście wyklucza wstrzykiwanie polityk do MarshalByRefObject). I po kilka dniach wspaniałej przygody z dynamicznym generowaniem kodu IL, z której wrażenia można porównać jedynie do czyszczenia zatkanych rur kanalizacyjnych, uzyskałem w miarę zadowalające rozwiązanie.

Pozwolę sobie nie przedstawiać dalszych szczegółów technicznych, gdyż nikt i tak by tego nie przeczytał, a najwięksi śmiałkowie pewnie zasnęliby w trakcie. Niemniej jednak, gdy moje rozwiązanie dopracuję (ma jeszcze kilka znanych mi błędów), to postaram się je opublikować, aby więcej osób mogło skorzystać z moich walk i nie musiało samemu przechodzić przez to piekiełko :).

Podsumowanie

Policy Injection Application Block to całkiem interesujące i wygodnie rozwiązanie, niemniej jednak, strategia przechwytywania wywołań wybrana przez twórców (infrastruktura .NET Remoting) może powodować problemy przy pewnych specyficznych sytuacjach, jak np. wdrażanie w środowisku o mniejszych uprawnieniach. Warto zdawać sobie z tego sprawę przy wyborze technologii wspierającej AOP podczas prac nad architekturą aplikacji.

przez jakubin | 4 komentarzy

opublikowano 16 lipca 2008 00:39
Transakcyjny mailing

W czym problem?

Wysłanie wiadomości e-mail w .NET jest dziecinnie proste:

MailMessage message = new MailMessage(
    "from@server.com", "to@server.com", "Temat", "Treść");
SmtpClient smtp = new SmtpClient();
smtp.Send(message);

Powyższy przykład jest bardzo krótki, choć i tak został napisany niezwykle rozwlekle - wersja zminimalizowana zajęłaby 1 linijkę (w obu przypadkach ustawienia serwera pocztowego znajdują się w pliku konfiguracyjnym). Właściwie ten kawałek kodu powinien wystarczyć nam do szczęścia, ale czasami zdarzają się bardziej skomplikowane przypadki.

Otóż, ostatnio pisałem kod, w którym kilka metod było wykonywanych w jednej transakcji. Jedna z metod wysyłała wiadomość e-mail z powiadomieniem o powodzeniu operacji. Problem pojawiał się, gdy wykonanie kolejnej się nie powiodło. Wówczas, transakcja nie była zatwierdzana - zmiany nie zostały zapisane do bazy, ale wiadomość o powodzeniu była wysyłana! Poniżej zamieściłem przykład takich metod:

public static void CreateUser(string email)
{
    //rejestracja konta użytkownika
    //...
    
    //wysłanie powiadomienia o rejestracji
    MailingHelper.Send(
        new MailMessage("admin@serwis.com",email,
            "Witaj na portalu!", 
            "Twoje konto zostało zarejestrowane."));
}

public static void GiveSiteAccess(string email)
{
    //nadanie uprawnień do serwisu WWW
    //...
}

Oraz ich wywołanie w ramach transakcji:

using (var ts = new TransactionScope())
{
    CreateUser("user@mail.com");
    GiveSiteAccess("user@mail.com");
    
    ts.Complete();
}

Jak widać, gdy wywołanie metody GiveSiteAccess się nie powiedzie, to i tak do użytkownika trafi wiadomość e-mail o sukcesie rejestracji, choć ta de facto nie nastąpiła.

Zadałem, więc sobie pytanie, czy nie da się jakoś podpiąć procesu wysyłania maili do bieżącej transakcji? Okazuje się, że tak i to bardzo łatwo. Wystarczy napisać własny menadżer zasobów i dołączyć go do transakcji.

Rozwiązanie - własny menadżer zasobów

Do informacji o bieżącej transakcji dostajemy się poprzez statyczną właściwość Current klasy System.Transactions.Transaction (w assembly System.Transactions.dll). Tam dostępne są metody, które pozwalają "dopisać się" do bieżącej transakcji:

  • EnlistVolatile - dołącza do transakcji menadżera zasobów ulotnych (zasoby ulotne to np. dane w pamięci; taki menadżer nie obsługuje odtwarzania stanu po błędzie),
  • EnlistPromotableSinglePhase - dołącza do transakcji menadżera zasobów, który obsługuje tryb promotable single phase enlistment,
  • EnlistDurable - dołącza do transakcji menadżera zasobów trwałych (zasoby trwałe to np. dane na dysku twardym; taki menadżer posiada obsługę odtwarzania stanu po błędzie).

Nas będzie interesować pierwsza metoda, gdyż wiadomość e-mail to zasób ulotny. Menadżer zasobów ulotnych w najprostszej postaci to klasa implementująca interfejs IEnlistmentNotification. Nasz menadżer będzie wyglądał następująco:

public class MailSender : IEnlistmentNotification
{
    private readonly MailMessage message;

    public MailSender(MailMessage message)
    {
        this.message = message;
    }

    #region IEnlistmentNotification Members

    public void Commit(Enlistment enlistment)
    {
        //powoduje faktyczne wysłanie wiadomości
        MailingHelper.SendImmediately(message);
        enlistment.Done(); //menadżer zgłasza zakończenie swojej pracy
    }

    public void InDoubt(Enlistment enlistment)
    {
        enlistment.Done();
    }

    public void Prepare(PreparingEnlistment preparingEnlistment)
    {
        //menadżer potwierdza zakończenie przygotowań
        preparingEnlistment.Prepared(); 
    }

    public void Rollback(Enlistment enlistment)
    {
        enlistment.Done(); //menadżer potwierdza wycofanie transakcji
    }

    #endregion
}

Metody interfejsu IEnlistmentNotification będą wykonywane zgodnie z protokołem dwufazowego zatwierdzania (two-phase commit). W dużym skrócie, proces ten składa się z dwóch faz: fazy przygotowania i fazy zatwierdzania. Menadżer transakcji najpierw żąda przygotowania się menadżerów zasobów - metoda Prepare. Każdy menadżer może zgłosić gotowość (wywołanie preparingEnlistment.Prepared()) lub wymusić wycofanie transakcji (wywołanie preparingEnlistment.ForceRollback()). Jeżeli wszystkie menadżery zgłoszą gotować, to transakcja jest zatwierdzana - metoda Commit. W przeciwnym razie wszystkie menadżery dostają polecenie wycofania transakcji - metoda Rollback. Natomiast, gdy menadżer transakcji straci łączność z którymś z menadżerów zasobów, to wywoływana jest metoda InDoubt.

Dołączenie menadżera zasobów do transakcji odbywa się następująco:

Transaction.Current.EnlistVolatile(new MailSender(message), EnlistmentOptions.None);

Ponieważ cała magia jest już znana, oto kod klasy pomocniczej MailingHelper:

public static class MailingHelper
{
    public static void Send(MailMessage message)
    {
        if (Transaction.Current != null)
        {
            Transaction.Current.EnlistVolatile(
                new MailSender(message), EnlistmentOptions.None);
        }
        else
            SendImmediately(message);
    }

    public static void SendImmediately(MailMessage message)
    {
        var smtp = new SmtpClient();
        smtp.EnableSsl = true;
        smtp.Send(message);
    }
}

Jak widać, metoda SendImmediately wysyła wiadomość natychmiast, podczas gdy metoda Send podpina wysłanie wiadomości pod bieżącą transakcję (jeżeli taka istnieje; w przeciwnym razie wysyła maila od razu). Efekt jest taki, jaki chcieliśmy osiągnąć - poczta jest wysyłana dopiero w momencie zatwierdzenia transakcji.

Kilka słów wyjaśnienia

Przedstawione rozwiązanie, wbrew tytułowi, nie jest receptą na zbudowanie w pełni transakcyjnego mailingu. Tutaj tylko osiągamy efekt opóźnienia wysłania wiadomości e-mail do momentu zatwierdzenia transakcji. Ponadto, kod został uproszczony dla zwiększenia czytelności.

Bardziej dociekliwych zapraszam do:

  1. Dokumentacja klasy Transaction.
  2. Dokumentacja klasy TransactionScope.
  3. Dokumentacja interfejsu IEnlistmentNotification.
  4. Artykuł o implementacji własnego menadżera zasobów.

przez jakubin | 7 komentarzy

Filed under:

opublikowano 18 czerwca 2008 08:25
Co z tym DataContext?

Chyba trudno znaleźć programistę, który po dłuższej pracy z LINQ-to-SQL nie uznałby tej technologii za przełomową pod względem wygody i szybkości tworzenia aplikacji w porównaniu do tego, co wcześniej oferował sam .NET: DataSets oraz czyste DbConnections i spółka. Jednakże używanie tego rozwiązania na dłużą metę nie jest wolne od kilku wyzwań (i bardzo dobrze).

W Linq2Sql klasą dającą dostęp do bazy danych jest DataContext (lub dziedzicząca po nim, dla silnie typowanych kontekstów). Jednakże nie trudno zadać sobie pytanie - jak skutecznie zarządzać obiektem DataContext, aby zapewnić maksimum wygody i wydajności. W tym wpisie chciałbym właśnie przedstawić rozwiązanie, którego ja używam i które do tej pory u mnie się sprawdza.

Od razu zaznaczam, że nie chodzi mi o kwestie czasu życia samego kontekstu (czy jeden na metodę, wątek, aplikację, itd.), bo zgodnie z MSDN:

In general, a DataContext instance is designed to last for one "unit of work" however your application defines that term. A DataContext is lightweight and is not expensive to create. A typical LINQ to SQL application creates DataContext instances at method scope or as a member of short-lived classes that represent a logical set of related database operations.

Ja przyjąłem zasadę jeden DataContext per metoda.

Problemy z DataContext

Z początku wydawać by się mogło, że goły DataContext nie stwarza żadnych wyzwań. W końcu piszemy:

using (var dc = new MyDbDataContext())
{
    //operacje na bazie
}

i wszystko pięknie działa. Problemy widać już lepiej w poniższym kodzie:

   1: public class App
   2: {
   3:     public static Owner GetOwner(int ownerId)
   4:     {
   5:         using (var dataContext = new MyDbDataContext())
   6:         {
   7:             var loadOptions = new DataLoadOptions();
   8:             loadOptions.LoadWith<Dog>(d => d.Breed);
   9:             dataContext.LoadOptions = loadOptions;
  10:  
  11:             return (from o in dataContext.Owners
  12:                     where o.OwnerId == ownerId
  13:                     select o)
  14:                 .FirstOrDefault();
  15:         }
  16:     }
  17:  
  18:     public static bool CanBeBreeder(int ownerId, string breed)
  19:     {
  20:         using (var dataContext = new MyDbDataContext())
  21:         {
  22:             var loadOptions = new DataLoadOptions();
  23:             loadOptions.LoadWith<Dog>(d => d.Breed);
  24:             dataContext.LoadOptions = loadOptions;
  25:  
  26:             return (from d in dataContext.Dogs
  27:                     where d.OwnerId == ownerId && d.Breed.Name == breed
  28:                     select d).Count() >= 2;
  29:         }
  30:     }
  31:  
  32:     public static void MakeBreeder(int ownerId)
  33:     {
  34:         using (var dataContext = new MyDbDataContext())
  35:         {
  36:             var loadOptions = new DataLoadOptions();
  37:             loadOptions.LoadWith<Dog>(d => d.Breed);
  38:             loadOptions.LoadWith<Dog>(d => d.Owner);
  39:             dataContext.LoadOptions = loadOptions;
  40:             
  41:             var owner = dataContext.Owners.First(o => o.OwnerId == ownerId);
  42:             owner.IsBreeder = true;
  43:             dataContext.SubmitChanges();
  44:         }
  45:     }
  46:  
  47:     static void Main(string[] argv)
  48:     {
  49:         int ownerId = 1;
  50:         using (var ts = new TransactionScope())
  51:         {
  52:             var owner = GetOwner(ownerId);
  53:             if (!owner.IsBreeder)
  54:             {
  55:                 if (CanBeBreeder(ownerId, "Labrador"))
  56:                     MakeBreeder(ownerId);
  57:             }
  58:             ts.Complete();
  59:         }
  60:     }
  61: }

Otóż, powyższy kod:

  • wykorzystuje 3 niezależne połączenia do tej samej bazy (z tym samym ConnectionString),
  • tworzy ciężką i kosztowną transakcję rozproszoną (wymaga włączonej usługi Distibuted Transaction Coordinator),
  • w każdej metodzie definiuje takie same LoadOptions dla DataContext (ale czasami rozszerza ten zbiór o dodatkowe ustawienia),

Innymi słowy - jest długi, mało wydajny i trudny do utrzymania. Od razu widać 2 problemy, z jakimi przyjdzie nam się zmagać:

  1. Jak zarządać połączeniem do bazy danych, aby ograniczyć ich ilość do minimum (a przy okazji unikać transakcji rozproszonych)?
    Idealnym rozwiązaniem byłoby, aby wszystkie operacje w ramach jednego wątku i takiego samego DataContextu odbywały się na tym jednym połączeniu.
  2. Jak zdefiniować domyślne Load Options dla DataContextu, które można rozszerzać?
    Właśnie kwestia rozszerzalności jest najbardziej problematyczna. O ile silnie typowany DataContext jest klasą partial i ma metodę partial OnCreated, w której możemy utworzyć domyślne DataLoadOptions, o tyle już po przypisaniu tych opcji do kontekstu, nie ma możliwości ich zmiany (bez tworzenia DataLoadOptions od nowa).

Propozycja rozwiązania

Moja propozycja opiera się na opakowaniu DataContext przez inną klasę. Zadaniem tej klasy jest zarządzanie połączeniem i Load Options oraz tworzenie odpowiednio skonfigurowanego DataContext. Oto jej kod:

/// <summary>
/// Creates a "DbConnection scope", DataContext and manages its LoadOptions.
/// </summary>
public class MyDbDatabase : IDisposable
{
    #region Private fields
 
    [ThreadStatic]
    private static MyDbDatabase CurrentContext;
    private readonly DataLoadOptionsBuilder loadOptionsBuilder = new DataLoadOptionsBuilder();
    private readonly ConnectionMode mode;
    private readonly MyDbDatabase parent;
    private DbConnection connection;
    private MyDbDataContext context;
    private bool createdConnection;
 
    #endregion
 
    #region Properties
 
    /// <summary>
    /// Gets the current connection.
    /// </summary>
    /// <value>The connection.</value>
    public DbConnection Connection
    {
        get
        {
            if (connection == null)
            {
                if (mode == ConnectionMode.UseExisting)
                {
                    if (parent != null)
                    {
                        connection = parent.Connection;
                    }
                    else
                    {
                        connection = CreateConnection();
                        connection.Open();
                        createdConnection = true;
                    }
                }
                else
                {
                    connection = CreateConnection();
                    createdConnection = true;
                }
            }
            return connection;
        }
    }
 
    /// <summary>
    /// Gets the current DataContext.
    /// </summary>
    /// <value>The DataContext.</value>
    public MyDbDataContext DataContext
    {
        get
        {
            if (context == null)
            {
                context = new MyDbDataContext(Connection)
                              {
                                  LoadOptions = loadOptionsBuilder.BuildDataLoadOptions()
                              };
            }
 
            return context;
        }
    }
 
    #endregion
 
    #region Constructors
 
    /// <summary>
    /// Initializes a new instance of the <see cref="MyDbDatabase"/> class.
    /// </summary>
    /// <param name="mode">The mode.</param>
    public MyDbDatabase(ConnectionMode mode)
    {
        this.mode = mode;
        parent = CurrentContext;
        CurrentContext = this;
        CreateDefaultDataLoadOptions();
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="MyDbDatabase"/> class.
    /// </summary>
    public MyDbDatabase()
        : this(ConnectionMode.UseExisting)
    {
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="MyDbDatabase"/> class.
    /// </summary>
    /// <param name="connection">The connection to use.</param>
    /// <remarks>This method uses ConnectionMode.CreateNew mode.</remarks>
    public MyDbDatabase(DbConnection connection)
        : this(ConnectionMode.CreateNew)
    {
        if (connection == null)
            throw new ArgumentNullException("connection");
 
        this.connection = connection;
        createdConnection = false;
    }
 
    #endregion Constructors  
 
    #region LoadOptions Methods
 
    /// <summary>
    /// Adds LoadWith option to DataLoadOptions.
    /// </summary>
    /// <param name="expr">The expression.</param>
    public void LoadWith(LambdaExpression expr)
    {
        if (context != null)
            throw new InvalidOperationException(
                "LoadOptions for DataContext are frozen and cannot be modified. Methods LoadWith, LoadWith<T>, AssociateWith, AssociateWith<T>, ClearLoadOptions cannot be used after DataContext property of EFSDatabase class has been accessed for the first time.");
        loadOptionsBuilder.Add(new LoadWith(expr));
    }
 
    /// <summary>
    /// Adds LoadWith option to DataLoadOptions.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="expr">The expression.</param>
    public void LoadWith<T>(Expression<Func<T, object>> expr)
    {
        if (context != null)
            throw new InvalidOperationException(
                "LoadOptions for DataContext are frozen and cannot be modified. Methods LoadWith, LoadWith<T>, AssociateWith, AssociateWith<T>, ClearLoadOptions cannot be used after DataContext property of EFSDatabase class has been accessed for the first time.");
        loadOptionsBuilder.Add(new LoadWith<T>(expr));
    }
 
    /// <summary>