août 2008 - Messages
Quel rapport entre DataSet, une architecture multi-tiers et Linq to SQL ?
Pas facile ? Si si, juste un Designer qui génère tout pour nous dans Visual Studio 2008, que ce soit pour un DataSet typé ou pour un DataContext Linq To SQL.
Oui mais voilà, là où ça se complique (si on peut dire) c' est lorsque qu' on veut faire une petite architecture 3 tiers (soyons pas fou, restons simple)
Dans Visual Studio 2008, le Designer du DataSet est capable de se générer dans 2 projets distincts.
- Un projet contenant les DataTables typés.
- Un projet contenant les TableAdapters typés.
Tout ça grâce à la petite option magique : DataSet Project

Voilà à quoi ressemble ma solution :
C' est beau, c' est propre et ça marche.
Alors, je vous le demande, Pourquoi ils n'ont pas fait pareil pour Linq To SQL !!!
J' avais espéré secrètement que le SP1 de Visual Studio 2008 apporte cette feature, mais ce n'est pas le cas (si je me trompe, montrez moi, je serai super heureux si c'était le cas)
Alors comment on fait aujourd'hui ? eh ben à la main, on passe le DataContext en internal et on copie colle le DataContext dans l'assembly service en le repassant en public.
Et si on rajoute des tables au schéma, bah on recommence l'opération.
C'est pas critique, mais je trouve ça dommage ...
Voilà mon coup de gueule du jour :)
Et voilà, aprés la sortie en release de Sql Server 2008, voici venir aujourd'hui le service pack 1 de Visual Studio 2008 (et par la même occase le sp1 du fx 3.5)
http://msdn.microsoft.com/en-us/vstudio/cc533448.aspx
Et hop !
Et voilà, SQL Server 2008 est enfin sortie en Release 1.0
Allez Hop, un ti tour sur MSDN et voilà la bonne surprise !
Allez, va falloir faire péter la bande passante ! 
Ce post fait suite au post d'hier, au sujet du multi threading avec Sql Server CE
Dans l'exemple, nous avons démontrer que nous ne pouvions pas faire de multi threading et que nous remontions de vieilles exceptions pas extraordinaire.
D'ailleurs, j'étais conforté par cette idée, au vu d'un post de Ayende quand celui ci cherchait une base de donnée embarquée.
En fait il existe une solution.
Sql Server CE accepte le multi threading, à partir du moment où chaque thread possède sa propre SqlCeConnection.
Mais me direz vous, ça parait normal que chaque opération crée une nouvelle Connection, réflexe d'utilisation de Sql Server et son pool de connexion.
Nous n'en avons pas sur Sql Server CE.
Et là où c'est balot, c'est que ce n'est ni Sql Server CE, ni mes Services asynchrones qui étaient en erreur .. mais Linq to Sql et ma petite classe static Manager, qui est chargée de me rapatrier une connexion SqlCeConnection.
Pour expliquer ce geste de folie, un DataContext Linq nécessite en constructeur une connexion (SqlCeConnection dans mon cas)
J'avais donc écrit un truc du genre:
private static SqlCeConnection configurationDbConnection;
/// <summary>
///Get Connection From Configuration File if any
/// </summary>
public static SqlCeConnection ConfigurationDbConnection
{
get
{
if(configurationDbConnection != null)
return configurationDbConnection;
ConnectionStringSettings connection =
ConfigurationManager.ConnectionStrings["LocalDatabaseConnection"]
as ConnectionStringSettings;
if (connection == null && ConfigurationManager.ConnectionStrings.Count > 1 &&
ConfigurationManager.ConnectionStrings[1].ProviderName == "Microsoft.SqlServerCe.Client.3.5")
connection = ConfigurationManager.ConnectionStrings[1] as ConnectionStringSettings;
if (connection == null)
throw new ApplicationException("No Connection String in application configuration file");
configurationDbConnection = new SqlCeConnection(connection.ConnectionString);
return configurationDbConnection;
}
}
On voit que je stocke la connection créée dans une variable statique.
Monumentale ERREUR !!
Chacun des threads créant son propre DataContexte, va utiliser la MÊME Connexion qu' un autre thread (static inside)
Pour être sûr que chaque DataContexte crée sa propre connexion, il suffit juste d' appeler son constructeur avec un paramètre non pas une connexion, mais juste une chaîne de connexion.
Un petit coup de reflector, nous montre qu'il va alors bien créer une nouvelle connexion :
provider.CreateConnection() appellant le DbProviderFactory de SqlServerCe, celui ci retournant un joli new SqlCeConnection().
Du coup, mon petit code de mon Manager devient :
private static ConnectionStringSettings connectionStringSettings;
/// <summary>
/// Get Connection From Configuration File if any
/// </summary>
public static SqlCeConnection GetConfigurationDbConnection()
{
if (connectionStringSettings == null)
{
connectionStringSettings =
ConfigurationManager.ConnectionStrings["LocalDatabaseConnection"]
as ConnectionStringSettings;
if (connectionStringSettings == null && ConfigurationManager.ConnectionStrings.Count > 1 &&
ConfigurationManager.ConnectionStrings[1].ProviderName == "Microsoft.SqlServerCe.Client.3.5")
connectionStringSettings = ConfigurationManager.ConnectionStrings[1] as ConnectionStringSettings;
if (connectionStringSettings == null)
throw new ApplicationException("No Connection String in application configuration file");
}
return new SqlCeConnection(connectionStringSettings.ConnectionString);
}
Et là, miracle (ou pas ) tout fonctionne...
Conclusion
Alors, pour faire du multi threading avec Sql Server CE, pensez à une chose importante :
Chaque Thread doit avoir sa propre connexion.
A partir de là, tout roule.
Et si on reprend les caractéristiques de Sql Server CE, on peut créer 256 connexion simultanées, ce qui vous laisse de la marge !
Et voilà, je viens d'y passer une journée (encore ...) et non, Sql Server CE, n'est PAS multi thread safe...
Alors voilà soyons clair :
|
Feature |
SQL CE |
|
Size of total deployment |
1,834 KB |
|
Number of concurrent connections |
256 |
|
Concurrent process connections |
1 |
|
Database Size Limit |
4GB |
|
Max CPUs Supported |
1 |
Alors la conclusion est consternante, mais :
- Oui, vous pouvez ouvrir plein de connections simultannément
- Non, vous ne pouvez pas ouvrir 2 connections sur 2 threads séparés..
Maintenant, un exemple que je viens de faire.
Une petite application où je teste 3 cas. Dans chacun des cas, je fais 1 appels sur 2 services différents. Chaque service me renvoyant des données provenant d'une base de donnée Sql Server CE.
les 3 cas que je veux tester :
- Apel des 2 services en synchrone, histoire de vérifier que ça marche dans un cas classique mono thread
- Appel de mes 2 services par WCF, en monde Asynchrone (via les proxys générés et leurs appels asynchrones générés)
- Appel de mes 2 services directement, et utilisation de BackgroundWorkers
L'application est architecturée de manière classique.
- Une UI
- Un Service WCF
- Un projet Entité, partagé par le service et l'UI
Je fais un peu de WCF, mes services sont exposés et je génère les proxys des services via un "Add Service Reference" :
Note : Je demande à générer les appels asynchrones, sinon aucun intéret de tester le multi-threading :)
Petit Hack dans mon architecture : Pour le 3eme cas, je référence directement mon assembly service dans mon projet UI (Rhoooo c'est pas bien; mais c'est pour l'exemple :))
Pour information, j'ai utilisé Linq To Sql pour générer le dbml dans le projet Entités, mais pour être absolument sûr du résultat, j'utilise une bonne vielle connection ADO.NET et je génère les commandes à la main.
Cas 1 : WCF Synchrone
Je crée mes 2 services (mes proxys) :
// For Exemple 1 and 2 : use of WCF Services
public ClientProxy.ClientServicesClient ClientServices { get; set; }
public ParameterProxy.ParameterServicesClient ParameterServices{ get; set; }
Je les instancie au démarrage :
ClientServices = new SqlServerCE_WCFAsync.ClientProxy.ClientServicesClient();
ParameterServices = new SqlServerCE_WCFAsync.ParameterProxy.ParameterServicesClient();
J'appel mes 2 services :
this.dataGridView1.DataSource = ClientServices.GetClients2();
var p = ParameterServices.GetParameter("AddressMail");
if (p != null)
{
label1.Text = p.Value;
}
Bon, ben ça, ça fonctionne...
Continuons..
Cas 2 : WCF Asynchrone
Mes 2 services sont créés (voir Cas 1)
Je configure le service Client, pour qu'il utilise son proxy de manière asynchrone :
ClientServices.GetClients2Completed += ClientServices_GetClients2Completed;
je crée la méthode Completed :
void ClientServices_GetClients2Completed(object sender,
SqlServerCE_WCFAsync.ClientProxy.GetClients2CompletedEventArgs e)
{
this.dataGridView1.DataSource = e.Result;
}
Et je tente d'appeller ce service en asynchrone, alors que j'appel le service Parameter en synchrone lui :
ClientServices.GetClients2Async();
var p = ParameterServices.GetParameter2("AddressMail");
if (p != null)
{
label1.Text = p.Value;
}
Et là ça se vautre lamentablement avec une exception qui ferait frémir un mamouth congelé :
"
A first chance exception of type 'System.AccessViolationException' occurred in System.Data.SqlServerCe.dll
Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
"
Cas 3 : Utilisation de BackGroundWorker
Ici, je me passe de WCF (et donc je référence directement l'assembly service dans mon projet UI)
et je tente d'utiliser un BackGroundWorker sur le service ClientServices et je fais un appel direct sur le service ParameterServices
Configuration du BackGroundWorker :
bgw1 = new BackgroundWorker();
bgw1.DoWork += new DoWorkEventHandler(bgw1_DoWork);
bgw1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgw1_RunWorkerCompleted);
Configuration du travail à effectuer :
void bgw1_DoWork(object sender, DoWorkEventArgs e)
{
SqlServerCE_WCFServices.ClientServices service = new SqlServerCE_WCFServices.ClientServices();
e.Result = service.GetClients2();
}
Configuration du completed du BackGround Worker :
void bgw1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
Trace.WriteLine("Erreur in User Interface in GetClients. Exception : " + e.Error.Message);
if (e.Cancelled)
Trace.WriteLine("Action cancelled in User Interface in GetClients");
this.dataGridView1.DataSource = e.Result as List<Client>;
}
Appel :
bgw1.RunWorkerAsync();
SqlServerCE_WCFServices.ParameterServices service = new SqlServerCE_WCFServices.ParameterServices();
var p = service.GetParameter2("AddressMail");
if (p != null)
{
label1.Text = p.Value;
}
Et là, c'est le drame ...
"
A first chance exception of type 'System.AccessViolationException' occurred in System.Data.SqlServerCe.dll
Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
"
Conclusion
Alors voilà, conclusion:
- Oui, utilisez un BackGround Worker pour faire des appels asynchrone et rendre votre application moins freezée et plus agréable
- Non, n'utilisez pas d'appel asynchrone sur plusieurs threads...
Et là, je demande, comment faire avec WCF ?? Le monsieur génère un Thread par Service Proxy... c'est balôt !
Et je demande encore, comment faire une opération (de maintenance, ou de synchronisation) en arrière plan dans mon application desktop sous SQL Server CE ? Je demande à mon utilisateur d'être sympa et de pas faire de requêtes en attendant ??
Je laisse le code à disposition si le coeur vous tente :)