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

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>
    /// Adds AssociateWith option to DataLoadOptions.
    /// </summary>
    /// <param name="expr">The expression.</param>
    public void AssociateWith(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 AssociateWith(expr));
    }
 
    /// <summary>
    /// Adds AssociateWith option to DataLoadOptions.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="expr">The expression.</param>
    public void AssociateWith<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 AssociateWith<T>(expr));
    }
 
    /// <summary>
    /// Clears all currently set load options.
    /// </summary>
    public void ClearLoadOptions()
    {
        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.Clear();
    }
 
    #endregion
 
    #region IDisposable Members
 
    public void Dispose()
    {
        if (context != null)
            context.Dispose();
 
        if (CurrentContext != this)
            throw new InvalidOperationException(
                "MyDbDatabase object is disposed before dependant MyDbDatabase object(s). This indicates a programming error.");
        CurrentContext = parent;
        if (createdConnection && connection != null)
            connection.Dispose();
    }
 
    #endregion
 
    #region Custom methods
 
    private const string DefaultConnectionStringName = "ConsoleApplication1.Properties.Settings.DbConnectionString";
 
    /// <summary>
    /// Creates the connection.
    /// </summary>
    /// <returns>New connection.</returns>
    public DbConnection CreateConnection()
    {
        //connection is created here
        return new SqlConnection(
            ConfigurationManager.ConnectionStrings[
                DefaultConnectionStringName].ConnectionString);
    }
 
    /// <summary>
    /// Creates the default data load options.
    /// </summary>
    public void CreateDefaultDataLoadOptions()
    {
        //default DataLoadOptions go here
        loadOptionsBuilder.Add(new LoadWith<Dog>(d => d.Breed));
    }
 
    #endregion
}
 
/// <summary>
/// The connection mode.
/// </summary>
public enum ConnectionMode
{
    /// <summary>
    /// Existing connection is used. If no such exists a new is created.
    /// </summary>
    UseExisting,
    /// <summary>
    /// A new connection is created.
    /// </summary>
    CreateNew
}
 
/// <summary>
/// Builds <see cref="DataLoadOptions"/> object.
/// </summary>
public class DataLoadOptionsBuilder : List<IDataLoadOptionsBuilderItem>
{
    /// <summary>
    /// Initializes a new instance of the <see cref="DataLoadOptionsBuilder"/> class.
    /// </summary>
    public DataLoadOptionsBuilder()
    {
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="DataLoadOptionsBuilder"/> class.
    /// </summary>
    /// <param name="collection">The collection whose elements are copied to the new list.</param>
    /// <exception cref="T:System.ArgumentNullException">
    ///     <paramref name="collection"/> is null.</exception>
    public DataLoadOptionsBuilder(IEnumerable<IDataLoadOptionsBuilderItem> collection) :
        base(collection)
    {
    }
 
    /// <summary>
    /// Builds the data load options.
    /// </summary>
    /// <returns><see cref="DataLoadOptions"/> object.</returns>
    public DataLoadOptions BuildDataLoadOptions()
    {
        var options = new DataLoadOptions();
        for (int i = 0; i < Count; i++)
        {
            this[i].BuildDataLoadOptions(options);
        }
 
        return options;
    }
}
 
/// <summary>
/// Interface for item that is able to partially prepare <see cref="DataLoadOptions"/> object.
/// </summary>
public interface IDataLoadOptionsBuilderItem
{
    /// <summary>
    /// Builds the data load options.
    /// </summary>
    /// <param name="options">The options.</param>
    void BuildDataLoadOptions(DataLoadOptions options);
}
 
/// <summary>
/// Sets AssociateWith option within <see cref="DataLoadOptions"/>.
/// </summary>
public class AssociateWith : IDataLoadOptionsBuilderItem
{
    private readonly LambdaExpression _expression;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="AssociateWith"/> class.
    /// </summary>
    /// <param name="expression">The expression.</param>
    public AssociateWith(LambdaExpression expression)
    {
        if (expression == null)
            throw new ArgumentNullException("expression");
        _expression = expression;
    }
 
    #region IDataLoadOptionsBuilderItem Members
 
    /// <summary>
    /// Builds the data load options.
    /// </summary>
    /// <param name="options">The options.</param>
    public void BuildDataLoadOptions(DataLoadOptions options)
    {
        options.AssociateWith(_expression);
    }
 
    #endregion
}
 
/// <summary>
/// Sets AssociateWith option within <see cref="DataLoadOptions"/>.
/// </summary>
public class AssociateWith<T> : IDataLoadOptionsBuilderItem
{
    private readonly Expression<Func<T, object>> _expression;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="AssociateWith&lt;T&gt;"/> class.
    /// </summary>
    /// <param name="expression">The expression.</param>
    public AssociateWith(Expression<Func<T, object>> expression)
    {
        if (expression == null)
            throw new ArgumentNullException("expression");
        _expression = expression;
    }
 
    #region IDataLoadOptionsBuilderItem Members
 
    /// <summary>
    /// Builds the data load options.
    /// </summary>
    /// <param name="options">The options.</param>
    public void BuildDataLoadOptions(DataLoadOptions options)
    {
        options.AssociateWith(_expression);
    }
 
    #endregion
}
 
/// <summary>
/// Sets LoadWith option within <see cref="DataLoadOptions"/>.
/// </summary>
public class LoadWith : IDataLoadOptionsBuilderItem
{
    private readonly LambdaExpression _expression;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="LoadWith"/> class.
    /// </summary>
    /// <param name="expression">The expression.</param>
    public LoadWith(LambdaExpression expression)
    {
        if (expression == null)
            throw new ArgumentNullException("expression");
        _expression = expression;
    }
 
    #region IDataLoadOptionsBuilderItem Members
 
    /// <summary>
    /// Builds the data load options.
    /// </summary>
    /// <param name="options">The options.</param>
    public void BuildDataLoadOptions(DataLoadOptions options)
    {
        options.LoadWith(_expression);
    }
 
    #endregion
}
 
/// <summary>
/// Sets LoadWith option within <see cref="DataLoadOptions"/>.
/// </summary>
public class LoadWith<T> : IDataLoadOptionsBuilderItem
{
    private readonly Expression<Func<T, object>> _expression;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="LoadWith&lt;T&gt;"/> class.
    /// </summary>
    /// <param name="expression">The expression.</param>
    public LoadWith(Expression<Func<T, object>> expression)
    {
        if (expression == null)
            throw new ArgumentNullException("expression");
        _expression = expression;
    }
 
    #region IDataLoadOptionsBuilderItem Members
 
    /// <summary>
    /// Builds the data load options.
    /// </summary>
    /// <param name="options">The options.</param>
    public void BuildDataLoadOptions(DataLoadOptions options)
    {
        options.LoadWith(_expression);
    }
 
    #endregion
}

Oczywiście powyższy kod jest przystosowany do wykorzystywania MyDbDataContext jako DataContextu oraz ściśle określonych domyślnych DataLoadOptions i konkretnego connection string'a. Jeżeli ktoś będzie chciał zastosować ten kod u siebie, to musi dokonać podmiany tych wartości.

Przyjrzyjmy się kodowi aplikacji z zastosowaniem nowej klasy:

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

Zyski są natychmiastowe:

  • kod się skrócił (o 10 wierszy),
  • wszystko odbywa się w ramach jednego połączenia, bez jawnego przekazywania obiektu DbConnection,
  • zostanie utworzona jedna lekka transakcja lokalna (brak potrzeby uruchamiania MS DTC),
  • domyślne LoadOptions są zdefiniowane w jednym miejscu i ładowane automatycznie,
  • mamy możliwość rozszerzenia domyślnych DataLoadOptions o nowe.
Podstawowe zastosowanie

Najprostsze zastosowanie wygląda następująco:

using (var db = new MyDbDatabase())
{
    var top10Owners = db.DataContext.Owners.Take(10);
    //...
}

Wówczas mamy gwarancję, że kod wewnątrz using będzie używał jednego połączenia do bazy (nawet w wywoływanych metodach). Ponadto, jeżeli metoda wywołująca aktualną funkcję też definiowała MyDbDatabase, to zostanie wykorzystanie nadrzędne wspólne połączenie. Można oczywiście zmienić to zachowanie, żądając utworzenia nowego:

using (var db = new MyDbDatabase(ConnectionMode.CreateNew))
{
    //operacje na bazie
}

lub też jawne przekazując połączenie (uwaga - w tym wypadku MyDbDatabase nie przejmuje "na własność" połączenia i my nadal mamy obowiązek jego zamknięcia):

using (var db = new MyDbDatabase(new SqlConnection("...")))
{
    //operacje na bazie
}

Definiowanie DataLoadOptions

Domyślne DataLoadOptions są określane w metodzie MyDbDatabase.CreateDefaultDataLoadOptions() i tutaj należy umieszczać własne wpisy. Robi się to wykorzystując metody LoadWith i AssociateWith klasy MyDbDatabase (nie DataContextu!). Analogicznie rozszerzamy domyślne Load Options:

using (var db = new MyDbDatabase())
{
    db.LoadWith<Dog>(d => d.Owner);
    //...
}

W celu zdefiniowania własnych - zupełnie innych Load Options, możemy wyczyścić domyślne ustawienia korzystając z metody db.ClearLoadOptions(). Należy pamiętać o jednej rzeczy - wszystkie zmiany należy dokonywać przed pierwszym odwołaniem do właściwości DataContext - wówczas tworzony jest obiekt DataContext i ustawiane ma LoadOptions.

Jak to działa?

Otóż, założenie jest proste:

  1. MyDbDatabase posiada pole:
    [ThreadStatic] private static MyDbDatabase CurrentContext;
    Dzięki temu ma dostęp do bierzącego MyDbDatabase w ramach aktualnego wątku (więcej o ThreadStaticAttribute znajduje się poniżej). Dzięki temu w momencie utworzenia, może pobrać połączenie z nadrzędnego MyDbDatabase i wykorzystać je w ramach własnego DataContextu.
  2. MyDbDatabase implementuje IDisposable, dzięki czemu może zostać użyte wewnątrz konstrukcji using w C#. To sprawia, iż w wygodny sposób możemy kontrolować zakres w jakim MyDbDatabase ma działać (od wywołania konstruktora do wywołania Dispose(().
  3. Został zaimplementowany dodatkowy mechanizm pozwalający przechowywać strukturę DataLoadOptions bez budowania go. DataLoadOptions jest tworzony i konfigurowany, dopiero w momencie tworzenia DataContext, czyli w momencie pierwszego odwołania do właściwości MyDbDatabase.DataContext.

Inne uwagi

Przy zastosowaniu MyDbDatabase należy zwrócić uwagę na kilka spraw:

  1. Ta klasa musi być używana wewnątrz dyrektywy using(). W innym wypadku, należy pamiętać o wywołaniu Dispose().
  2. Obiekty MyDbDatabase muszą być usuwane (nie wiem jak to przetłumaczyć - chodzi o wywołanie Dispose) w kolejności odwrotnej niż zostały utworzone. Prościej mówiąc, podrzędny MyDbDatabase musi być usunięty przed nadrzędnym. W innym wypadku prędzej czy później zostanie wyrzucony wyjątek.
  3. DataLoadOptions mogą być ustawiane do pierwszego odwołania się do właściwości MyDbDatabase.DataContext.
  4. Każda instancja MyDbDatabase tworzy nowy, niezależny DataContext. Dziedziczone jest tylko połączenie.
  5. Instancja MyDbDatabase nie jest przeznaczona do bycia współdzieloną przez wiele wątków. Ze swojej natury działa w ramach jednego wątku.

Krótko o ThreadStaticAttribute

Słowem wstępu - zastanawiałem się, czy pisać na ten temat, bo wydawało mi się, że jest on dość dobrze znany większości programistów, więc taka informacja nie będzie za wiele wnosiła. Postanowiłem sprawdzić. Otóż, okazuje się, że po polsku nie ma o ThreadStaticAttribute żadnej wzmianki w Internecie, nie licząc pozycji w spisie treści dwóch książek (źródło: Google)! Byłem tym faktem zaskoczony, ale też pocieszony, bo mogę być pierwszy ;).

Jeżeli utworzymy pole statyczne, to jego wartość będzie współdzielona przez wszystkie wątki w ramach aplikacji (a dokładnie AppDomain). A gdybyśmy chcieli, żeby każdy wątek miał swoją, całkowicie niezależną wartość wewnątrz danego pola statycznego? Nic prostszego - wystarczy opisać go atrybutem ThreadStaticAttribute. Wówczas dla każdego wątku nasze pole będzie przyjmowało oddzielną, niezależną wartość. Przykład:

   1: class Program
   2: {
   3:     [ThreadStatic] 
   4:     private static string ThreadName;
   5:  
   6:     private static void ThreadMain(object name)
   7:     {
   8:         ThreadName = (string)name;
   9:         Thread.Sleep(100);
  10:         Console.WriteLine(ThreadName);
  11:     }
  12:  
  13:     static void Main(string[] args)
  14:     {
  15:         var th1 = new Thread(ThreadMain);
  16:         var th2 = new Thread(ThreadMain);
  17:         th1.Start("Thread 1");
  18:         th2.Start("Thread 2");
  19:         Console.ReadKey();
  20:     }
  21: }

Efekt będzie następujący:

ex1

Przy okazji mała uwaga. Używając ThreadStatic, należy unikać deklaracji z inicjalizatorem (to samo tyczy się konstruktora statycznego):

[ThreadStatic] private static string ThreadName = "Tekst";

Wbrew oczekiwaniom, nie będzie tak, że każdy nowy wątek będzie miał inicjalizowane pola ThreadName wartością "Tekst". Stanie się tak jedynie dla wątku, w którym zostanie uruchomiony konstruktor statyczny klasy, a więc nastąpi to tylko jednokrotnie. Pozostałe wątki na początku będą miały wartość domyślną dla typu - w przypadku string będzie to null.

 

PS. Jeżeli ktoś zauważy jakiś błąd w moim kodzie to niech krzyczy:).

EDIT - poprawki po komentarzu apl.

Opublikowane 18 czerwca 2008 08:25 przez jakubin
Filed under: , ,

Komentarze:

# 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

18 czerwca 2008 08:39 by Zine.NET

# re: Co z tym DataContext?

Używam podobnej technologi w połączeniu ze starym (dobrym?) czystym ADO.NET. Różnica polega tylko na tym, że aby uzyskać efekt atrybutu ThreadStatic użyłem "thread specific data". W danych specyficznych wątku trzymam IDbConnection i IDbTransaction. Świetna sprawa. Dziwne tylko, że ludzie od ADO.NET nie zaimplementowali tego we frejmłorku.

18 czerwca 2008 10:05 by simon_v

# re: Co z tym DataContext?

@simon_v: ThreadStatic wykorzystuje Thread Local Storage ("thread specific data"), więc rozwiazanie sprowadza się do tego samego.

Do trzymania transakcji równie dobry i pewniejszy jest TransactionScope, który ja preferuję.

Co do .NETa - mi też bardzo brakuje takiego mechanizmu wbudowanego w platformę. Zwłaszcza przy wszystkich wbudowanych providerach (Membership, Roles, itd.), które zawsze tworzą swoje własne połączenie. W przypadku transakcji to boli...

18 czerwca 2008 10:30 by jakubin

# re: Co z tym DataContext?

Ja się przyznam, że o ThreadStaticAttribute nie wiedziałem ;) Dzięki!

18 czerwca 2008 10:30 by nuwanda

# re: Co z tym DataContext?

Ja bym jeszcze dorzucil od siebie tego linka:

http://www.dotnetlog.com/archive/2008/03/18/best-practice-and-effective-way-of-using-datacontext-in-linq.aspx

jako ze sam przechodzilem przez ten etap i zastanawialem sie jak z tego korzystac by mialo to rece i nogi... krotko zostalo opisanych kilka roznych praktyk korzystania z DataContext oraz rozne za i przeciw...

18 czerwca 2008 11:46 by yaceq

# re: Co z tym DataContext?

@yaceq: Dzięki za tego linka - ciekawy wpis. Dotyczy on akurat czasu życia DataContext, o czym nie chciałem pisać ("Od razu zaznaczam, że nie chodzi mi o kwestie czasu życia samego kontekstu"), ale jest to temat równie ważny.

Od razu chcę wyjaśnić jedną rzecz, żeby nie było wątpliwości. Rozwiązanie które ja stosuję, tworzy w każdej instancji MyDbDatabase *nowy* DataContext. Dziedziczone jest jedynie połączenie. Dzięki temu unikamy magicznych update'ów encji, gdy w jednej metodzie zostanie zmodyfikowana encja bez wywoływania SubmitChanges (tu spodziewamy się, że zmiany pójdą w niebyt), a w metodzie ją wywołującej wykonamy SubmitChanges. Przykład:

private void ChangeAndForget(MyDbDataContext db)

{

   var owner = db.Owners.First();

   owner.FirstName = "Heniek";

   //rezygnujemy z zapisania zmian - brak SubmitChanges()

}

private void SubmitGhostChanges()

{

   var db = new MyDbDataContext();

   ChangeAndForget(db);

   //...

   db.SubmitChanges();

}

Po wywołaniu SubmitGhostChanges zmiany z ChangeAndForget zostaną jednak zapisane.

18 czerwca 2008 12:22 by jakubin

# re: Co z tym DataContext?

Świetne. Podoba mnie sie. Musze to jednak przekleic do VS i przeanalizowac dokladniej :) Poza tym konstruktor jeszcze nie jest dla mnie do konca jasny :)

BTW. Rozumiem, ze twoje rozwiazanie nie zaleca korzystania z kilku DC jednoczesnie?

using (var db = new MyDbDatabase())

using (var db2 = new MyOtherDbDatabase())

{

}

Preferowane jest przetwarzanie po kolei?

18 czerwca 2008 12:54 by M

# re: Co z tym DataContext?

Świetne. Podoba mnie sie. Musze to jednak przekleic do VS i przeanalizowac dokladniej :) Poza tym konstruktor jeszcze nie jest dla mnie do konca jasny :)

BTW. Rozumiem, ze twoje rozwiazanie nie zaleca korzystania z kilku DC jednoczesnie?

using (var db = new MyDbDatabase())

using (var db2 = new MyOtherDbDatabase())

{

}

Preferowane jest przetwarzanie po kolei?

18 czerwca 2008 12:55 by M

# re: Co z tym DataContext?

@M: Korzystanie z kilku różnych jest jak najbardziej dozwolone i nie ma wpływu na siebie (oczywiście każdy skorzysta wówczas z oddzielnego połączenia).

18 czerwca 2008 14:00 by jakubin

# re: Co z tym DataContext?

Przypomniało mi się rozwiązanie, które kiedyś pokazał mi znajomy, a które dotyczyło ADO.NET. Wyglądało to mniej więcej w ten sposób:

using (new ConnectionScope()) {

  SqlConnection connection = Connection.Current;

  SqlCommand command = connection.CreateCommand();

  ...

}

Idea była taka sama - zapobiec promowaniu transakcji lokalnych poprzez współdzielenie raz otwartego połączenia - zaś model obiektowy zaprojektowany na wzór System.Transactions (ConnectionScope <-> TransactionScope, Transaction.Current <-> Connection.Current). Pod maską wyglądało to praktycznie tak, jak u Kuby - ThreadStaticAttribute i struktura typu LIFO.

Tak się składa, że akurat wtedy po raz pierwszy usłyszałem o ThreadStaticAttribute:

- A Connection.Current jak realizujesz?

- Przez ThreadStatic.

- ...yyy... N-no tak, jasne.

Najwyraźniej jest to jakieś wzorcowe zastosowanie dla tego atrybutu, więc w sumie dobrze, że w tekście znalazła się o nim wzmianka ;).

P.S. Wydaje mi się, że kod został lekko nadpsuty przez automat do kolorowania składni, np. linia:

loadOptionsBuilder.Add(new LoadWith(BLOCKED EXPRESSION;

chyba nie miała tak wyglądać.

P.S.2 W komunikatach wyjątków przemyciło się EFSDatabase ;).

18 czerwca 2008 16:49 by apl

# re: Co z tym DataContext?

@apl: Olek, po raz kolejny przekonałem się, że na Twoje komentarze zawsze można liczyć :). Dzięki za te uwagi - już wprowadziłem zmiany. Co do BLOCKED EXPRESSION to nie wiem czemu, ale community server tak wyświetla wyrażenie "(expression)".

18 czerwca 2008 18:42 by jakubin

# re: Co z tym DataContext?

Pobierzenie przeczytając bardzo podobną rzecz wymyśliłem... też wykorzystałem TLS do tego dorzuciłem tam coś w rodzaju reference countera zliczającego bloki we/wy using. Zeby przez stos wywołań korzystać z tego samego data-contekstu. Oprucz tego klasa z dostępem do contekstu z TLS przez statyczne odwołanie. Ehhh...

20 czerwca 2008 16:23 by Neo

# re: Co z tym DataContext?

@neo: Ty wymyśliłeś, ja wymyśliłem, a przed nami jeszcze wielu :). Ja opisałem tylko jak zbiorczo można rozwiązać problem wykorzystania jednego połączenia do bazy oraz budowania DataLoadOptions. Nie przypisuje sobie ani pierwszeństwa ani genialności ;).

20 czerwca 2008 16:55 by jakubin

# Co z tym DataContext?

Dziękujemy za publikację - Trackback z dotnetomaniak.pl

10 listopada 2009 12:48 by dotnetomaniak.pl
Komentarze anonimowe wyłączone

About jakubin

MVP w kategorii C#, MCP. Aktualnie pracuje w Webstruments.pl jako programista C#.