LINQ to SQL používá (používalo) docela dost lidí (podle mě). Mezi “uživatele” patřím i já, ale jelikož mě docela zajímá oblast návrhových vzorů a různých “best practices” tak mě zajímalo, zda něco podobného existuje i pro LINQ to SQL. Něco ve smyslu jak správně vytvořit datovou vrstu s podporou LINQ to SQL. Našel jsem plno zajímavých odkazů, ale ne přesně to co jsem chtěl, tak jsem jednotlivé segmenty kódu od jiných autorů dal dohromady + přidal svojí myšlenku a vzniklo něco co mě docela zaujalo, ale ještě jsem to nestihl nikde použít :-)

Podle článku http://oddiandeveloper.blogspot.com/2009/01/linq-best-practice-1-repository-pattern.html jsem trošku upravil Repository pattern a vytvořil si následující rozhraní a třídu:

public interface IRepository<T> where T:class 
{
    /// <summary>
    /// Vrati vsechny prvky
    /// </summary>
    /// <returns></returns>
    IEnumerable<T> All();
 
    /// <summary>
    /// Vrati vsechny prvky podle zadane podminky
    /// </summary>
    /// <param name="exp">Podminka pro hledani</param>
    /// <returns></returns>
    IEnumerable<T> FindAll(System.Linq.Expressions.Expression<Func<T, bool>> exp);
 
    /// <summary>
    /// Vrati konkretni prvek
    /// </summary>
    /// <param name="exp"></param>
    /// <returns></returns>
    T Single(System.Linq.Expressions.Expression<Func<T, bool>> exp);
 
    /// <summary>
    /// Vrati prvni prvek v kolekci
    /// </summary>
    /// <returns></returns>
    T First();
 
    /// <summary>
    /// Vrati prvni prvek podle zadane podminky
    /// </summary>
    /// <param name="exp">Podminka pro hledani</param>
    /// <returns></returns>
    T First(System.Linq.Expressions.Expression<Func<T, bool>> exp);
 
    /// <summary>
    /// Vrati posledni prvek v kolekci
    /// </summary>
    /// <returns></returns>
    T Last();
 
    /// <summary>
    /// Oznaci entitu jako pripravenou pro smazani, po zavolani SaveAll bude entita vymazana z database
    /// </summary>
    /// <param name="entity"></param>
    void Delete(T entity);
 
    /// <summary>
    /// Vytvori novou instanci entity, po zavolani SaveAll bude entita vlozena do databaze
    /// </summary>
    /// <returns></returns>
    T CreateInstance();
 
    /// <summary>
    /// Ulozi vsechny provedene zmeny
    /// </summary>
    void SaveAll();
}
public class Repository<T> : IRepository<T> where T:class 
{
  public static Repository<T> Instance
  {
      get { return new Repository<T>();}
  }
 
  #region IRepository<T> Members
 
  public IEnumerable<T> All()
  {
      return GetTable();
  }
 
  public IEnumerable<T> FindAll(System.Linq.Expressions.Expression<Func<T, bool>> exp)
  {
      return GetTable().Where(exp);
  }
 
  public T Single(System.Linq.Expressions.Expression<Func<T, bool>> exp)
  {
      return FindAll(exp).SingleOrDefault();
  }
 
  public T First()
  {
      return All().FirstOrDefault();
  }
 
  public T First(System.Linq.Expressions.Expression<Func<T, bool>> exp)
  {
      return FindAll(exp).FirstOrDefault();
  }
 
  public T Last()
  {
      return All().LastOrDefault();
  }
 
  public void Delete(T entity)
  {
      GetTable().DeleteOnSubmit(entity);
  }
 
  public T CreateInstance()
  {
      T entity = Activator.CreateInstance<T>();
      GetTable().InsertOnSubmit(entity);
      return entity;
  }
 
  public void SaveAll()
  {
      DbDataContext.Instance.SubmitChanges();
  }
 
  #endregion
 
  private Table<T> GetTable()
  {
      return DbDataContext.Instance.GetTable<T>();
  }
}

Jakmile budu chtít provést nějaký dotaz, vždy volám metodu GetTable, která volá vlastnost DbDataContext o které zatím nic nevíme. Tato vlastnost vrátí instanci našeho DataContextu, ale o tom trošku později.

Když mám toto hotové, je důležité si uvědomit kdy budeme chtít vytvářet instanci DataContext objektu. Někde píšou při každém provedení dotazu, jinde na úrovni UI, nebo třeba jeden pro celou aplikaci. Já jsem zastáncem vytvoření jednoho datacontextu pro celou aplikaci. Rick Strahl uvedl na svém blogu zajímavý článek Linq to SQL DataContext Lifetime Management právě o životnosti DataContextu. Opět použiju kousek cizího kódu a tedy třídu, která se nám postará o DataContext

/// <summary>
/// Vytvori/vrati data context svazany s threadem nebo web requestem
/// </summary>
internal class DataContextFactory
{
   #region Constants
 
   /// <summary>
   /// Vychozi klic pouzivany pro ukladani datakontextu
   /// </summary>
   private const string DefaultKey = "DATA_CONTEXT";
   static readonly object Locker = new object();
 
   #endregion
 
   #region Public methods
 
   /// <summary>
   /// Vrati/vytvori data kontext svazany bud s threadem nebo web requestem
   /// </summary>
   /// <param name="key">klic pro ulozeni datakontextu</param>
   /// <returns></returns>
   public static DataContext GetScopedDataContext(string key)
   {
       lock (Locker)
       {
           if (HttpContext.Current != null)
               return (DataContext)GetWebRequestScopedDataContextInternal(key);
 
           return (DataContext)GetThreadScopedDataContextInternal(key);
       }
   }
 
   /// <summary>
   /// Vrati/vytvori data kontext svazany bud s threadem nebo web requestem
   /// </summary>
   /// <returns></returns>
   public static DataContext GetScopedDataContext()
   {
       lock (Locker)
       {
           if (HttpContext.Current != null)
               return (DataContext)GetWebRequestScopedDataContextInternal(null);
 
           return (DataContext)GetThreadScopedDataContextInternal(null);
       }
   }
 
   #endregion
 
   #region Other methods
 
   private static object GetThreadScopedDataContextInternal(string key)
   {
       if (key == null)
           key = DefaultKey; //+ Thread.CurrentContext.ContextID;
 
       var threadData = Thread.GetNamedDataSlot(key);
       object context = null;
       if (threadData != null)
           context = Thread.GetData(threadData);
 
       if (context == null)
       {
           context = CreateInstance();
           if (context != null)
           {
               if (threadData == null)
                   threadData = Thread.AllocateNamedDataSlot(key);
 
               Thread.SetData(threadData, context);
           }
       }
 
       return context;
   }
 
   private static object CreateInstance()
   {
       DataContext context = null;
       if (ConfigurationManager.GetSection("DataContextSection") != null)
       {
           var section = ConfigurationManager.GetSection("DataContextSection") as DataContextSection;
           string connectionString = ConfigurationManager.ConnectionStrings[section.ConnectionString].ConnectionString;
           string strType = section.Type;
           string strAsm = section.Assembly;
           ObjectHandle handle = Activator.CreateInstance(strAsm, strType);
           if (handle != null)
           {
               object instance = handle.Unwrap();
               if (instance != null)
               {
                   Type type = instance.GetType();
                   if (connectionString == null)
                       context = (DataContext)Activator.CreateInstance(type);
                   else
                       context = (DataContext)Activator.CreateInstance(type, connectionString);
                   context.Log = Console.Out;
               }
           }
       }
       return context;
   }
 
 
   private static object GetWebRequestScopedDataContextInternal(string key)
   {
       object context;
 
       if (HttpContext.Current == null)
       {
           context = CreateInstance();
           return context;
       }
       // vytvori unikatni klic pro webrequest/context
       if (key == null)
           key = DefaultKey + HttpContext.Current.GetHashCode().ToString("x"); //+ Thread.CurrentContext.ContextID; 
       context = HttpContext.Current.Items[key];
       if (context == null)
       {
           context = CreateInstance();
           if (context != null)
               HttpContext.Current.Items[key] = context;
       }
       return context;
   }
 
 
   #endregion
}
internal class DbDataContext
{
    public static DataContext Instance
    {
        get { return DataContextFactory.GetScopedDataContext(); }
    }
 
}

Když se podíváte do třídy DataContextFactory na metodu CreateInstance tak uvidíte, že se zde načítají hodnoty z konfiguračního souboru.

Už bylo dost kopírování :-) a teď přichází na řadu to vylepšení, které mě napadlo. Když vytvářím totiž projekt, který bude používat LINQ to SQL tak vždycky musím řešit životnost mého DataContextu, což je otravné a já bych to chtěl mít pouze v dll-ce, kterou bych si přidal do projektu a do configu nastavil pouze potřebné věci, o zbytek už by se pak starala ta moje dll-ka. Konfigurační soubor má následující vlasnosti:

  • Type – Namespace.MyDataContext (typ datacontextu, abych mohl vytvořit instanci)
  • Assemlby – název assembly ve které se datacontext nachází
  • ConnectionString – název connection stringu, který se vytvoři po přidání LINQ to SQL do projektu

A třída, která má tohle na starost vypadá následovně:

internal class DataContextSection : ConfigurationSection
{
    [ConfigurationProperty("Type")]
    public string Type
    {
        get { return (string)this["Type"]; }
        set { this["Type"] = value;}
    }
 
    [ConfigurationProperty("Assembly")]
    public string Assembly
    {
        get { return (string)this["Assembly"]; }
        set { this["Assembly"] = value; }
    }
 
    [ConfigurationProperty("ConnectionString")]
    public string ConnectionString
    {
        get { return (string)this["ConnectionString"]; }
        set { this["ConnectionString"] = value; }
    }
}

Nyní je moje dll-ka hotova a můžeme jí použít jak třeba ve WinForm tak WebForm:

WinForm:

Přidám si referenci na mojí dll-ku a upravím pouze konfigurační soubor:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
      <section name="DataContextSection" type="DAL.DataContextSection, DAL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    </configSections>
    <connectionStrings>
        <add name="DAL.Desktop.Properties.Settings.KnihyConnectionString"
            connectionString="Data Source=.;Initial Catalog=Knihy;Integrated Security=True"
            providerName="System.Data.SqlClient" />
    </connectionStrings>
  <DataContextSection ConnectionString="DAL.Desktop.Properties.Settings.KnihyConnectionString"
                      Type="DAL.Desktop.DataClasses1DataContext"
                      Assembly="DAL.Desktop, Version=1.0.0.0"/> 
</configuration>

V configu mám connection string, který se jmenuje DAL.Desktop.Properties.Settings.KnihyConnectionString. Nyní pouze přidám mojí sekci DataContextSection, kde vyplním název connection stringu, který je využíván DataContextem, typ a assembly a můžu začít tvořit vlastní část programu, která se bude starat o přístup do databáze, např.

  1. Získání všech položek z konkrétní tabulky
    1. var autori = Repository<Autori>.Instance.All().ToList();
      dataGridView1.DataSource = autori;
  2. Vytvoření nového záznamu
    1. var diskuze = Repository<Diskuze>.Instance.CreateInstance();
      diskuze.Text = "erer";
      diskuze.IdAutor = 1;
      Repository<Diskuze>.Instance.SaveAll();
  3. Vyhledávání podle různých kritérií
    1. var diskuze = Repository<Diskuze>.Instance.FindAll(p => p.Text.Contains("a")).ToList();
      dataGridView1.DataSource = diskuze;
  4. Smazání záznamu
    1. var entity = Repository<Diskuze>.Instance.Single(p => p.Text == "erer");
      Repository<Diskuze>.Instance.Delete(entity);
      Repository<Diskuze>.Instance.SaveAll();

To je vše a nyní bych uvítal vaše názory. Znáte nějaké “Best practices” pro použití LINQ to SQL ?