Roberto Brunetti

Developing in the cloud

.NET Programming

luglio 2007 - Posts

VSTS for DB Pro Service Release 1

Disponibile al download la Service Release 1 per Visual Studio Team System for Database Professionals.

Queste le caratteristiche aggiunte a questa versione'

• Cross-database references

Support is improved to enable you to reference objects in different databases by using database project references or referencing a database metafile (.dbmeta). This support will reduce or eliminate the cross database reference warnings within a database project.

• Improved file support within SQL Server file groups

You may define files within file groups as database project properties instead of having to create files and file groups within the pre-deployment storage script.

• Variables

A Variables page is added to the database properties. This new page enables you to define setvar variables for use in the deployment scripts. Additionally, SR1 supports the latest service pack release from Microsoft SQL Server 2005 (SP2). The SR1 also supports the Windows Vista operating system.

Il download da http://www.microsoft.com/downloads/details.aspx?FamilyID=9810808C-9248-41A5-BDC1-D8210A06ED87&displaylang=en

Posted: lug 28 2007, 05.03 by rob | with 3 comment(s)
Filed under: ,
eScrum for VSTS

E' stato rilasciato eScrum per la gestione di progetti Scrum (appunto) con TFS.

questo i link: http://www.microsoft.com/downloads/details.aspx?FamilyID=55A4BDE6-10A7-4C41-9938-F388C1ED15E9&displaylang=en.

Si installa su una macchina 2003 Server e necessita, a parte il FW .NET 2.0, anche delle libreria AJAX.

Buon scrumming.

Posted: lug 16 2007, 06.48 by rob | with no comments
Filed under:
Workflow Custom Activity e VSTS Unit Test

Una custom activity è una unità di esecuzione a se stante riutilizzabile in vari workflow: questa definizione da manuale ci dice che
1) Una activity non deve mai "ragionare" rispetto al Workflow in cui viene inserita
2) Una activity non deve mai "ragionare" rispetto ad altre activity
3) Una activity deve poter essere testata singolarmente per capire se a fronte di determinati parametri risponde come ci aspettiamo

Senza dilungarci sui primi due punti che chi sviluppa su Wofkflow Foundation conosce bene o sulle teorie dello unit testing volevo dare due dritte su come usare strumenti tipo Visual Studio Team System (edizione Dev o Tester) oppure NUnit per testare una singola activity senza dover per forza costruire un workflow che la contiene e un client che lancia il wofkflow. Del resto le tecniche di unit testing (e gli strumenti citati) partono dal presupposto di poter testare appunto una unità di esecuzione.

Quello che ci interessa testare di una activity è il suo comportamento all'interno di una istanza di workflow che la contiene. E' ovvio che si possano testare i singoli metodi (ad esempio un metodo che muove qualcosa in un DB) utilizzando uno unit test diretto sulla classe Activity: una activity infatti non è nient'altro che una classe .NET, quindi qualunque strumenti per effettuare test può essere congruo rispetto all'obiettivo.

Testando il singolo metodo, però, non abbiamo un test completo dell'activity al momento del suo utilizzo all'interno di un workflow: le activity custom sfrutta le Dependency Property per bindare dati che arrivano dal Workflow o da altri activity e utilizzarli all'interno del metodo Execute dell'activity. Uno unit classico potrebbe creare un'istanza dell'activity e passare dei parametri ad un metodo interno che effettua le operazioni, ma difficilmente potrebbe eseguire il metodo Execute visto che tale metodo si aspetta in ingresso un ActivityExecutionContext: è difficile fornire un ActivityExecutionContext fuori da un workflow a meno di non mettersi all'anima di ricreare l'ambiente di esecuzione interno di un workflow nell'ambiente di unit testing.

Prendiamo ad esempio questa semplicissima activity:

using System;
using System.ComponentModel;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;

namespace DevLeap.CustomActivity
{
    public class WebTearActivity : System.Workflow.ComponentModel.Activity
    {

        public static DependencyProperty UrlProperty = DependencyProperty.Register("Url",
                        typeof(System.String),
                        typeof(WebTearActivity));

        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [BrowsableAttribute(true)]
        [DescriptionAttribute("Url to download")]
        [CategoryAttribute("WebTearActivity Property")]
        public string Url
        {
            get
            {
                return ((string)(base.GetValue(WebTearActivity.UrlProperty)));
            }
            set
            {
                base.SetValue(WebTearActivity.UrlProperty, value);
            }
        }

        public static DependencyProperty PageDownloadedProperty = DependencyProperty.Register("PageDownloaded",
                typeof(System.String),
                typeof(WebTearActivity));

        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [BrowsableAttribute(true)]
        [DescriptionAttribute("Page Downloaded")]
        [CategoryAttribute("WebTearActivity Property")]
        public string PageDownloaded
        {
            get
            {
                return ((string)(base.GetValue(WebTearActivity.PageDownloadedProperty)));
            }
            set
            {
                base.SetValue(WebTearActivity.PageDownloadedProperty, value);
            }
        }


        protected override ActivityExecutionStatus Execute(ActivityExecutionContext context)
        {
            string pageData;
            System.Net.WebClient client = new System.Net.WebClient();

            try
            {
                // Download the web page data
                this.PageDownloaded = client.DownloadString(this.Url);
            }
            catch (Exception e)
            {
                this.PageDownloaded = e.Message;
            }
           
            // Notifiy the runtime that the activity has finished
            return ActivityExecutionStatus.Closed;
        }

    }
}

Questa semplicissima Activity espone due Dependency Property denominate Url e PageDownload e internamente (appunto nel citato metodo execute) non fa altro che scaricare la pagina indicata nella proprietà Url e assegnarla alla proprietà PageDownloaded.

Per testare questa activity potremmo semplicemente creare un metodo privato denominato ad esempio Scarica(string url) che restituisce sotto forma di stringa la pagina scaricata. A questo punto lo unit test sarebbe semplice da implementare (con VSTS basta fare tasto destro sul metodo per creare progetto e metodo di test): lo unit test fornirà un Url e controllerà se la risposta è quella che ci aspettiamo. In questo caso però stiamo testando un metodo di una activity fuori dal contesto del workflow runtime: stiamo semplicemente testando un metodo di una classe.

Pensate ad una activity più complessa che ad esempio "ragiona" sull'activity execution context (AEC) controllandone dei parametri o valori e agendo di conseguenza; oppure una activity che lavora in asincrono tramite code o servizi del workflow; oppure ancora una activiy compesabile o che implementa IPendingWork: in questi casi non basta semplicemente verificare su un particola metodo fa il suo mestiere, ma occorre controllare il flusso di esecuzione, lo sblocco della chiamata asincrona o il lavoro del Work Item legato al WorkBatch del workflow. Spesso occorre anche verificare la corretta persistenza/reload dell'activity in base al PersistOnClose o attività transazionali o compensabili.

Morale della favola: occorre testare l'activity dentro il workflow runtime.  Come dicevamo all'inizio un metodo è quello di creare un workflow che ospita l'activity, un clientino (anche console) che passa i parametri corretti e una persona che verifica l'esito dei test: in pratica stiamo facendo unit test manuale costruendoci tutto noi.

L'obiettivo di questo mini-articoletto è vedere come automatizzare questi test dentro uno Uni Test di VSTS.

Per sfruttare il wizard di creazione di unit test evitando di fare tutto a mano si può: andare sul metodo execute dell'activity e con tasto destro selezionare Create Unit Test. Questo wizard ci crea un nuovo progetto, con le reference alla DLL che contiene la nostra activity e le referenze agli assembly System.Workflow.* necessari all'esecuzione di un workflow.

Il metodo di test creato nella nuova classe che rapprenta lo unit test non è particolarmente interessante, ma almeno il wizard ci ha creato tutta l'infrastruttura (ci evita le reference da fare, ci predispone una classe per fare unit test: sono 5 minuti di lavoro manuale, ma perchè sprecarli ? :-))

Ora, visto che un worklow è comunque una classe che deriva da Acrivity (ad esempio un Sequential Workflow è una classe SequentialWorkflowActivity che deriva da SequenceActivity, a sua volta derivata da CompositeActivity che deriva da Acivity) possiamo avviare tramite il Workflow runtime la nostra Activity (WebTearActivity nel nostro esempietto), senza dover creare un workflow che la contenga.

La prima cosa da fare per testare l'activity con il runtime del Workflow è creare una istanza del WorkflowRuntime: questa operazione si può fare la metodo TestInitialize in modo da centralizzarlo per tutti gli unit test di activiy presenti nella classe: in questo caso per ogni unit test nella classe di test verrà fatto partire il runtime. Possiamo anche sfruttare il metodo ClassInitialize che Team System invoca una sola volta (e quindi non per ogni test da lanciare) al momento in cui deve eseguire gli unit test presenti nella classe.

Ecco un esempio:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Text;
using System.Collections.Generic;
using Microsoft.Samples.Workflow.Tutorials.CustomActivity;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime;
using System.Threading;
namespace CustomActivity.Test
{
    /// <summary>
    ///This is a test class for Microsoft.Samples.Workflow.Tutorials.CustomActivity.WebTearActivity and is intended
    ///to contain all Microsoft.Samples.Workflow.Tutorials.CustomActivity.WebTearActivity Unit Tests
    ///</summary>
    [TestClass()]
    public class WebTearActivityTest
    {
        private WorkflowRuntime wkRuntime;
        private WorkflowInstance wkInstance;
        private AutoResetEvent waitHandle = new AutoResetEvent(false);
        private WorkflowCompletedEventArgs completedArgs;

        private TestContext testContextInstance;

        /// <summary>
        ///Gets or sets the test context which provides
        ///information about and functionality for the current test run.
        ///</summary>
        public TestContext TestContext
        {
            get
            {
                return testContextInstance;
            }
            set
            {
                testContextInstance = value;
            }
        }
        #region Additional test attributes

        //Use TestInitialize to run code before running each test
        //
        [TestInitialize()]
        public void MyTestInitialize()
        {
            wkRuntime = new WorkflowRuntime();
            wkRuntime.WorkflowCompleted += new EventHandler<WorkflowCompletedEventArgs>(wkRuntime_WorkflowCompleted);
            wkRuntime.WorkflowTerminated += new EventHandler<WorkflowTerminatedEventArgs>(wkRuntime_WorkflowTerminated);
            wkRuntime.StartRuntime();
        }

        //
        //Use TestCleanup to run code after each test has run
        //
        [TestCleanup()]
        public void MyTestCleanup()
        {
            if (wkRuntime != null)
            {
                wkRuntime.StopRuntime();
                wkRuntime.Dispose();
            }
        }

Il codice è praticamente quello utilizzato da una console application che avvia il runtime (ovviamente aggiunti servizi di persistenza, tracking, ExternalDataExchange in base alle necessità di test o constraint dell'activity). Si tengono private nella classe le variabili che rappresentano runtime, istanza e EventArgs di completamento.

Veniamo al test dell'activity: quello che vogliamo testare è
1) Passaggio di un Url sulla dependency property
2) Esecuzione dell'activity
3) Controllo della pagina scaricata

Il primo punto si esegue con il passaggio del famoso oggetto Dictionary che contiene i parametri da passare normalmente ad un workflow: ma visto che un workflow è in ultima battuta una Acivity, possiamo tranquillamente creare dal runtime una istanza della nostra activity passandole i parametri che corrispondono alle sue proprietà o dependency property pubbliche.

Ecco la prima parte del codice di test:

        [DeploymentItem("WebTearWorkflow.exe")]
        [TestMethod()]
        public void WebTearActivityCorrectTest()
        {

            Dictionary<string, object> properties = new Dictionary<string, object>();
            properties.Add("Url", "http://localhost:9000/SalamiManagementWebContent/Default.aspx");

            wkInstance = wkRuntime.CreateWorkflow(typeof(WebTearActivity), properties);

            Assert.IsNotNull(wkInstance, "Non posso creare l'istanza");

            wkInstance.Start();

In pratica creiamo dal runtime (istanziato nel metodo comune a tutti i test) una istanza della nostra activity passando il dictionaty di parametri come faremo con un normale workflow per poi avviare l'istanza. In caso di null sull'istanza facciamo una Assert (per raccoglilere i dati dello unit test associando al test run).

A questo punto l'activity al suo interno (metodo Execute mostrato all'inizio di questo post) proverà a scaricare l'url designato come test. Il risultato lo troveremo nella Dependency Property denominata PageDownloaded: questo risultato sarà però disponibile solo al completamente della nostra activity; quindi dobbiamo attendere il completamente del workflow per poter controllare tale valore.

Per attendere il completamento del workflow possiamo sfruttare l'evento WorkflowCompleted, che infatti è stato agganciato nel codice di partenza del test. Nell'evento andemo a salvare nella variabile privata completedArgs il risultato del workflow per fare in modo da poterlo rileggere durante il test.
In pratica questo il codice degli eventi completed e terminated (che segnala un errore):

        void wkRuntime_WorkflowTerminated(object sender, WorkflowTerminatedEventArgs e)
        {
            Assert.Fail("WebTearActivity Terminated {0}", e.Exception.Message);
            waitHandle.Set();
        }

        void wkRuntime_WorkflowCompleted(object sender, WorkflowCompletedEventArgs e)
        {
            completedArgs = e;
            waitHandle.Set();
        }

Come normalmente facciamo da una applicazione console, nel caso di terminate o complete sblocchiamo il WaitHandle: nel caso di terminate facciamo anche una Assert per indicare l'errore ricevuto. Nel caso di completed, appunto, salviamo nella variabile privata i WorkflowCompletedEventArgs.

Torniamo sul codice del test: dopo aver avviato l'istanza (in asincrono con il metodo Start) attendiamo che venga sbloccato il WaitHandle per poi controllare se esistono e sono validi i parametri di output ovvero la dependency property che contiene la pagina scaricata.

Ecco il codice completo del test:

        [DeploymentItem("WebTearWorkflow.exe")]
        [TestMethod()]
        public void WebTearActivityCorrectTest()
        {

            Dictionary<string, object> properties = new Dictionary<string, object>();
            properties.Add("Url", "http://localhost:9000/SalamiManagementWebContent/Default.aspx");

            wkInstance = wkRuntime.CreateWorkflow(typeof(WebTearActivity), properties);

            Assert.IsNotNull(wkInstance, "Non posso creare l'istanza");

            wkInstance.Start();

            waitHandle.WaitOne();    // Vorrei che entro 5 sec finisse
 
            // Controllo se completed in base a WKCompletedArgs
            Assert.IsNotNull(completedArgs, "Non è stato completato");
            String pageDownloaded = (String)completedArgs.OutputParameters["PageDownloaded"];

            Assert.IsTrue(pageDownloaded.Contains("SalameDescrizione"), "Test non valido " + pageDownloaded);
        }

Come si nota, dopo la WaitOne controllo che il WF sia stato completato (se è null il completedArgs vuol dire che non sono passato da WorfklowCompleted) e poi volendo controllo che nella proprietà PageDownloaded sia presente quanto mi aspetto.

Nel caso in cui voglia anche verificare le tempistiche di esecuzione, ad esempio verificare che il test non impieghi più di X secondi posso

1) Agire sullo Unit Test (dalle sue proprietà nel designer) impostando un timeout
2) Per scenari più complessi poteri assegnare un tempo alla WaitOne

Spero utile

Altri  riferimenti sul nostro sito:
Intro http://blogs.devleap.com/rob/archive/2007/03/31/windows-workflow-foundation-runtime.aspx
Rientranza delle chiamate: http://blogs.devleap.com/paolo/archive/2007/02/06/rientranza-delle-chiamate-in-wf.aspx
Problematiche esecuzione server side http://blogs.devleap.com/rob/archive/2007/02/07/windows-workflow-foundation-thread-pool.aspx
Problematiche asincrone in ASP.NET http://blogs.devleap.com/articolidevleap/archive/2007/02/07/asp-net-2-0-async-techniques.aspx

 

Alla prossima

 

Quando si dice "far paginare il DB e non ASP.NET"

Ho preso in mano un'applicazione di un cliente che, purtroppo, faceva uso di paginazione direttamente nel DataGrid (anzi GridView adesso che ha la 2.0) di ASP.NET.

Da sempre consigliamo di non fare paginazione automatica, in quanto in memoria non si riescono a raggiungere le performance di un DB che sfrutta indici e query processor; inoltre, passano più dati dal server DB al server web e non ultimo il carico di memoria sull'applicazione ASP.NET è mostruoso rispetto ai record che l'utente vedrà nelle pagine. Questo ovviamente vale anche per servizi che espongono i dati.

Per una idea delle tecniche utilizzabili su SQL 2000 date un occhio al nostro articolo del http://www.devleap.com/Articolo3723.devleap del 2003. Adesso con SQL 2005 si possono valutare le CTE come ha avuto modo di scrivere Paolo per un articolo uscito su Infomedia e che fra poco possiamo ripubblicare sul sito.

Fin quì niente di nuovo :-) Quello che volevo sottolineare sono i dati di stress test che ho effettuato per questo cliente su SOLI 700 record presentati su una pagina che ne visualizza 10. Il flusso completo prevede uno strato DAL che riempie delle entità di business che vengono poi passato allo strato UI. L'applicazione esistente estraeva 714 record (sono c.a. 8 campi di media lunghezza) tramite Stored Procedure. Lo strato business fa solo da passacarte e lo strato UI visualizza 10 di questi record. Il test che ho fatto non prevede sort (che ovviamente peggiorerebbe ulteriormente il confronto rispetto alla soluzione di far paginare il DB) quindi è il più semplice "tipo di accesso" che l'applicazione può fare.

Vediamo ai numeri:

714 Record - 8 campi di lunghezza variabile (non sono presenti Image...cosa che peggiorebbe ulteriormente il riempimento in memoria su ASP.NET)
25 Utenti in contemporanea che effettuano richieste senza think times (in pratica al termine di una richiesta si procede subito con la successiva)
Durata del test 2 minuti "secchi" per capire le differenze: il cliente è già convinto a paginare dal DB quindi non credo che avrò modo di fare altri test).
Test fatto con VSTS da una seconda macchina per non inficiare i dati globali
Richiesta per la prima pagina di 70, quindi sempre i primi record (cercando gli ultimi i tempi peggiorano con paginazione automatica)

Soluzione precedente (con paginazione da griglia)
1.600 richieste totali
13 richieste al secondo come media (con punte di 27)
100% del processore spesso...e meno male...lo stiamo facendo lavorare
5.8 Average Response Time: con punte di 11,3 massimo
9.398 Garbage Collection a generazione 0
2.626 Garbage Collection a generazione 1
233 MB Memoria media del processo ASP.NET

Soluzione con paginazione fatta dal DB
17.289 richieste totali
143 richieste al secondo come media (con punte di 148)
100% del processore spesso...e meno male...lo stiamo facendo lavorare
0,34 Average Response Time: con punte di 0,45 massimo
9.419 Garbage Collection a generazione 0
1.858 Garbage Collection a generazione 1
206 MB Memoria media del processo ASP.NET

I numeri non hanno bisogno di spiegazioni :-) Siamo oltre 10 volte come numero di richieste nello stesso arco di tempo. Il numero di interventi del GC è simile, ma nel primo caso per servire 1.600 richieste, nel secondo per servirne 17.289, Giusto per curiosità ho provato a limitare il test a 1.600 richieste per il secondo caso....e il GC è intervenuto solo 702 volte.

Quando si fanno le cose però si vanno bene fino in fondo :-) quindi ho provato, per fortuna grazie al generatore di codice GAT implementato, altrimenti ero ancora a scrivere codice, a rendere le pagine e l'accesso ai dati asincrono (per la cronaca sotto c'è SQL 2005).

Soluzione con paginazione fatta dal DB e pagine/ADO asincroni
21.249 richieste totali
177 richieste al secondo come media (con punte di 189)
100% del processore spesso...e meno male...lo stiamo facendo lavorare
0,14 Average Response Time: con punte di 0,15 massimo
9.218 Garbage Collection a generazione 0
1.757 Garbage Collection a generazione 1
206 MB Memoria media del processo ASP.NET

La sfiga, ha voluto che nell'ultimo test ci sia stato un recycle Application Pool di IIS...altrimenti i numeri sarebbero ancora migliori !!!

Questi numeri per dare un senso alle nostre parole di sempre...ricordatevi che stiamo parlando di 714 record (un niente praticamente) e già la differenza è più che abissale; non ci sort che peggiorerebbero i tempi e la memoria da allocare (e quindi ripulire) e tantomeno filtri che rallenterebbero notevolmente quando effettuati in memoria. Per quanto riguarda la parte asincrona, è bene ricordare che un allungamento dei tempi di SQL Server ridurrebbe le prestazioni in misura minore rispetto a pagine sincrone.

Inoltre è doveroso dire che in tutti e tre i test non abbiamo stressato la macchina IIS/ASP.NET in quanto tutte le pagine sono state servite al client: non ci sono in pratica riechieste accodate oltre il limite di default di ASP.NET: se stressassimo ulteriormente il carico le differenze tenderebbero ad aumentare notevolmente...e pensate che il test è solamente con 25 richieste in contemporanea e in sequenza !