tag:blogger.com,1999:blog-64328438683912685342024-03-19T11:02:57.951+01:00Zen Programming.itPrima medita, poi agisci...Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.comBlogger38125tag:blogger.com,1999:blog-6432843868391268534.post-69501868754519078832020-04-08T21:48:00.000+02:002020-04-08T21:48:11.950+02:00Restore di un database SQL Server in un container Docker<div class="code-line code-line" data-line="2" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Qualche giorno fa un collega aveva la necessità di eseguire il restore di un database creato con <span style="box-sizing: border-box; font-weight: 600;">Microsoft SQL Server 2016</span> in locale, in modo da poter eseguire la creazione di una dashboard in PowerBI.</div>
<div class="code-line code-line" data-line="6" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
La nostra scelta di team - ormai da anni - è stata quella di installare sui nostri computer meno strumenti possibili, e solo quelli essenziali per portare avanti il lavoro quotidiano. Meno strumenti su un computer = meno spazio disco consumato = più risorse disponibili; quindi la scelta è sempre quella di appoggiarci a risorse ospitate <span style="box-sizing: border-box; font-weight: 600;">in cloud</span>.</div>
<div class="code-line code-line" data-line="11" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
In questo caso lo scenario era più complesso, e il semplice restore del nostro backup di database nel cloud, non sembrava essere la soluzione più performante, specialmente per via dell'obiettivo che avevamo - ovvero un' <span style="box-sizing: border-box; font-weight: 600;">analisi dei dati</span> - in un ambiente di sviluppo.</div>
<div class="code-line code-line" data-line="15" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Quindi quello che ho cercato di fare è semplicemente prendere il backup in questione - che nel nostro esempio sarà il file <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">test.bak</code> - e fare il suo restore in un container <span style="box-sizing: border-box; font-weight: 600;">Docker</span> equipaggiato con Microsoft SQL Server 2019, che gira un kernel <span style="box-sizing: border-box; font-weight: 600;">Ubuntu versione 16.04</span>.</div>
<div class="code-line code-line" data-line="20" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Il container da cui partiremo è disponibile su <span style="box-sizing: border-box; font-weight: 600;">Docker Hub</span>, nello specifico alla pagina <a data-href="https://hub.docker.com/_/microsoft-mssql-server" href="https://hub.docker.com/_/microsoft-mssql-server" style="box-sizing: border-box; color: #0366d6; text-decoration-line: none;" title="https://hub.docker.com/_/microsoft-mssql-server">https://hub.docker.com/_/microsoft-mssql-server</a>. Nonostante ci siano sempre versioni aggiornate, la mia scelta è stata quella di usare la <em style="box-sizing: border-box;">"2019-GA"</em>.</div>
<div class="code-line code-line" data-line="24" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Usando Powershell (o la commanline che preferite) eseguiamo prima di tutto lo scaricamento del container in questione, impostando la password amministrativa che sarà poi utilizzata anche per le operazioni di ripristino del backup dei dati:</div>
<pre style="background-attachment: initial !important; background-clip: initial !important; background-color: #f6f8fa; background-image: initial !important; background-origin: initial !important; background-position: initial !important; background-repeat: initial !important; background-size: initial !important; border-radius: 3px; box-sizing: border-box; color: initial; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.45; margin-bottom: 16px; overflow-wrap: normal; overflow: auto; padding: 16px; white-space: pre-wrap;"><code class="code-line language-sh code-line language-sh" data-line="28" style="background: transparent; border-radius: 3px; border: 0px; box-sizing: border-box; color: initial; display: inline; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: inherit; margin: 0px; overflow-wrap: normal; overflow: visible; padding: 0px; position: relative; tab-size: 4; white-space: pre; word-break: normal;"><div style="background: none; box-sizing: border-box;">
docker run -e <span class="hljs-string" style="box-sizing: border-box; color: #dd1144;">"ACCEPT_EULA=Y"</span> -e <span class="hljs-string" style="box-sizing: border-box; color: #dd1144;">"MSSQL_SA_PASSWORD=YourP@ssw0rd"</span> `
--name <span class="hljs-string" style="box-sizing: border-box; color: #dd1144;">"mssql1"</span> -p 1401:1433 `
-d mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04
</div>
</code></pre>
<div class="code-line code-line" data-line="34" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Attenzione! E' molto importante che non sia specificato alcun volume esterno nel momento in cui si avvia il container. In caso contrario, il vostro container finale non sarà "auto-consistente".</div>
<div class="code-line code-line" data-line="38" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Se non avete ancora in locale l'immagine di SQL Server, dovreste avere un output di questo tipo:</div>
<pre style="background-attachment: initial !important; background-clip: initial !important; background-color: #f6f8fa; background-image: initial !important; background-origin: initial !important; background-position: initial !important; background-repeat: initial !important; background-size: initial !important; border-radius: 3px; box-sizing: border-box; color: initial; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.45; margin-bottom: 16px; overflow-wrap: normal; overflow: auto; padding: 16px; white-space: pre-wrap;"><code class="code-line language-sh code-line language-sh" data-line="41" style="background: transparent; border-radius: 3px; border: 0px; box-sizing: border-box; color: initial; display: inline; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: inherit; margin: 0px; overflow-wrap: normal; overflow: visible; padding: 0px; position: relative; tab-size: 4; white-space: pre; word-break: normal;"><div style="background: none; box-sizing: border-box;">
Unable to find image <span class="hljs-string" style="box-sizing: border-box; color: #dd1144;">'mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04'</span> locally
2019-GA-ubuntu-16.04: Pulling from mssql/server
59ab41dd721a: Pull complete
57da90bec92c: Pull complete
06fe57530625: Pull complete
5a6315cba1ff: Pull complete
739f58768b3f: Pull complete
fd449e8d7345: Pull complete
51d0933375e5: Pull complete
64f21ba81504: Pull complete
55b6919c0cc6: Pull complete
Digest: sha256:c8fa22553ce421b0482febcafa712b29cbb933b0d97a8671686797b31cf157a9
Status: Downloaded newer image <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">for</span> mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04
89e348fd2d5eafdce27d55bea8eacc0d01a21923b6f4b0a6bfc9a52323434efd
</div>
</code></pre>
<div class="code-line code-line" data-line="58" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
A questo punto, verefichiamo che il container sia <span style="box-sizing: border-box; font-weight: 600;">"up & running"</span> (abbreviato)...</div>
<pre style="background-attachment: initial !important; background-clip: initial !important; background-color: #f6f8fa; background-image: initial !important; background-origin: initial !important; background-position: initial !important; background-repeat: initial !important; background-size: initial !important; border-radius: 3px; box-sizing: border-box; color: initial; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.45; margin-bottom: 16px; overflow-wrap: normal; overflow: auto; padding: 16px; white-space: pre-wrap;"><code class="code-line language-sh code-line language-sh" data-line="60" style="background: transparent; border-radius: 3px; border: 0px; box-sizing: border-box; color: initial; display: inline; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: inherit; margin: 0px; overflow-wrap: normal; overflow: visible; padding: 0px; position: relative; tab-size: 4; white-space: pre; word-break: normal;"><div style="background: none; box-sizing: border-box;">
CONTAINER ID IMAGE ... CREATED STATUS
89e348fd2d5e mcr.microsoft.c... 6 minutes ago Up 6 minutes
</div>
</code></pre>
<div class="code-line code-line" data-line="65" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Usando SQL Server Management Studio (liberamente scaricabile da questo link <a data-href="https://aka.ms/ssmsfullsetup" href="https://aka.ms/ssmsfullsetup" style="box-sizing: border-box; color: #0366d6; text-decoration-line: none;" title="https://aka.ms/ssmsfullsetup">https://aka.ms/ssmsfullsetup</a> ), bisogna verificare se l'istanza di SQL Server che gira nel container Docker è effettivamente raggiungibile dal nostro host. Ricordiamoci che in fase di avvio, abbiamo rimappato la porta standard di SQL Server <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">1433</code> (esposta all'interno del container) con la porta della macchina locale <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">1401</code>. Quindi i dati per accedere sono i seguenti:</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjR9MzyZtZOaP_ZtK2M1onzFiA9YVJd3VnV-M3-W5hI9TGzDJq5rnu_qsZX7IPQPCrLeHpAxpiOm34vZinfqB6jr61NtXkd2m2-2aA9bck8zGBbcAfec2Yy5KybpnTqpKUPxbEq4eNNA74/s1600/01.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="336" data-original-width="500" height="215" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjR9MzyZtZOaP_ZtK2M1onzFiA9YVJd3VnV-M3-W5hI9TGzDJq5rnu_qsZX7IPQPCrLeHpAxpiOm34vZinfqB6jr61NtXkd2m2-2aA9bck8zGBbcAfec2Yy5KybpnTqpKUPxbEq4eNNA74/s320/01.png" width="320" /></a></div>
<div class="code-line code-line" data-line="72" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
<br /></div>
<div class="code-line code-line" data-line="74" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
A questo punto è necessario passare nuovamente a Powershell per eseguire il ripristino del file backup, che nel mio caso ho posizionato sotto in <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">E:\mssql-backup\test.bak</code>.</div>
<div class="code-line code-line" data-line="77" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Il seguente comando avvia la creazione della cartella di destinazione (<code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">/var/opt/mssql/backup</code>) dove sarà copiato il file di backup, all'interno del container Docker in esecuzione (denominato <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">mssql1</code>):</div>
<pre style="background-attachment: initial !important; background-clip: initial !important; background-color: #f6f8fa; background-image: initial !important; background-origin: initial !important; background-position: initial !important; background-repeat: initial !important; background-size: initial !important; border-radius: 3px; box-sizing: border-box; color: initial; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.45; margin-bottom: 16px; overflow-wrap: normal; overflow: auto; padding: 16px; white-space: pre-wrap;"><code class="code-line language-sh code-line language-sh" data-line="81" style="background: transparent; border-radius: 3px; border: 0px; box-sizing: border-box; color: initial; display: inline; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: inherit; margin: 0px; overflow-wrap: normal; overflow: visible; padding: 0px; position: relative; tab-size: 4; white-space: pre; word-break: normal;"><div style="background: none; box-sizing: border-box;">
docker <span class="hljs-built_in" style="box-sizing: border-box; color: #0086b3;">exec</span> -it mssql1 mkdir /var/opt/mssql/backup
</div>
</code></pre>
<div class="code-line code-line" data-line="85" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
A seguire, avviamo la copia del file sorgente (<code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">E:\mssql-backup\test.bak</code>) nel container <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">mssql1</code>, sotto la cartella appena creata (<code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">/var/opt/mssql/backup</code>):</div>
<pre style="background-attachment: initial !important; background-clip: initial !important; background-color: #f6f8fa; background-image: initial !important; background-origin: initial !important; background-position: initial !important; background-repeat: initial !important; background-size: initial !important; border-radius: 3px; box-sizing: border-box; color: initial; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.45; margin-bottom: 16px; overflow-wrap: normal; overflow: auto; padding: 16px; white-space: pre-wrap;"><code class="code-line language-sh code-line language-sh" data-line="88" style="background: transparent; border-radius: 3px; border: 0px; box-sizing: border-box; color: initial; display: inline; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: inherit; margin: 0px; overflow-wrap: normal; overflow: visible; padding: 0px; position: relative; tab-size: 4; white-space: pre; word-break: normal;"><div style="background: none; box-sizing: border-box;">
docker cp E:\mssql-backup\test.bak mssql1:/var/opt/mssql/backup
</div>
</code></pre>
<div class="code-line code-line" data-line="92" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
NOTA: la copia del file di backup nel container può richiedere più o meno tempo a seconda della dimensione del file. Durante la copia non sarà visualizzato nessun feedback, quindi armatevi di pazienza e attendete ;).</div>
<div class="code-line code-line" data-line="96" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Una volta terminata la copia, dobbiamo semplicemente ripristinare il database dal file appena copiato nel server in esecuzione in Docker. Questa operazione si può fare alla "vecchia maniera", direttamente da SQL Management Studio:</div>
<ul style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; margin-top: 0px; padding-left: 2em;">
<li class="code-line code-line" data-line="100" style="box-sizing: border-box; position: relative;">tasto destro sulla cartella "Database"</li>
<li class="code-line code-line" data-line="101" style="box-sizing: border-box; margin-top: 0.25em; position: relative;">selezione "Restore database"</li>
<li class="code-line code-line" data-line="102" style="box-sizing: border-box; margin-top: 0.25em; position: relative;">selezione dell'opzione "Device"</li>
<li class="code-line code-line" data-line="103" style="box-sizing: border-box; margin-top: 0.25em; position: relative;">scelta delle opzioni aggiuntive (pulsante "...")</li>
<li class="code-line code-line" data-line="104" style="box-sizing: border-box; margin-top: 0.25em; position: relative;">selezione del file sorgente dalla cartella che lo contiene (nel nostro caso <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">/var/opt/mssql/backup</code>) e pressione del pulsante "refresh"</li>
<li class="code-line code-line" data-line="106" style="box-sizing: border-box; margin-top: 0.25em; position: relative;">selezione del file <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">test.bak</code> e conferma</li>
<li class="code-line code-line" data-line="107" style="box-sizing: border-box; margin-top: 0.25em; position: relative;">pressione del pulsante "Ok" per confermare il ripristino</li>
</ul>
<div class="separator" style="clear: both; text-align: center;">
</div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3t08ChdmcBQUT_e3wQL_8c_2kq3XPjBBqr-44m-yd47mqTGizPdVF_thKc1UzXvPTqidzhfkKkTYtgDfn6bUxqP52OmFJN4vbolO7CVjo99xqFvGl6f8tt_flIVRa3SA4dZCzMeRKlbQ/s1600/05.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="760" data-original-width="876" height="277" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3t08ChdmcBQUT_e3wQL_8c_2kq3XPjBBqr-44m-yd47mqTGizPdVF_thKc1UzXvPTqidzhfkKkTYtgDfn6bUxqP52OmFJN4vbolO7CVjo99xqFvGl6f8tt_flIVRa3SA4dZCzMeRKlbQ/s320/05.png" width="320" /></a></div>
<div class="code-line code-line" data-line="109" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
<br /></div>
<div class="code-line code-line" data-line="111" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Oppure, possiamo fare la stessa cosa usando la linea di comando:</div>
<pre style="background-attachment: initial !important; background-clip: initial !important; background-color: #f6f8fa; background-image: initial !important; background-origin: initial !important; background-position: initial !important; background-repeat: initial !important; background-size: initial !important; border-radius: 3px; box-sizing: border-box; color: initial; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.45; margin-bottom: 16px; overflow-wrap: normal; overflow: auto; padding: 16px; white-space: pre-wrap;"><code class="code-line language-sh code-line language-sh" data-line="113" style="background: transparent; border-radius: 3px; border: 0px; box-sizing: border-box; color: initial; display: inline; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: inherit; margin: 0px; overflow-wrap: normal; overflow: visible; padding: 0px; position: relative; tab-size: 4; white-space: pre; word-break: normal;"><div style="background: none; box-sizing: border-box;">
docker <span class="hljs-built_in" style="box-sizing: border-box; color: #0086b3;">exec</span> -it mssql1 /opt/mssql-tools/bin/sqlcmd `
-S localhost -U SA -P <span class="hljs-string" style="box-sizing: border-box; color: #dd1144;">"YourP@ssw0rd"</span> `
-Q <span class="hljs-string" style="box-sizing: border-box; color: #dd1144;">"RESTORE DATABASE NuovoTest FROM DISK = '/var/opt/mssql/backup/test.bak' `
WITH MOVE 'test' TO '/var/opt/mssql/data/NuovoTest.mdf', `
MOVE 'test_Log' TO '/var/opt/mssql/data/NuovoTest.ldf'"</span>
</div>
</code></pre>
<div class="code-line code-line" data-line="121" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Fate attenzione alle denominazione dei file e alias he userete per i ripristino; se il nome del database target finale (<code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">NuovoTest</code>) e per i nomi dei corrispondenti files su disco (nel nostro caso <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">.../NuovoTest.mdf</code> e <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">.../NuovoTest.ldf</code>), è tutto puramente a vostra discrezione, per i nomi logici dei file sorgenti, non è così, e dovete rispettare quello che è contenuto nel backup: <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">test</code> e <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">test_Log</code>.</div>
<div class="code-line code-line" data-line="127" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Ma se è andato tutto bene, dovreste avere il database perfettamente funzionante ed attivo. Lo potete facilmente verificare anche questa volta da SQL Management:</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7Ti-l8lk_WDPmk-XBKyDhUgOfpo-nciSmi4wuqzLIfjHIMfAXGEaA4HQq_KEbMsiYa6Mqe9Eb6yiKXaleQYQbRlNWgkOZuJiXKug_KuMnX5NwRJZCaB9Ka3YZzE6YcCGhdsg7pqYMTWc/s1600/08.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="545" data-original-width="429" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7Ti-l8lk_WDPmk-XBKyDhUgOfpo-nciSmi4wuqzLIfjHIMfAXGEaA4HQq_KEbMsiYa6Mqe9Eb6yiKXaleQYQbRlNWgkOZuJiXKug_KuMnX5NwRJZCaB9Ka3YZzE6YcCGhdsg7pqYMTWc/s320/08.png" width="251" /></a></div>
<div class="code-line code-line" data-line="130" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
<br /></div>
<h2 class="code-line code-line" data-line="132" id="bonus-feature-esportazione-di-una-immagine-del-container" style="border-bottom: 1px solid rgb(234, 236, 239); box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; line-height: 1.25; margin-bottom: 16px; margin-top: 24px; padding-bottom: 0.3em; position: relative;">
Bonus feature, esportazione di una immagine del container</h2>
<div class="code-line code-line" data-line="134" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Una volta che avete ripristinato in locale il vostro ambiente, se volete condividerlo con un collega - oppure semplicemente averne un backup locale, sempre pronto in caso di danno accidentale - è possibile creare <span style="box-sizing: border-box; font-weight: 600;">una immagine</span> Docker partendo dal container in esecuzione, contenente database e relativi dati.</div>
<div class="code-line code-line" data-line="139" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Prima di tutto eseguiamo una operazione di <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">commit</code>, in modo che venga fatta una foto del container al momento dell'esecuzione del comando.</div>
<pre style="background-attachment: initial !important; background-clip: initial !important; background-color: #f6f8fa; background-image: initial !important; background-origin: initial !important; background-position: initial !important; background-repeat: initial !important; background-size: initial !important; border-radius: 3px; box-sizing: border-box; color: initial; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.45; margin-bottom: 16px; overflow-wrap: normal; overflow: auto; padding: 16px; white-space: pre-wrap;"><code class="code-line language-sh code-line language-sh" data-line="142" style="background: transparent; border-radius: 3px; border: 0px; box-sizing: border-box; color: initial; display: inline; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: inherit; margin: 0px; overflow-wrap: normal; overflow: visible; padding: 0px; position: relative; tab-size: 4; white-space: pre; word-break: normal;"><div style="background: none; box-sizing: border-box;">
docker commit -p mssql1 my-mssql-with-db
</div>
</code></pre>
<div class="code-line code-line" data-line="146" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Il comando prevede l'indicazione del container da cui partire per creare l'immagine, che nel nostro caso è <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">mssql1</code>, e il nome dell'immagine finale <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">my-mssql-with-db</code>.</div>
<div class="code-line code-line" data-line="149" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Per verificare che l'immagine sia stata effettivamente geneata, utilizziamo il solito comando di inspection delle immagini docker:</div>
<pre style="background-attachment: initial !important; background-clip: initial !important; background-color: #f6f8fa; background-image: initial !important; background-origin: initial !important; background-position: initial !important; background-repeat: initial !important; background-size: initial !important; border-radius: 3px; box-sizing: border-box; color: initial; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.45; margin-bottom: 16px; overflow-wrap: normal; overflow: auto; padding: 16px; white-space: pre-wrap;"><code class="code-line language-sh code-line language-sh" data-line="152" style="background: transparent; border-radius: 3px; border: 0px; box-sizing: border-box; color: initial; display: inline; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: inherit; margin: 0px; overflow-wrap: normal; overflow: visible; padding: 0px; position: relative; tab-size: 4; white-space: pre; word-break: normal;"><div style="background: none; box-sizing: border-box;">
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
my-mssql-with-db latest 7b30daab90a7 4 seconds ago 1.57GB
mcr.microsoft.com/mssql/server 2019-GA-ubuntu-16.04 76c7c66bff02 6 months ago 1.57GB
</div>
</code></pre>
<div class="code-line code-line" data-line="160" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Se l'immagine <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">my-mssql-with-db</code> è presente, possiamo procedere con una esportazione del backup della stessa su disco locale, in un file Tarball <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">.tar</code>:</div>
<pre style="background-attachment: initial !important; background-clip: initial !important; background-color: #f6f8fa; background-image: initial !important; background-origin: initial !important; background-position: initial !important; background-repeat: initial !important; background-size: initial !important; border-radius: 3px; box-sizing: border-box; color: initial; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.45; margin-bottom: 16px; overflow-wrap: normal; overflow: auto; padding: 16px; white-space: pre-wrap;"><code class="code-line language-sh code-line language-sh" data-line="163" style="background: transparent; border-radius: 3px; border: 0px; box-sizing: border-box; color: initial; display: inline; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: inherit; margin: 0px; overflow-wrap: normal; overflow: visible; padding: 0px; position: relative; tab-size: 4; white-space: pre; word-break: normal;"><div style="background: none; box-sizing: border-box;">
docker save -o my-mssql-with-db.tar my-mssql-with-db
</div>
</code></pre>
<div class="code-line code-line" data-line="167" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Anche in questo caso, l'operazione di backup richiede molta pazienza; il file finale è infatti comprensivo sia dell'immagine originale di Microsoft SQL Server su container Ubuntu, sia delle nostre configurazioni, sia del database applicativo che abbiamo inserito nel container stesso.</div>
<h2 class="code-line code-line" data-line="172" id="ripristino-del-container-da-file-tarball" style="border-bottom: 1px solid rgb(234, 236, 239); box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; line-height: 1.25; margin-bottom: 16px; margin-top: 24px; padding-bottom: 0.3em; position: relative;">
Ripristino del container da file Tarball</h2>
<div class="code-line code-line" data-line="174" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Che significato ha fare un backup di una immagine, se non possiamo ripristinarla? Anche in questo caso è tutto molto semplice:</div>
<pre style="background-attachment: initial !important; background-clip: initial !important; background-color: #f6f8fa; background-image: initial !important; background-origin: initial !important; background-position: initial !important; background-repeat: initial !important; background-size: initial !important; border-radius: 3px; box-sizing: border-box; color: initial; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.45; margin-bottom: 16px; overflow-wrap: normal; overflow: auto; padding: 16px; white-space: pre-wrap;"><code class="code-line language-sh code-line language-sh" data-line="177" style="background: transparent; border-radius: 3px; border: 0px; box-sizing: border-box; color: initial; display: inline; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: inherit; margin: 0px; overflow-wrap: normal; overflow: visible; padding: 0px; position: relative; tab-size: 4; white-space: pre; word-break: normal;"><div style="background: none; box-sizing: border-box;">
docker load -i E:\mssql-backup\my-mssql-with-db.tar
</div>
</code></pre>
<div class="code-line code-line" data-line="181" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Al termine del ripristino, usando il solito <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">docker images</code> dovreste riuscire a vedere l'immagine <code style="background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; box-sizing: border-box; color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.357em; margin: 0px; padding: 0.2em 0.4em;">my-mssql-with-db</code> presente tra le immagini disponibili in locale per la creazione di nuovi container. Procediamo quindi all'avvio del container partendo dalla stessa immagine:</div>
<pre style="background-attachment: initial !important; background-clip: initial !important; background-color: #f6f8fa; background-image: initial !important; background-origin: initial !important; background-position: initial !important; background-repeat: initial !important; background-size: initial !important; border-radius: 3px; box-sizing: border-box; color: initial; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: 1.45; margin-bottom: 16px; overflow-wrap: normal; overflow: auto; padding: 16px; white-space: pre-wrap;"><code class="code-line language-sh code-line language-sh" data-line="186" style="background: transparent; border-radius: 3px; border: 0px; box-sizing: border-box; color: initial; display: inline; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; line-height: inherit; margin: 0px; overflow-wrap: normal; overflow: visible; padding: 0px; position: relative; tab-size: 4; white-space: pre; word-break: normal;"><div style="background: none; box-sizing: border-box;">
docker run --name <span class="hljs-string" style="box-sizing: border-box; color: #dd1144;">"mssql1"</span> -p 1401:1433 -d my-mssql-with-db
</div>
</code></pre>
<div class="code-line code-line" data-line="190" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Chiudo con un'ultima nota, molto importante. In fase di generazione del primo container che contiene Microsoft SQL Server ("pulito", senza nessuna modifica) è <span style="box-sizing: border-box; font-weight: 600;">importantissimo</span> non specificare nessun <span style="box-sizing: border-box; font-weight: 600;">volume</span> esterno al container. Se questa cosa dovesse accedere, in fase di "backup" del container e generazione della sua immagine, i dati non saranno inclusi nel file Tarball. Evitando invece di specificare il volume, tutto quanto - funzioni applicative + dati - saranno inclusi in un unico pacchetto, rendendolo quindi esportabile e ripristinabile anche su macchine diverse da quella dove è stato creato.</div>
<div class="code-line code-line" data-line="198" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
Buon divertimento con Docker!</div>
<div class="code-line code-line" data-line="200" style="box-sizing: border-box; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; margin-bottom: 16px; position: relative;">
M.</div>
Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com1tag:blogger.com,1999:blog-6432843868391268534.post-90354620652326045982019-09-08T22:08:00.000+02:002019-09-08T22:08:05.501+02:00Code Coverage su .NET Core con Coverlet e ReportGenerator<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Inserire nel processo di sviluppo software la pratica dei <strong style="box-sizing: inherit; line-height: inherit;">Test</strong> - soprattutto se si sta lavorando ad un progetto di una certa portata - è un'ottima abitudine che incrementa notevolmente il livello di <em style="box-sizing: inherit; line-height: inherit;">qualità</em> e <em style="box-sizing: inherit; line-height: inherit;">manutenibilità</em> della soluzione stessa, e ci mette al riparo da situazioni inaspettate che possono sempre accadere.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Purtroppo questa pratica è sempre stata vista come una "inutile perdita di tempo", che sottrae giornate di lavoro che si potrebbero altrimenti dedicare allo sviluppo. Si tratta tuttavia di un'idea ormai superata: è infatti provato che il tempo speso ad invididuare e a correggere errori introdotti in fase di sviluppo, è superiore al tempo che si sarebbe dedicato a realizzare i test che avrebbero garantito un livello di qualità più elevato del prodotto.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Superato lo scoglio della volontà di "fare test" (ed è uno scoglio bello grosso, credetemi!), ci si trova spesso a dover decidere quali test fare: Unit Tests, Behavioral Tests, System Tests, End-to-end Tests, ecc. Non è mia intenzione in questo post fare una panoramica di ciascuno di essi: ci sono moltissimi articoli in rete che rispondono a questi quesiti. Voglio invece porre un quesito differente: "Quante righe del mio codice sono effettivamente sottoposte a tests?".</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Per rispondere a questa domanda esistono una serie di strumenti che ormai da molti anni sono disponibili in versione "pay" o "free". Tutti sono riconosciuti sotto il nome di <strong style="box-sizing: inherit; line-height: inherit;">"Code Coverage Tools"</strong>.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Nel mondo Java, Python e NodeJs fare "code coverage" è una pratica ben diffusa da parecchio tempo. Ma <strong style="box-sizing: inherit; line-height: inherit;">.NET Core</strong> è una piattaforma piuttosto recente, nonostante nasca dalla lunga esperienza Microsoft di .NET. Qui gli strumenti, soprattutto quelli "open", non sono moltissimi e sono ancora poco conosciuti alla maggior parte degli sviluppatori che si approcciano al mondo dei tests.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
In questo post voglio descrivere la combinazione che quotidianamente utilizzo quando lavoro a progetti di una certa importanza. Per il mio tutorial mi avvarrò di <strong style="box-sizing: inherit; line-height: inherit;">Coverlet</strong>, <strong style="box-sizing: inherit; line-height: inherit;">Report Generator</strong> e output results in formato <strong style="box-sizing: inherit; line-height: inherit;">Cobertura</strong>.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Per prima cosa è necessario creare un progetto .NET Core da testare: niente di interessante, ma valido per il nostro scopo. Mi voglio sforzare di utilizzare gli strumenti da commandline per essere il più possibile "cross-platform", avvalendomi come editor di <a href="https://code.visualstudio.com/" style="box-sizing: inherit; color: #008cba; line-height: inherit; text-decoration-line: none;">Visual Studio Code</a>.</div>
<blockquote style="background-color: white; border-left: 1px solid rgb(221, 221, 221); box-sizing: inherit; color: #6f6f6f; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin: 0px 0px 1.25rem; padding: 0.5625rem 1.25rem 0px 1.1875rem;">
<div style="box-sizing: inherit; font-family: inherit; font-size: 1rem; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Premessa: come mi succede già da qualche tempo, ho scelto di realizzare questo tutorial su una macchina Linux, quindi tutti i percorsi delle cartelle e file di progetto utilizzano il "forward-slash" (<code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">/</code>) invece che il "back-slash" di Windows (<code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">\</code>); se deciderete di eseguire il tutto su Windows, ricordatevi quindi di invertire i separatori ;) …</div>
</blockquote>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Creiamo una cartella di lavoro (la mia l'ho chiamata con molta fantasia <em style="box-sizing: inherit; line-height: inherit;">SampleCodeCoverage</em>) e una soluzione vuota con il comando:</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="sh language-sh" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">dotnet new sln</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Nella stessa folder lanciamo i comandi per la creazione del progetto da testare ("SampleCodeCoverage") e il progetto di test realizzato con <a href="https://xunit.net/" style="box-sizing: inherit; color: #008cba; line-height: inherit; text-decoration-line: none;">xUnit</a>("SampleCodeCoverage.Tests"). Quindi aggiungiamo i due progetti alla soluzione e il progetto da testare al progetto di test, eseguiamo un restore dei pacchetti e un build della soluzione:</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="sh language-sh" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">dotnet new classlib -n SampleCodeCoverage
dotnet new xunit -n SampleCodeCoverage.Tests
dotnet sln add ./SampleCodeCoverage/SampleCodeCoverage.csproj
dotnet sln add ./SampleCodeCoverage.Tests/SampleCodeCoverage.Tests.csproj
dotnet add ./SampleCodeCoverage.Tests/SampleCodeCoverage.Tests.csproj reference ./SampleCodeCoverage/SampleCodeCoverage.csproj
dotnet restore
dotnet build</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
A questo punto aggiungiamo un po' di codice: una semplice classe di utilità con 3 metodo banali: andremo a coprire con Unit Test <em style="box-sizing: inherit; line-height: inherit;">solo 2 di questi</em>. Inseriamo la classe statica "SampleCodeCoverageUtils" nel progetto "SampleCodeCoverage":</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="csharp language-csharp" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">namespace SampleCodeCoverage
{
public static class SampleCodeCoverageUtils
{
public static int DoSum(int first, int second)
{
return first + second;
}
public static int DoSubtract(int first, int second)
{
return first - second;
}
public static int DoMultiply(int first, int second)
{
return first * second;
}
}
}</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Nel progetto "SampleCodeCoverage.Tests" aggiungiamo invece la classe di test, basata su xUnit, chiamata "SampleCodeCoverageUtilsTests":</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="csharp language-csharp" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">using Xunit;
namespace SampleCodeCoverage.Tests
{
public class SampleCoverageUtilsTests
{
[Fact]
public void ShouldDoSumExecuteSum()
{
var result = SampleCodeCoverageUtils.DoSum(2, 3);
Assert.Equal(5, result);
}
[Fact]
public void ShouldDoSubtractExecuteSubtract()
{
var result = SampleCodeCoverageUtils.DoSubtract(3, 2);
Assert.Equal(1, result);
}
}
}</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Lanciando il comando di test di .NET Core <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">dotnet test</code> è immediatamente evidente come entrambi i test scritti soddisfano i requisiti (in gergo "sono in fase verde") e producono un output di questo genere:</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="sh language-sh" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">Microsoft (R) Test Execution Command Line Tool Version 16.2.0-preview-20190606-02
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
Test Run Successful.
Total tests: 2
Passed: 2
Total time: 1.6376 Seconds</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
A questo punto possiamo iniziare ad inserire i tools di code coverage, iniziando da <strong style="box-sizing: inherit; line-height: inherit;">Coverlet</strong> e dal suo adattatore per <strong style="box-sizing: inherit; line-height: inherit;">MSBuild</strong>. Sempre con la linea di comando nella folder della soluzione digitiamo:</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="sh language-sh" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">dotnet add ./SampleCodeCoverage.Tests/SampleCodeCoverage.Tests.csproj package coverlet.msbuild</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Dopo il ripristino da NuGet del pacchetto possiamo fare un check rapido per verificare se lo strumento è stato installato correttamente ed è attivo:</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="sh language-sh" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">dotnet test /p:CollectCoverage=true</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Se va tutto bene, dovrebbe essere mostrato il seguente output</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="sh language-sh" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">Starting test execution, please wait...
Test Run Successful.
Total tests: 2
Passed: 2
Total time: 1.6655 Seconds
Calculating coverage result...
Generating report '[...]/SampleCodeCoverage/SampleCodeCoverage.Tests/coverage.json'
+--------------------+--------+--------+--------+
| Module | Line | Branch | Method |
+--------------------+--------+--------+--------+
| SampleCodeCoverage | 66.66% | 100% | 66.66% |
+--------------------+--------+--------+--------+
+---------+--------+--------+--------+
| | Line | Branch | Method |
+---------+--------+--------+--------+
| Total | 66.66% | 100% | 66.66% |
+---------+--------+--------+--------+
| Average | 66.66% | 100% | 66.66% |
+---------+--------+--------+--------+</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
E' evidente che solo il <strong style="box-sizing: inherit; line-height: inherit;">66%</strong> delle linee di codice del nostro programma sono sotto copertura da parte dei test; con uno sguardo veloce al codice sorgente che abbiamo realizzato solo qualche linea più sopra, è chiaro che il metodo <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">DoMultiply</code> e il suo contenuto non sono coperte, mentre i metodi <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">DoSum</code>e <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">DoSubtract</code> lo sono.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Però questa cosa la possiamo desumere dal fatto che abbiamo solo poche linee di codice nel nostro programma. Immaginatevi uno scenario normale, in cui il progetto a cui state lavorando è composto da decine o addirittura centinaia di migliaia di righe di codice. Come potreste essere in grado di determinare quali solo le porzioni di codice coperte da test e quali no? Per fortuna qui ci viene in aiuto un altro prezioso strumento che - con l'aiuto di alcune opzioni di Coverlet - ci fornisce questa visione più dettaglitata.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Prima di tutto variamo leggermente il comando di test lanciato poco fa in modo da ridirezionare l'output nella posizione e nel formato:</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="sh language-sh" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput="./TestResults/"</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Il parametro opzionale <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">CoverletOutputFormat</code> permette di variare la tipogia di formato di uscita; nel nostro caso useremo il provider per <strong style="box-sizing: inherit; line-height: inherit;">Cobertura</strong>, ma sono supportate altre opzioni, come si può apprendere dalla <a href="https://github.com/tonerdo/coverlet/blob/master/Documentation/MSBuildIntegration.md#coverage-output" style="box-sizing: inherit; color: #008cba; line-height: inherit; text-decoration-line: none;">documentazione ufficiale</a>.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Il secondo parametro <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">CoverletOutput</code> serve per ridirezionare il percorso di generazione del file di output: in questo caso lo faremo in una directory che non sia la root del progetto di test, ma in una sotto-directory dedicata.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Dopo l'esecuzione il nuovo file di output con la code coverage dovrebbe essere stato generato nel percorso: <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">./SampleCodeCoverage.Tests/TestResults/coverage.cobertura.xml</code>.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
L'output a linea di comando non è male per avere una overview della percentuale di codice attualmente sotto copertura di test. Tuttavia non è sufficiente - soprattutto per uno sviluppatore - per comprendere nel dettaglio quali sono in punti in cui deve migliorare, facendo nel contempo le giuste valutazioni. Per questo motivo introduciamo nell'equazione un altro strumento più "user friendly": <a href="https://github.com/danielpalme/ReportGenerator" style="box-sizing: inherit; color: #008cba; line-height: inherit; text-decoration-line: none;"><strong style="box-sizing: inherit; line-height: inherit;">Report Generator</strong></a>, ottimo strumento realizzato da Daniel Palme.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Per utilizzare questo strumento è sufficiente aggiungere una sorta di "plugin" alla commandline di .NET Core. E' semplicissimo: basta aggiungere queste tre righe di codice al file <em style="box-sizing: inherit; line-height: inherit;">.csproj</em> del progetto di test della vostra soluzione:</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="xml language-xml" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;"><ItemGroup>
<DotNetCliToolReference Include="dotnet-reportgenerator-cli" Version="4.2.15" />
</ItemGroup></code></pre>
<blockquote style="background-color: white; border-left: 1px solid rgb(221, 221, 221); box-sizing: inherit; color: #6f6f6f; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin: 0px 0px 1.25rem; padding: 0.5625rem 1.25rem 0px 1.1875rem;">
<div style="box-sizing: inherit; font-family: inherit; font-size: 1rem; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Nota: al momento della scrittura di questo articolo la versione del tool è la <em style="box-sizing: inherit; line-height: inherit;">4.2.15</em>, ma potete verificare l'ultima versione direttamente sulla pagina <strong style="box-sizing: inherit; line-height: inherit;">NuGet</strong> di riferimento (<a href="https://www.nuget.org/packages/dotnet-reportgenerator-cli/" style="box-sizing: inherit; color: #008cba; line-height: inherit; text-decoration-line: none;">https://www.nuget.org/packages/dotnet-reportgenerator-cli/</a>).</div>
</blockquote>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Eseguiamo un restore del plugin con il solito comando <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">dotnet restore</code> e siamo pronti per il prossimo step.</div>
<blockquote style="background-color: white; border-left: 1px solid rgb(221, 221, 221); box-sizing: inherit; color: #6f6f6f; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin: 0px 0px 1.25rem; padding: 0.5625rem 1.25rem 0px 1.1875rem;">
<div style="box-sizing: inherit; font-family: inherit; font-size: 1rem; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Consiglio: nonostante sia possibile referenziare <em style="box-sizing: inherit; line-height: inherit;">reportgenerator</em> usando il comando <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">dotnet add package ...</code>, vi sconsiglio di farne uso perchè non vi permette di utilizzare le sue funzionalità da linea di comando.</div>
</blockquote>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
A differenza dei precedenti comandi, a questo punto dobbiamo spostarci <em style="box-sizing: inherit; line-height: inherit;">nella folder del progetto di test</em>. La motivazione è semplice: abbiamo referenziato l'estensione di "dotnet" unicamente sul progetto "SampleCodeCoverage.Tests" e quindi solo in quel contesto potrà essere utilizzata.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Lanciamo quindi il seguente comando che, come potete notare, specifica la posizione precisa del file di code coverage generato precedentemente (<code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">TestResults/coverage.cobertura.xml</code>), e definisce la folder target in cui il report sarà generato (<code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">TestResults/html</code>).</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="sh language-sh" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">dotnet reportgenerator "-reports:TestResults/coverage.cobertura.xml" "-targetdir:TestResults/html" -reporttypes:HTML;</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
L'ultimo parametro <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">reporttypes</code> permette di specificare la tipologia di formato del report. Nel nostro caso, per semplicità di consultazione, abbiamo optato per un semplice <strong style="box-sizing: inherit; line-height: inherit;">HTML</strong>, ma lo strumento supporta una quantità considerevole di differenti formati: <a href="https://github.com/danielpalme/ReportGenerator#supported-input-and-output-file-formats" style="box-sizing: inherit; color: #008cba; line-height: inherit; text-decoration-line: none;">https://github.com/danielpalme/ReportGenerator#supported-input-and-output-file-formats</a>.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Ad esecuzione avvenuta, nella folder target <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">TestResults/html</code> saranno presenti i file HTML con i dettagli della code coverage: basta lanciare il file <strong style="box-sizing: inherit; line-height: inherit;">index.html</strong> per accedere a tutte le informazioni:</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5lTWk_CBASDBwBx1DrkJ7O6UlQzCAHJpKFH-H5BbUBz8LNWqBaIhULkWHXL-ETh7tFRGMUAuca5vAbRG1bSiTr4veb05pD3EC7TU92_nRCZe3sExoqg_ANU7hoBbRkOL4eeepLnjwojk/s1600/report-generator-index.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="593" data-original-width="1301" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5lTWk_CBASDBwBx1DrkJ7O6UlQzCAHJpKFH-H5BbUBz8LNWqBaIhULkWHXL-ETh7tFRGMUAuca5vAbRG1bSiTr4veb05pD3EC7TU92_nRCZe3sExoqg_ANU7hoBbRkOL4eeepLnjwojk/s1600/report-generator-index.png" /></a></div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
<br /></div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Subito notiamo come il <strong style="box-sizing: inherit; line-height: inherit;">66% di coverage</strong> dell'intero progetto è evidenziato anche in questo caso. Ma la cosa veramente interessante è che il nostro report è completamente interattivo: possiamo sfruttare gli hyperlink del formato HTML per navigare i singoli artefatti sottoposti a test. Cliccando quindi su <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">SampleCodeCoverage.SampleCodeCoverageUtils</code> possiamo accedere alla pagina di dettaglio:</div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjCLeFYHTUARuUg9OVu1_FGjSKZm2CukZAUHbSQBCy6mQRBo0QNjqGpvZhBCGS_iNQqoK9cmfCWLN11M9T4mDGd1nehDR5HuEWheF2afub9dZuvFPpZ1PcnwEOM75iVAnFlbsDyxZXA6TY/s1600/report-generator-details.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1322" data-original-width="1107" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjCLeFYHTUARuUg9OVu1_FGjSKZm2CukZAUHbSQBCy6mQRBo0QNjqGpvZhBCGS_iNQqoK9cmfCWLN11M9T4mDGd1nehDR5HuEWheF2afub9dZuvFPpZ1PcnwEOM75iVAnFlbsDyxZXA6TY/s1600/report-generator-details.png" /></a></div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
<br /></div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Qui sono riportati i singoli metodi coperti da test, evidenziate le righe di codice sotto <em style="box-sizing: inherit; line-height: inherit;">code coverage</em> e le righe dei metodi che invece non sono testate.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Il tutto è molto chiaro e facilmente navigabile. E per generare un nuovo report aggiornato è sufficiente ricordarsi i due passaggi chiave:</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
1) Generare il file di output usando <strong style="box-sizing: inherit; line-height: inherit;">Coverlet</strong> (formato "Cobertura", nel nostro caso) 2) Lanciare <strong style="box-sizing: inherit; line-height: inherit;">Report Generator</strong> dalla cartella con il progetto di test</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Esiste anche la possibilità di installare sia Coverlet che Report Generator come strumenti globali, accessibili da qualunque folder del vostro environment senza che gli stessi siano installati come packages oppure come dipendenze.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Se per Coverlet non è la ritengo una cosa particolarmente utile (è possibile comunque attivarla tramite il comando <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">dotnet tool install --global coverlet.console</code>), per Report Generator può essere estremamente comodo, soprattutto in contesti di <strong style="box-sizing: inherit; line-height: inherit;">automazione</strong>. Lanciamo quindi il seguente comando:</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="sh language-sh" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">dotnet tool install --global dotnet-reportgenerator-globaltool</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Se va tutto bene il comando <code style="background-color: #f8f8f8; border-color: rgb(223, 223, 223); border-style: solid; border-width: 1px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0.125rem 0.3125rem 0.0625rem;">reportgenerator</code> dovrebbe essere attivo e, partendo dalla root-folder della nostra soluzione possiamo generare nuovamente il report HTML usando:</div>
<pre style="background-color: #f8f8f8; border: 1px solid rgb(223, 223, 223); box-sizing: inherit; color: #222222; margin-bottom: 1.5em; margin-top: 1.5em; overflow-wrap: break-word; padding: 0.125rem 0.3125rem 0.0625rem; white-space: pre-wrap;"><code class="sh language-sh" style="background-color: transparent; border: 0px; box-sizing: inherit; color: #333333; font-family: Consolas, "Liberation Mono", Courier, monospace; padding: 0px;">reportgenerator "-reports:./SampleCodeCoverage.Tests/TestResults/coverage.cobertura.xml" "-targetdir:./SampleCodeCoverage.Tests/TestResults/html" -reporttypes:HTML;</code></pre>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Fare <strong style="box-sizing: inherit; line-height: inherit;">code coverage</strong> non è molto più complesso che fare dei normali test. Tuttavia strumenti come quelli descritti in questo articolo dovrebbero farci comprendere che una copertura del <strong style="box-sizing: inherit; line-height: inherit;">100%</strong> delle nostre linee di codice di progetto è più una utopia che un obiettivo difficile da raggiungere. Quindi, come sempre, usiamo il buonsenso e sforziamoci di avere strumenti di "quality assurance" che coprono le parti importanti del nostro sistema, accettando che qualche errore fa parte del nostro mestiere ed è un rischio che possiamo correre.</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
Happy covering…</div>
<div style="background-color: white; box-sizing: inherit; color: #222222; font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.6; margin-bottom: 1.25rem; padding: 0px; text-rendering: optimizelegibility;">
M.</div>
Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-69638794048329638252018-08-28T19:08:00.000+02:002018-08-28T19:08:00.178+02:00Rimuovere fastidiose shortcut da Visual Studio 2017<div class="code-line" data-line="2" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
Premetto che questo non è un articolo tecnico, ma più un suggerimento che mi sembrava <strong>doveroso</strong> diffondere, per risolvere un fastidioso problema che affligge tutti gli sviluppatori che hanno un layout di tastiera <strong>italiano</strong>. Questo è un blog in italiano, quindi mi sembrava il posto più giusto per postarlo ;) ...</div>
<div class="code-line" data-line="2" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<br /></div>
<div class="code-line" data-line="4" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
Al mio rientro dalle ferie - come ogni buon sviluppatore che si rispetti - ho aggiornato la macchina di svilippo e gli ambienti, Visual Studio 2017 compreso. Installato l'update <strong>15.8.1</strong> mi sono trovato alcune novita (che non vi sto a raccontare) e una strana sorpresa: con la solita combinazione di tasti <code style="color: var(--vscode-textPreformat-foreground); font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; line-height: 19px;">Ctrl + AltGr + ]</code> invece che digitare la cara e vecchia parentesi graffa (a.k.a. <code style="color: var(--vscode-textPreformat-foreground); font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; line-height: 19px;">}</code>) mi sono trovato questo:</div>
<div class="code-line" data-line="4" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<br /></div>
<div class="code-line" data-line="6" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<img alt="Visual Studio Editor" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQ651HqxnJwS_H3ptIp0Rp9NKg00ekV-be9CSrsM1JlAX_4GEno3gKitqIyXRnEfGT4U8RMUN3gTNdyL4b3VWX8XjBgMMCmImkgvXCVUjbWQ0pTyyBR__RqL0wW1hjqXQn-LV7pHqvFos/s1600/00.png" style="max-height: 100%; max-width: 100%;" /></div>
<div class="code-line" data-line="6" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<br /></div>
<div class="code-line" data-line="8" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
Mi sono detto: "Nulla di importante. A che serve la parentesi graffa ad uno sviluppatore C# e JavaScript? Posso anche vivere senza...".</div>
<div class="code-line" data-line="8" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<br /></div>
<div class="code-line" data-line="10" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
Ironia a parte, dopo diversi minuti persi a "googleare" mi sono imbattuto nel post di un altro che - come me - ha gettato via buona parte del relax delle ferie per trovare una soluzione. A quanto pare non è complesso, e potevo anche arrivarci da solo a dirla tutta; è sufficiente andare raggiungere le opzioni di Visual Studio 2017 => Tools => Options => Environment => Keyboard, quindi digitare nella casella di ricerca la voce <strong>"Edit.ExpandSelectiontoContainingBlock"</strong> e rimuovere lo shortcut assegnato o cambiarlo.</div>
<div class="code-line" data-line="10" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<br /></div>
<div class="code-line" data-line="12" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<img alt="Visual Studio Options" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGybs9yFWXBfPDjcKuz_4gC44ZPNYzT0pvjkd8lxycOZebKnHk6o5HVMBYG7a0BpdI996RWo6lgNIfhuT1Iyt80SqIAabh35pBhUwWae6FOBV416CDPPKlKRGa5zrPX5L169ph9fpgwYc/s1600/02.PNG" style="max-height: 100%; max-width: 100%;" /></div>
<div class="code-line" data-line="14" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<br /></div>
<div class="code-line" data-line="14" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
A quanto pare la sola versione affetta dal problema è l'update <strong>15.8.1</strong> rilasciato il 21 di agosto. Ma è probabile che la cosa si presenterà nuovamente al prossimo update.</div>
<div class="code-line" data-line="14" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<br /></div>
<div class="code-line" data-line="16" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<img alt="Visual Studio About" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBLjKJ9CCFoHpu2fJK2sErCCHIFDpKLPzvEbIasiyL_52kc7ojMI5Umk4fYv2uhFBxvHjjsm4G_6X8JqlRt8LOkDTE4mPPWLd_aq1kU17-2Sm3qecH9DRtqQblIHe-3fDZA7TI1gpkkwo/s1600/01.png" style="max-height: 100%; max-width: 100%;" /></div>
<div class="code-line" data-line="18" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<br /></div>
<div class="code-line" data-line="18" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
Forse questo post è inutile, ma la prossima volta che mi succede, almeno so dove guardare ;)</div>
<div class="code-line" data-line="18" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
<br /></div>
<div class="code-line" data-line="20" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
Alla prossima!</div>
<div class="code-line" data-line="20" style="color: #236ebf; font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; font-size: 14px; position: relative;">
M.</div>
Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com2tag:blogger.com,1999:blog-6432843868391268534.post-76816300457950468142018-01-17T23:37:00.000+01:002018-02-15T09:53:23.136+01:00Costruire una DataSession custom con Chakra.Core<p>Lo scopo <strong>primario</strong> del framework <a href="https://www.nuget.org/packages/Chakra.Core">Chakra</a> è sempre stato quello di fornire delle strutture e guidelines per la scrittura di applicazioni enterprise che fossero al tempo stesso <strong>robuste</strong>, <strong>modulari</strong> e <strong>facilmente manutenibili</strong> nel corso del tempo.</p>
<p>Ma, visto che il tutto è farina del mio sacco, e che mi ritengo uno sviluppatore assolutamente nella media, non ho mai pensato che bastassero un pugno di classi ed interfacce per mettere in piedi una struttura tanto ambiziosa.</p>
<p>E’ con questo spirito che va approcciato questo framework. Farsi aiutare da alcune linee guida che indirizzano lo sviluppo, con la maggior parte del lavoro demandata alle qualità dello sviluppatore che scrive il codice: è lui l’unico responsabile della qualità del prodotto finale.</p>
<p>In questo post il mio scopo è mostrare come è possibile utilizzare alcune delle interfacce “meno conosciute” contenute in Chakra, per realizzare un provider custom di accesso dati. L’obiettivo è realizzare una di quelle feature tanto care a Chakra che è la strutturazione di un sistema completamente <strong>“Database Agnostic”</strong>.</p>
<p>Prima di tutto partiamo da un semplice esempio di come avviene l’inizializzazione di una <strong>Data Session</strong> fatta con un provider nativo già presente nel framework, quello “Mock”:</p>
<pre><code class="language-csharp">IDataSession dataSession = SessionFactory.OpenSession<MockupDataSession>()
</code></pre>
<p>L’istruzione utilizza la classe statica <strong>SessionFactory</strong> per aprire una nuova sessione dati - immaginatela come una astrazione di una connessione al vostro database - e manda in uscita un’istanza della sessione stessa attraverso la sua interfaccia generica.</p>
<p>L’istruzione successiva per mandare in esecuzione un <strong>discovery</strong> dei repository che implementano quel particolare provider di riferimento (quello “Mock” in questo caso) è la seguente:</p>
<pre><code class="language-csharp"><span class="hljs-keyword">var</span> productRepository = dataSession.ResolveRepository<IProductRepository>();
</code></pre>
<p>Come è ipotizzabile, se non avete nel vostro progetto o nei vostri riferimenti alcuna classe che implementa <strong>IProductRepository</strong> e sia formalmente legata al provider <strong>Mock</strong>, otterrete un errore a runtime che vi informa di questa situazione.</p>
<p>La vera domanda è questa: dove è nascosta la dipendenza di <strong>MockupDataSession</strong> - che abbiamo utilizzato prima per indirizzare la creazione dell’istanza di DataSession - e l’istanza del repository che implementa <strong>IProductRepository</strong> e che sarà realizzato usando la classe <strong>MockProductRepository</strong>?</p>
<p>La risposta è nascosta dalle interfacce che implementano sia <strong>MockupDataSession</strong> che <strong>MockProductRepository</strong>, passando per <strong>MockupDataTransation</strong> (di cui non abbiamo ancora parlato e parleremo solo marginalmente).</p>
<p>Vediamo un esempio pratico, partendo dalla nostra volontà di realizzare un provider custom che invece che andare su un database locale, utilizza come storage un servizio web remoto. L’obiettivo è che ogni entità che passa dai repository possa essere “creata-letta-aggiornata-cancellata” (<strong>C.R.U.D.</strong>) da dal nostro servizio web remoto.</p>
<p>Partiamo con l’implementazione di una DataSession custom che chiameremo <strong>RemoteApiDataSession</strong>.</p>
<pre><code class="language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">RemoteApiDataSession</span> : <span class="hljs-title">IDataSession</span>
{
<span class="hljs-keyword">private</span> <span class="hljs-keyword">bool</span> _IsDisposed;
<span class="hljs-keyword">public</span> HttpClient Client { <span class="hljs-keyword">get</span>; }
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">RemoteApiDataSession</span>(<span class="hljs-params"></span>)
</span>{
<span class="hljs-comment">//Inizializzazione del client HTTP con l'indirizzo base</span>
<span class="hljs-comment">//che magari è meglio mettere nei settings ;)</span>
<span class="hljs-keyword">var</span> baseUrl = <span class="hljs-string">"http://remoteapi.something.com/"</span>;
Client = <span class="hljs-keyword">new</span> HttpClient { BaseAddress = <span class="hljs-keyword">new</span> Uri(baseUrl) };
}
<span class="hljs-keyword">public</span> TRepositoryInterface ResolveRepository<TRepositoryInterface>()
<span class="hljs-keyword">where</span> TRepositoryInterface : IRepository
{
<span class="hljs-comment">//Utilizzo il metodo presente sull'helper per generare il repository</span>
<span class="hljs-keyword">return</span> RepositoryHelper.Resolve<TRepositoryInterface, IRemoteApiRepository>(<span class="hljs-keyword">this</span>);
}
<span class="hljs-keyword">public</span> IDataTransaction Transaction { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">private</span> <span class="hljs-keyword">set</span>; }
<span class="hljs-function"><span class="hljs-keyword">public</span> IDataTransaction <span class="hljs-title">BeginTransaction</span>(<span class="hljs-params"></span>)
</span>{
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> RemoteApiDataTransaction(<span class="hljs-keyword">this</span>);
}
<span class="hljs-keyword">public</span> TOutput As<TOutput>()
<span class="hljs-keyword">where</span> TOutput : <span class="hljs-keyword">class</span>
{
<span class="hljs-comment">//Se il tipo di destinazione non è lo stesso dell'istanza </span>
<span class="hljs-comment">//corrente emetto un'eccezione per indicare errore</span>
<span class="hljs-keyword">if</span> (GetType() != <span class="hljs-keyword">typeof</span>(TOutput))
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> InvalidCastException(<span class="hljs-keyword">string</span>.Format(<span class="hljs-string">"Unable to convert data session of "</span> +
<span class="hljs-string">"type '{0}' to requested type '{1}'."</span>, GetType().FullName, <span class="hljs-keyword">typeof</span>(TOutput).FullName));
<span class="hljs-comment">//Eseguo la conversione e ritorno</span>
<span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span> <span class="hljs-keyword">as</span> TOutput;
}
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">SetActiveTransaction</span>(<span class="hljs-params">IDataTransaction dataTransaction</span>)
</span>{
<span class="hljs-comment">//Validazione argomenti</span>
<span class="hljs-keyword">if</span> (dataTransaction == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ArgumentNullException(nameof(dataTransaction));
<span class="hljs-comment">//Imposto la transazione</span>
Transaction = dataTransaction;
}
~RemoteApiDataSession()
{
<span class="hljs-comment">//Richiamo i dispose implicito</span>
Dispose(<span class="hljs-keyword">false</span>);
}
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Dispose</span>(<span class="hljs-params"></span>)
</span>{
<span class="hljs-comment">//Eseguo una dispose esplicita</span>
Dispose(<span class="hljs-keyword">true</span>);
GC.SuppressFinalize(<span class="hljs-keyword">this</span>);
}
<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Dispose</span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> isDisposing</span>)
</span>{
<span class="hljs-comment">//Se l'oggetto è già rilasciato, esco</span>
<span class="hljs-keyword">if</span> (_IsDisposed)
<span class="hljs-keyword">return</span>;
<span class="hljs-comment">//Se è richiesto il rilascio esplicito</span>
<span class="hljs-keyword">if</span> (isDisposing)
{
<span class="hljs-comment">//Rilascio eventualmente il client</span>
}
<span class="hljs-comment">//Marco il dispose e invoco il GC</span>
_IsDisposed = <span class="hljs-keyword">true</span>;
}
}
</code></pre>
<p>In questa implementazione troviamo già un paio di interfacce/classi che normalmente non si vedono o utilizzano nell’inizializzazione dei repository e delle sessioni dati, ma che sono fondamentali per creare un legame tra la sessione stessa e l’istanza di repository: <strong>RemoteApiDataTransaction</strong> e <strong>IRemoteApiRepository</strong>.</p>
<pre><code class="language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">RemoteApiDataTransaction</span>: <span class="hljs-title">IDataTransaction</span>
{
<span class="hljs-keyword">private</span> <span class="hljs-keyword">bool</span> _IsDisposed;
<span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> RemoteApiDataSession _DataSession;
<span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> IsActive { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">private</span> <span class="hljs-keyword">set</span>; }
<span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> WasRolledBack { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">private</span> <span class="hljs-keyword">set</span>; }
<span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> WasCommitted { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">private</span> <span class="hljs-keyword">set</span>; }
<span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> IsTransactionOwner { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">protected</span> <span class="hljs-keyword">set</span>; }
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">RemoteApiDataTransaction</span>(<span class="hljs-params">RemoteApiDataSession dataSession</span>)
</span>{
<span class="hljs-comment">//Validazione argomenti</span>
<span class="hljs-keyword">if</span> (dataSession == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ArgumentNullException(nameof(dataSession));
<span class="hljs-comment">//Imposto lo stato iniziale</span>
IsActive = <span class="hljs-keyword">true</span>;
IsTransactionOwner = <span class="hljs-keyword">true</span>;
WasCommitted = <span class="hljs-keyword">false</span>;
WasRolledBack = <span class="hljs-keyword">false</span>;
<span class="hljs-comment">//Imposto la data session</span>
_DataSession = dataSession;
<span class="hljs-comment">//Se già esiste una transanzione sull'holder, esco</span>
<span class="hljs-keyword">if</span> (_DataSession.Transaction != <span class="hljs-keyword">null</span>)
<span class="hljs-keyword">return</span>;
<span class="hljs-comment">//Imposto l'istanza corrente</span>
_DataSession.SetActiveTransaction(<span class="hljs-keyword">this</span>);
}
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Commit</span>(<span class="hljs-params"></span>)
</span>{
<span class="hljs-comment">//Se l'istanza è la proprietaria della transazione</span>
<span class="hljs-keyword">if</span> (IsTransactionOwner)
{
<span class="hljs-comment">//Imposto i flag per commit</span>
IsActive = <span class="hljs-keyword">false</span>;
WasCommitted = <span class="hljs-keyword">true</span>;
WasRolledBack = <span class="hljs-keyword">false</span>;
<span class="hljs-comment">//Rimuovo il riferimento alla transazione</span>
_DataSession.SetActiveTransaction(<span class="hljs-keyword">this</span>);
}
}
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Rollback</span>(<span class="hljs-params"></span>)
</span>{
<span class="hljs-comment">//Se l'istanza è la proprietaria della transazione</span>
<span class="hljs-keyword">if</span> (IsTransactionOwner)
{
<span class="hljs-comment">//Imposto i flag per rollbak</span>
IsActive = <span class="hljs-keyword">false</span>;
WasCommitted = <span class="hljs-keyword">false</span>;
WasRolledBack = <span class="hljs-keyword">true</span>;
<span class="hljs-comment">//Rimuovo il riferimento alla transazione</span>
_DataSession.SetActiveTransaction(<span class="hljs-keyword">this</span>);
}
}
~RemoteApiDataTransaction()
{
<span class="hljs-comment">//Richiamo i dispose implicito</span>
Dispose(<span class="hljs-keyword">false</span>);
}
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Dispose</span>(<span class="hljs-params"></span>)
</span>{
<span class="hljs-comment">//Eseguo una dispose esplicita</span>
Dispose(<span class="hljs-keyword">true</span>);
GC.SuppressFinalize(<span class="hljs-keyword">this</span>);
}
<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Dispose</span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> isDisposing</span>)
</span>{
<span class="hljs-comment">//Se l'oggetto è già rilasciato, esco</span>
<span class="hljs-keyword">if</span> (_IsDisposed)
<span class="hljs-keyword">return</span>;
<span class="hljs-comment">//Se è richiesto il rilascio esplicito</span>
<span class="hljs-keyword">if</span> (isDisposing)
{
<span class="hljs-comment">//Se l'istanza è proprietaria e non ho chiuso, eccezione</span>
<span class="hljs-keyword">if</span> (IsTransactionOwner && IsActive)
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> InvalidOperationException(<span class="hljs-string">"Transaction was opened but never commited or rolled back."</span>);
}
<span class="hljs-comment">//Marco il dispose e invoco il GC</span>
_IsDisposed = <span class="hljs-keyword">true</span>;
}
}
</code></pre>
<p>La struttura <strong>RemoteApiDataTransaction</strong> immaginiamola come se fosse una <em>transazione dati</em> su cui è possibile fare “commit” o “rollback”. Si tratta però solo di una astrazione che serve per indirizzare quei “motori di database” che sono in grado di supportare le transazioni nativamente; solitamente solo i <strong>database relazionali</strong> (ma non tutti) e secondo modalità e stati che dipendono fortemente dalla natura del database stesso. Questa classe deve implementare <strong>IDataTransaction</strong> e tutte le sue caratteristiche.</p>
<p>Nel nostro caso, essendo il nostro “database” un servizio web, la transazione non esiste, e quindi l’implementazione presentata è assolutamente innoqua. Ma è necessario averla perchè la struttura di Chakra prevede che si approcci lo sviluppo del <strong><em>Business Layer</em></strong> applicativo senza conoscere la natura dello storage che sta sotto, e che quindi può essere cambiato a piacimento senza impattare minimamente sul funzionamento del sistema.</p>
<p>Cosa assolutamente differente è l’interfaccia <strong>IRemoteApiRepository</strong> che rappresenta di fatto la base per tutte le classi di repository che saranno afferenti alla data session <strong>RemoteApiDataSession</strong>.</p>
<pre><code class="language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IRemoteApiRepository</span>: <span class="hljs-title">IRepository</span>
{
}
</code></pre>
<p>E’ semplicissima: implementa l’interfaccia base <strong>IRepository</strong> e non fornisce alcun metodo custom, ma serve unicamente come <em>placeholder</em> per permettere al motore di Chakra di fare il discovery dinamico delle implementazioni legate a <strong>RemoteApiDataSession</strong>.</p>
<p>Se ritorniamo rapidamente ad un metodo implementato in RemoteApiDataSession possiamo notare una cosa fondamentale; l’interfaccia in questione è utilizzata da un helper interno di Chakra per identificare la classe in questione sulla base di due vincoli:</p>
<ul>
<li>Cerca una classe che implementi <strong>TRepositoryInterface</strong>, dove il tipo generico è l’interfaccia del repository da risolvere (es. <strong>IProductRepository</strong>).</li>
<li>Cerca una classe che implementi <strong>IRemoteApiRepository</strong></li>
</ul>
<pre><code class="language-csharp"><span class="hljs-keyword">public</span> TRepositoryInterface ResolveRepository<TRepositoryInterface>()
<span class="hljs-keyword">where</span> TRepositoryInterface : IRepository
{
<span class="hljs-keyword">return</span> RepositoryHelper.Resolve<TRepositoryInterface, IRemoteApiRepository>(<span class="hljs-keyword">this</span>);
}
</code></pre>
<p>Quindi, quello che rimane da fare è realizzare una classe con quelle caratteristiche, che quindi possa essere scoperta e innestata nel momento in qui la data session chiede la <strong>“Resolve”</strong> sul repository stesso.</p>
<p>Facciamo le cose per bene e facciamo una classe base, che tutti i repository con le stesse caratteristiche implementeranno:</p>
<pre><code class="language-csharp">public abstract class RemoteApiRepositoryBase<TEntity> : IRepository<TEntity>, IRemoteApiRepository
where TEntity : class, IEntity, new()
{
private bool _IsDisposed;
protected RemoteApiDataSession DataSession { get; }
protected RemoteApiRepositoryBase(IDataSession dataSession)
{
//Validazione argomenti
if (dataSession == null) throw new ArgumentNullException(nameof(dataSession));
//Tento il cast della sessione generica a RemoteApiDataSession
if (!(dataSession is RemoteApiDataSession currentSession))
throw new InvalidCastException(string.Format("Specified session of type '{0}' cannot be converted to type '{1}'.",
dataSession.GetType().FullName, typeof(RemoteApiDataSession).FullName));
//Imposto la proprietà della sessione
DataSession = currentSession;
}
public virtual TEntity GetSingle(Expression<Func<TEntity, bool>> expression)
{
//TODO Implementare la chiamata remota usando l'HttpClient presente nella DataSession locale
}
public virtual IList<TEntity> Fetch(Expression<Func<TEntity, bool>> filterExpression = null, int? startRowIndex = null,
int? maximumRows = null, Expression<Func<TEntity, object>> sortExpression = null, bool isDescending = false)
{
//TODO Implementare la chiamata remota usando l'HttpClient presente nella DataSession locale
}
public virtual int Count(Expression<Func<TEntity, bool>> filterExpression = null)
{
//TODO Implementare la chiamata remota usando l'HttpClient presente nella DataSession locale
}
public virtual void Save(TEntity entity)
{
//TODO Implementare la chiamata remota usando l'HttpClient presente nella DataSession locale
}
public bool IsValid(TEntity entity)
{
//Validazione argomenti
if (entity == null) throw new ArgumentNullException(nameof(entity));
//Utilizzo il metodo di validazione
var validations = Validate(entity);
//E' valido se non ho validazioni errate
return validations.Count == 0;
}
public IList<ValidationResult> Validate(TEntity entity)
{
//Validazione argomenti
if (entity == null) throw new ArgumentNullException(nameof(entity));
//Utilizzo l'helper per eseguire l'operazione
return RepositoryHelper.Validate(entity, DataSession);
}
public virtual void Delete(TEntity entity)
{
//TODO Implementare la chiamata remota usando l'HttpClient presente nella DataSession locale
}
~RemoteApiRepositoryBase()
{
//Richiamo i dispose implicito
Dispose(false);
}
public void Dispose()
{
//Eseguo una dispose esplicita
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Performs application-defined tasks associated with freeing,
/// releasing, or resetting unmanaged resources.
/// </summary>
/// <param name="isDisposing">Explicit dispose</param>
protected virtual void Dispose(bool isDisposing)
{
//Se l'oggetto è già rilasciato, esco
if (_IsDisposed)
return;
//Se è richiesto il rilascio esplicito
if (isDisposing)
{
//Rilascio della logica non finalizzabile
//=> Qui non è necessario
}
//Marco il dispose e invoco il GC
_IsDisposed = true;
}
}
</code></pre>
<p>Avere un repository di base ci semplifica drasticamente l’implementazione dei repository reali se questi non hanno metodi custom:</p>
<pre><code class="language-csharp">[Repository]
public class RemoteApiProductRepository: RemoteApiRepositoryBase<Product>, IProductRepository
{
public RemoteApiProductRepository(IDataSession dataSession)
: base(dataSession) { }
}
</code></pre>
<p>Attenzione! Ricordatevi sempre di marcare il repository appena creato con l’attributo <strong>[Repository]</strong> (lo trovate nel namespace <strong>ZenProgramming.Chakra.Core.Data.Repositories.Attributes</strong>) per assicurarsi che il motore di discovery di Chakra sia in grado di fare una ricerca ottimizzata considerando solo le classi opportunamente contrassegnate con quell’attributo.</p>
<p>Fatto tutto quanto sopra - dopo aver ovviamente interfacciato l’HttpClient per parlare con i servizio remoto - siamo pronti per fare quanto segue:</p>
<pre><code class="language-csharp">
<span class="hljs-comment">//Registrazione della sessione dati di default ad inizializzazione della nostra applicazione</span>
SessionFactory.RegisterDefaultSession<RemoteApiDataSession>();
<span class="hljs-keyword">using</span> (IDataSession dataSession = SessionFactory.OpenSession())
{
<span class="hljs-keyword">var</span> productRepository = dataSession.ResolveRepository<IProductRepository>();
<span class="hljs-keyword">var</span> entities = productRepository.GetSingle(p => p.Code == <span class="hljs-string">"001"</span>);
}
</code></pre>
<p>Questo avendo la certezza che la variabile <strong>productRepository</strong> sia un’istanza della classe <strong>RemoteApiProducRepository</strong> che abbiamo creato.</p>
<p>Chiudo con una nota. In questo post abbiamo realizzato un provider che lavora con uno storage remoto sotto forma di servizio web. Ma l’approccio è esattamente lo stesso se si volesse, per esempio, implementare un provider per <a href="https://www.mongodb.com/">MongoDB</a> oppure per <a href="http://couchdb.apache.org/">CouchDb</a>; si tratta di recuperare da NuGet i driver specifici per l’engine, e “wrapparli” all’interno di ipotetiche implementazioni di <strong>MongoDbDataSession</strong> utilizzando le stesse accortezze che ho usato io nell’implementazione presentata.</p>
<p>Alla prossima!<br>
M.</p>Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com4tag:blogger.com,1999:blog-6432843868391268534.post-388127878154005952016-09-17T09:44:00.000+02:002016-09-17T09:44:06.183+02:00Disabilitare la compilazione TypeScript in Visual Studio 2015Come sviluppatore Microsoft, è normale che sia un fan "più-che-sfegatato" di Visual Studio; ritengo sia l'ambiente
di sviluppo di gran lunga più completo e confortevole presente in commercio. Ma nonostante gli sforzi che il nuovo
corso della "Grande M" sta facendo per migliorarlo ancora, denota anche qualche difetto e limite nel momento in cui
si va al di la dei confini dell'ambiente .NET.
<br /><br />
In particolare, se ci si trova a lavorare con <strong>Angular2</strong> e <strong>TypeScript</strong> - benchè si sia ancora parzialmente sul territorio
Microsoft dal punto di vista del linguaggio - lavorando con Visual Studio (versione 2015) ci si può scontrare con
una serie di errori di compilazione dei file ".ts" e delle dipendende di riferimento.
<br /><br />
Il ritmo con cui vengono rilasciati gli aggiornamenti delle dipendenze e del compilatore TypeScript, è troppo elevato
per poter adeguatamente tenere il passo tramite un update del nostro amato IDE; forse anche per questo, il team di
Redmond ha pensato di affiancare ad esso un ambiente di sviluppo più leggero, semplificato e flessibile denominato
<a href="https://code.visualstudio.com/" target="_blank"><strong>Visual Studio Code</strong></a>, sul quale potrei spendere solo ottime parole.
<br /><br />
Ma la mia intenzione in questo post è porre l'accento sulla problematica sopra menzionata: si sta realizzando una
applicazione Angular2, usando un template ASP.NET MVC, ma Visual Studio 2015 non sembra essere è in grado di soddisfare
la compilazione client-side seppur non ci siano formali errori.
<br /><br />
In questi casi la soluzione migliore - a mio parere - è lavorare con il doppio IDE: VSCode per sviluppare i moduli
Angular2 con TypeScript (e ovviamente compilarli, bundlizzarli e fare unit test) e Visual Studio 2015 per compilare
l'applicazione "host" .NET che ospiterà tutti i file che finiranno sul server di produzione (magari con qualche
feature interessate fornita server-side). Questo ovviamente significa dover disabilitare la compilazione di TypeScript in VS2015, in modo che almeno
l'operazione di building del codice C# vada a buon fine.
<br /><br />
Non essendo presente un opportuno setting tra i menu dell'IDE per raggiungere lo scopo, è necessario agire in maniera più "artigianale",
editando il file <strong>".csproj"</strong> con Notepad (o altro editor), raggiungere il primo nodo XML denominato
<strong>PropertyGroup</strong> (solitamente è quello senza alcun attributo aggiuntivo) e cambiare il valore del tag
<strong>TypeScriptCompileBlocked</strong> - che come è facilmente intuibile, blocca i tentativi di compilazione dei
files TypeScript - dal valore di default <strong>false</strong> a <strong>true</strong>. Ad esempio:
<br /><br />
<script src="https://gist.github.com/maurobussini/859dc690afea6f79dee7520c6b683c84.js"></script>
E' da sottolineare che gli errori di compilazione TypeScript saranno ancora visibili nel pannello <strong>Error
List</strong> di VS2015 - putroppo ve li dovete tenere - ma la compilazione del vostro progetto andrà a buon fine, e con essa
una riduzione significativa della quantità di imprecazioni nella vostra giornata...
<br /><br />
Alla prossima!
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-57719844746381226842016-06-11T17:27:00.000+02:002016-06-11T17:27:02.389+02:00Utilizzare il nuovo Router di Angular2 (versione RC)Nel precedente post (<a href="http://zenprogramming-it.blogspot.it/2016/05/angular2-pipes-e-integrazione-con.html">link</a>) ho mostrato come integrare un componente JavaScript (per esempio MomentJs) con l'ecosistema TypeScript di Angular2. Questo approccio potrebbe essere molto utile nel primo periodo di utilizzo di questo framework, vista la momentanea scarsità di plugin nativi rispetto la controparte di AngularJs (prima versione).
<br /><br />
Benché il funzionamento di NG2 sia molto diverso da AngularJs, molti concetti sono stati ripresi e opportunamente riveduti. Inoltre, se avete avuto esperienze di utilizzo di <a href="http://durandaljs.com">DurandalJs</a> - altro fantastico framework JavaScript - noterete certamente alcuni aspetti che da esso sono stati acquisiti: l'ideatore di DurandalJs, Rob Eisenberg, ha lavorato fianco a fianco con il team di Angular2 per il primo anno di incubazione del progetto, per poi lasciare a causa di divergenza di opinioni. Ma la sua presenza ha fatto si che il nuovo sistema prendesse il meglio dei due mondi che fino a quel momento dominavano la scena per le Single Page Application.
<br /><br />
Tra tutte queste novità, è naturale che anche il modulo <strong>Router</strong> abbia subito una radicale riscrittura, portando con sé numerose novità, e sopratutto continui aggiornamenti che si sono presentati fino alla Release Candidate di Angular2 (N.d.R. in questo momento siamo alla RC1). È così che con l'uscita dalla "fase beta" e l'inizio della fase "release candidate", il team di sviluppo di Angular ha scelto di marcare come <strong>"deprecated"</strong> (=obsoleto) l'implementazione del router presente fino a quel momento, a favore di una versione migliorata, non così profondamente differente.
<br /><br />
Il problema più grosso di questa nuova implementazione non è tanto la diversità di funzionamento, ma quanto la difficoltà di reperire documentazione a riguardo su come riadattare il codice scritto in precedenza con questa nuova versione. Con un po' di fatica - e molto "googling" - le informazioni si trovano, ma la cosa richiede tempo: per questo motivo ho deciso di scrivere questo post, come mio personale "memento" e sperando di risparmiare a qualche collega del tempo prezioso.
<br /><br />
Come dicevo, l'approccio è molto simile. Basta innanzi tutto rimuovere i riferimenti a <strong>@angular/router-deprecated</strong> - che i ragazzi di Angular hanno comunque lasciato disponibile nelle ultime release del framework - e referenziare il nuovo <strong>@angular/router</strong>.
<br /><br />
A seguire riporto il codice ("quasi" completo) di una minuscola applicazione di tre pagine: una home, una pagina secondaria, e una di dettaglio che prende un parametro dalla querystring: spero apprezziate il mio sforzo nello "spirito meno parole e più codice" ;) .
<br /><br />
<script src="https://gist.github.com/maurobussini/e5cd3a5cb3ec6206eb62b01a5402481a.js"></script>
<br />
Le differenze sono quasi impercettibili, come potrete notare già da "app.component.ts":
<ul>
<li>il vecchio <strong>"RouteConfig"</strong> usato per definire i percorsi di navigazione è diventato <strong>"Routes"</strong></li>
<li>la route di default è sempre <strong>"/"</strong>, visto che è stata rimossa la proprietà <strong>"useAsDefault"</strong></li>
<li>è stata rimossa anche la proprietà <strong>"name"</strong> a favore dell'utilizzo del <strong>"path"</strong> (personalmente non l'ho mai capita la scelta di utilizzare "name" per riferirsi ad una particolare route)</li>
</ul>
<br />
Conseguenza delle modifiche di cui sopra, la direttiva per la costruzione dei percorsi (mi riferisco a <strong>routerLink</strong>) utilizza direttamente il path di navigazione, compresivo di eventuali parametri (molto più intuitivo, no?).
<br /><br />
In ultimo, cambia radicalmente il modo di intercettare i parametri di route nel componente che ne fa uso (nel nostro esempio in "detail.component.ts"). Sarà necessario implementare l'interfaccia <strong>OnActivate</strong> (o "CanActivate" o "CanDeactivate") e utilizzare le funzionalità di <strong>RouteSegment</strong>.
<br /><br />
L'impronta data dal team di Angular2 al nuovo framework mostra senza dubbio una visione molto ampia. C'è la sensazione che questo gioiellino, dopo un normale periodo di rodaggio, darà delle soddisfazioni e permetterà veramente di abbracciare lo sviluppo web senza piegarsi troppo ad una serie di compromessi che - fino a questo momento - abbiamo dovuto forzatamente accettare. Ora attendiamo l'uscita della versione ufficiale e definitiva...e partiamo!
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-76907382629881325442016-05-22T19:22:00.001+02:002016-05-22T19:22:31.026+02:00Angular2 - Pipes e integrazione con librerie JavascriptOrmai da qualche mese mi sto "dilettando" con Angular2. Essendo un grandissimo fan (ed utilizzatore) di AngularJs, mi sembrava una naturale evoluzione; questo senza dimenticare che questa nuova reincarnazione del gioiellino di Google, chiude le porte con il passato, rivisitando completamente la maggior parte dei concetti e la sua infrastruttura in nome delle performance e dell'estendibilità.
<br />
<br />
Con questo post - giuro che questa volta cercherò di essere più breve del solito - non è mia intenzione fare una panoramica di Angular2; navigando in rete si possono facilmente trovare moltissimi articoli (molti dei quali veramente ben scritti) che fanno introduzioni dettagliate al framework, e imbastiscono in maniera esauriente il classico "Hello World, Angular2!".
<br />
<br />
Vorrei quindi condividere un paio di pensieri in merito ad una delle funzionalità che più sto apprezzando di NG2 e che - in qualche modo - ereditano da AngularJs prima versione: le <strong>Pipes</strong>, precedentemente note come "Filters".
<br />
<br />
Per illustrare questa funzionalità utilizzerò una situazione reale in cui mi sono trovato. Avevo un classico back-end realizzato con ASP.NET Web Api - ovviamente REST - e una serie di emissioni di strutture JSON; alcune di esse - come è normale - ritornavano date e orari nel classico formato stringa ISO (es. "2016-05-22T18:40:02.926Z", il default di Json.NET). L'esigenza era quella di renderizzare sulla UI tali componenti di data/ora, nel formato che meglio si adatta alla situazione.
<br/>
<br/>
Mi direte: "E qual'è il problema? Basta usare la pipe 'date' già presente nel "core" di Angular2, e tutto si risolve.". L'affermazione è vera solo in parte; questo perchè la pipe in "bundle" con NG2 (almeno per l'attuale versione RC1) non è in grado di gestire il formato ISO delle date che ho riportato poco sopra. Bisogna prima riconvertirlo in un oggetto "Date" Javascript (o TypeScript) e poi usarlo come binding per la UI per fare in modo che funzioni. Ma ovviamente questo vuol dire perdersi la possibilità di usare direttamente il modello proveniente dal server, scrivendo codice aggiuntivo per venire incontro all'inconveniente.
<br />
<br />
Ho deciso quindi di indirizzare la questione con una pipe custom, che sfrutta le potenzialità di <strong>MomentJs</strong> direttamente da TypeScript.
<br />
<br />
Per prima cosa - come potete vedere nel codice a seguire - dichiaro la variabile "moment", presente a livello globale perchè ho incluso nella mia pagina host il relativo file di MomentJs (direttamente il file ".js"); quindi dichiaro la Pipe custom (che con poca fantasia ho chiamato <strong>MomentDatePipe</strong>), che implementa <strong>PipeTransform</strong>. L'unico metodo richiesto per implementare l'interfaccia in questione è la dichiarazione di <strong>transform</strong>, che prende in input il valore di binding presente nel modello (nel mio caso una stringa con la data ISO), un "exponent" (che è solitamente un parametro passato alla pipe ed è facoltativo), e restituisce un valore (nel mio caso un'altra stringa con la formattazione di uscita).
<br />
<br />
<script src="https://gist.github.com/maurobussini/98a73cf2c75a5af85f00540e8181101a.js"></script>
<br />
<br />
Gli script aggiuntivi sono la dichiarazione del componente che farà uso della pipe (che riporta semplicisticamente la stringa data ISO sorgente), e il suo template HTML dove utilizzo in maniera dichiarativa la pipe appena realizzata, sfruttando l'alias riportato nel metadato di dichiarazione (=> @Pipe({ name: 'momentDate' })). Non dimentichiamoci di dichiarare l'uso della pipe nel componente: se non lo facciamo, il componente stesso non sarà in grado di trattare in maniera corretta l'alias dichiarato!
<br />
<br />
Con poche righe di codice, ho compensato una limitazione di Angular2; anche se sono abbastanza sicuro che con l'uscita della versione definitiva il team di sviluppo di NG2 avrà egli stesso provveduto a indirizzare la problematica in maniera più elegante.
<br />
Happy NG2!
<br />
M.
Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-2232179144648575762015-08-08T15:36:00.000+02:002015-08-10T08:31:58.379+02:00RESTful Stress: misurare le performance di un servizio REST Nel post di qualche settimana fa (lo potete trovare a <a href="http://zenprogramming-it.blogspot.it/2015/07/load-tests-stress-tests-e-performance.html">questo link</a>) abbiamo fatto una rapida introduzione al tema delle <b>misurazioni delle prestazioni</b> di una piattaforma applicativa fortemente basata su REST. Il tema è estremamente complesso, e le quattro idee che ho "buttato giù" sono solo la superficie di un mondo vastissimo ed articolato.
<br />
<br />
Al mio primo approccio con questo argomento - non lo nego - mi sono trovato parecchio spiazzato. Sono sempre stato abituato a "calcoli insindacabili" e certezze; ma tutto questo richiede una buona dose di analisi dei comportamenti e di...speranza.
<br />
<br />
Dico questo per un motivo molto semplice: rispondere alla domanda <b>"quanti utenti l'ora è in grado di supportare la mia applicazione?"</b>, non è così banale. Vuol dire prima misurare tutto il misurabile, e poi dover fare i conti con la variabile più imprevedibile con cui un professionista dell'IT deve fare i conti: il comportamento dell'utente medio.
<br />
<br />
Se per la "variabile utente" non potete fare altro che osservare e ipotizzare che venga fatto un uso normale del vostro sistema, per le misurazioni - forse - può darvi una mano una piccola Chrome Application (originariamente detta "Chrome Packaged App") a cui ho iniziato a lavorare nel 2013 e che ho recentemente reingegnerizzato: <a href="https://chrome.google.com/webstore/detail/restful-stress/lljgneahfmgjmpglpbhmkangancgdgeb"><b>RESTful Stress</b></a>.
<br />
<br />
In breve. Si tratta di uno strumento in grado di simulare le chiamate HTTP verso una piattaforma di servizi REST, usando il componente XHR (XMLHttpRequest) del browser. In parole povere, fa esattamente quello che farebbe uno script jQuery client-side (una cosa del tipo "$.ajax(...)" ecc.) che invia e riceve dati da un server remoto.
<br />
<br />
Mi chiederete: "E dove sta la differenza?". È in grado di <b>iterare</b> la vostra chiamata HTTP (una o più chiamate) numerose volte, campionando l'orario di inizio, di fine, la durata, lo stato e il contenuto della response. Una volta rilevate le informazioni è in grado di produrre un feedback relativo alle performance che può essere facilmente e rapidamente interpretato.
<br />
<br />
Una volta installato RESTful Stress (versione 1.2.0) dal Chrome Web Store (a <a href="https://chrome.google.com/webstore/detail/restful-stress/lljgneahfmgjmpglpbhmkangancgdgeb">questo indirizzo</a>), lo avviate, e vi troverete davanti a questa prima pagina:
<br /><br/>
<div class="separator" style="clear: both; text-align: left;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzqitq7xda5WiBa-m8AwrzDnZAaYiedq_kR5UuPF2zST9v6aN90kLZhFKoqICgehDAxV88T63vUApUMY77WHimEOSfedPYvC19OMjEnZ7qLxFKXSgnAKkTKbeCygUuZY3TDyhJGjqTyWo/s1600/screen_001.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;">
<img border="0" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzqitq7xda5WiBa-m8AwrzDnZAaYiedq_kR5UuPF2zST9v6aN90kLZhFKoqICgehDAxV88T63vUApUMY77WHimEOSfedPYvC19OMjEnZ7qLxFKXSgnAKkTKbeCygUuZY3TDyhJGjqTyWo/s640/screen_001.PNG" width="640" />
</a>
</div>
<br />
Non complichiamo subito le cose: tralasciamo la modalità di misurazione presente in alto a sinistra (quella con " Atom", "Massive" e "Scenario", per intenderci) prendendo in considerazione il default <b>"Atom"</b>. Come è possibile vedere c'è tutto l'occorrente per definite le specifiche di una chiamata HTTP:
<br />
<ul>
<li><b>Metodo HTTP</b>, o "Verb": GET, POST, PUT, DELETE i più usati</li>
<li><b>Headers</b>, in formato JSON, per maggiore flessibilità</li>
<li><b>Body</b>, anche questo in JSON visto che siamo su REST</li>
<li><b>With credentials</b>, ovvero forzando il passaggio di cookie autorizzativi ottenuti in una precedente iterazione</li>
<li><b>URL</b> di destinazione (beh....ovviamente)</li>
</ul>
Nel box posto in alto alla view in questione, sono presenti le specifiche dell'iterazione di RESTful Stress: numero di cicli di esecuzione (<b>"iterations"</b>), intervallo tra una iterazione e la successiva (<b>"delay"</b>), <b>"timeout"</b> oltre il quale una chiamata HTTP è considerata fallita, e numero di iterazioni considerate <b>"warmup"</b> del sistema.
<br />
<br />
Solo una piccola parentesi su questa ultima specifica ("warmup", appunto), per sottolineare come - sovente - quando si invoca un servizio web (di qualunque genere) è normale che le prime chiamate siano particolarmente <b>lente rispetto alle successive</b>. In particolare, in ambiente .NET, IIS ha un comportamento tale che porta ad inizializzare alcuni componenti applicativi solo alla prima esecuzione degli stessi. E' così quindi che, durante i primi cicli di esecuzione, la nostra applicazione sembra essere particolarmente letargica: in realtà è sono in fase di "risveglio" prima di prendere il giusto ritmo. E' un diesel, se vogliamo...
<br />
<br />
A fondo pagina c'è tutto l'occorrente per non perdere le nostre impostazioni: "Store settings", che permetterà di salvare le specifiche nella storage area di RESTFul Stress (unitamente ad un nome mnemonico), e due comodi pulsanti per scaricare gli stessi settings su un file del vostro computer locale ("Download"), e per poi ripristinarli in seguito ("Upload").
<br />
<br />
Una volta impostati i dettagli, non resta altro da fare che cliccare il pulsante "Play", ed attendere che il tool faccia il suo sporco lavoro, eventualmente fermandolo con "Stop" (attenzione: bisognerà attendere la fine dell'esecuzione della request per avere l'arresto effettivo) o "Reset".
<br />
<br />
Completata l'esecuzione dei cicli (o durante il progress...e chi ce lo vieta?) possiamo analizzare la "history" delle chiamate; ciascun elemento è caratterizzato da :
<br />
<ul>
<li><b>codice di Status HTTP</b> recuperato dal server: 200, 401, 404, ecc...(eventuali status codes con valore "0" rappresentano sempre una request mai effettuata)</li>
<li><b>orario di inizio</b> della request verso il server</li>
<li><b>durata</b> in millisecondi, ovvero il tempo tra l'invio della request e la ricezione della corrispondente response</li>
<li><b>lunghezza</b> in bytes del contenuto della risposta</li>
</ul>
...e un comodo pulsante per visualizzare il contenuto del body della response stessa.
<br />
<br />
<div class="separator" style="clear: both; text-align: left;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifxOFUo5uBhueodpYVlk4Xeiyc1t22-NHjLeKNd4hjD4GLwpXRUQi99X0vZSQ2EEHPxPj7wfwb3Jm1vwJrWi4nA8uPWXKYUiIYSvDmPkjh9IfXk0RnKYdLCSkpWeI2XPsbyG5aUXw76Ns/s1600/screen_002.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;">
<img border="0" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEifxOFUo5uBhueodpYVlk4Xeiyc1t22-NHjLeKNd4hjD4GLwpXRUQi99X0vZSQ2EEHPxPj7wfwb3Jm1vwJrWi4nA8uPWXKYUiIYSvDmPkjh9IfXk0RnKYdLCSkpWeI2XPsbyG5aUXw76Ns/s640/screen_002.PNG" width="640" />
</a>
</div>
<br />
Utilizzando gli appositi "commands" localizzati nella parte alta della pagina, per non perdere i risultati del proprio lavoro, è possibile estrarre tutte le informazioni raccolte in formato JSON ("Download"), ricaricarle in RESTFul Stress per una consultazione successiva ("Upload") oppure produrre il più classico dei file CSV con un subset dei dati in elenco.
<br />
<br />
La scelta di JSON come formato di esportazione dei risultati di esecuzione non è casuale: spesso mi sono trovato a dover utilizzare gli stessi dati in altre applicazioni JavaScript-based (es. NodeJs o AngularJs) o .NET (usando il meraviglioso componente Json.NET): quale miglior formato per manipolare dati privi di uno schema predefinito?
<br />
<br />
La pagina "Chart" mostra in formato grafico l'andamento delle iterazioni sul servizio: in un secondo è possibile evidenziare eventuali deterioramenti della performance della piattaforma (magari per risorse che non vengono rilasciate correttamente), picchi di utilizzo, eventuali response con codici di errori (rappresentati con il colore rosso), e - ovviamente - le iterazioni che abbiamo battezzato nella pagina di settings come "warmup" del sistema (colore azzurro); queste ultime - come detto - vanno correttamente pesate nelle nostre considerazioni, essendo figlie di una fase di inizializzazione del sistema, e che quindi non rappresentano la reattività dello stesso a regime.
<br /><br/>
<div class="separator" style="clear: both; text-align: left;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWMpIA3iGw6P6_hS1noPNpUsU2B46jLa-VyJdCdjcfRtbU-RRFw8RywllnmN4QivJdBKDPaEXBfCEh92CQFIdLsHtWm_ZoA59wKRUbxnIU7g43BwNwvf9MUUdkTw7mQ8ldRTgJaFEX4jk/s1600/screen_003.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;">
<img border="0" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWMpIA3iGw6P6_hS1noPNpUsU2B46jLa-VyJdCdjcfRtbU-RRFw8RywllnmN4QivJdBKDPaEXBfCEh92CQFIdLsHtWm_ZoA59wKRUbxnIU7g43BwNwvf9MUUdkTw7mQ8ldRTgJaFEX4jk/s640/screen_003.PNG" width="640" />
</a>
</div>
<br />
Il grafico è corredato di alcune misurazioni "grossolane" delle tempistiche: <b>valori medi</b> che danno un'idea della reattività della piattaforma, e permettono di iniziare a fare qualche considerazione in più rispetto i concetti espressi nel <a href="http://zenprogramming-it.blogspot.it/2015/07/load-tests-stress-tests-e-performance.html">precedente post</a>.
<br />
<br />
Facciamo un salto in avanti, e arriviamo alla sezione che permette di ripristinare i settings salvati nella prima pagina di RESTful Stress. Mi sembra una grossa perdita di tempo illustrarvi il suo contenuto: l'immagine parla da sola, e con essa il funzionamento di questa feature.
<br /><br/>
<div class="separator" style="clear: both; text-align: left;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhXYTg4t-aIR4HUOg1BOXCvDx2B87w8smY8OtcppkHhi0PGVfFC8haO0gIOERdWuGQ4hq9f6D7mZgLssKDr18so7rzRKUsUGztABMvK3-rcMPrTKiSQu9DzQ2CtHR4T-zFGv5tGiBvN9H0/s1600/screen_005.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;">
<img border="0" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhXYTg4t-aIR4HUOg1BOXCvDx2B87w8smY8OtcppkHhi0PGVfFC8haO0gIOERdWuGQ4hq9f6D7mZgLssKDr18so7rzRKUsUGztABMvK3-rcMPrTKiSQu9DzQ2CtHR4T-zFGv5tGiBvN9H0/s640/screen_005.PNG" width="640" />
</a>
</div>
<br />
No, non mi sono dimenticato della pagina <b>"Performance"</b>: è senza dubbio la parte più interessante e quella per cui avrete probabilmente iniziato a leggere questo post. Essa porta con se molte considerazioni, calcoli, manipolazioni e spiegazioni che richiedono il loro tempo...e non quattro concetti buttati sul piatto in malo modo. Me li custodisco come "succosi" argomenti per il prossimo articolo...
<br />
<br />
Buone ferie a tutti...
<br />
M.
Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-42886046109357654812015-07-11T14:14:00.000+02:002015-07-23T19:21:41.322+02:00Load tests, Stress tests e performance di un servizio RESTA fine 2012 ho iniziato ad avvertire una maggiore sensibilità dei clienti riguardo il tema delle prestazioni di un sistema basato sul web. Parecchi degli applicativi a cui stavo lavorando in quel periodo erano già basati su architetture con piattaforme REST e dati in formato JSON, che si adattavano molto bene al tema della "misurazione dei tempi di risposta". La separazione dei moduli applicativi e - in particolare - del "presentation layer" dallo strato di servizi, è a mio parere una caratteristica essenziale per ottenere una metrica dell'applicazione sotto carico.
<br /><br />
Faccio subito una precisazione ritengo doverosa. In ogni situazione - ed in particolar modo se si lavora con SPA (Single Page Application) - è fondamentale distinguere tra le problematiche prestazioni dovute ad un codice client "lento", magari non adeguatamente ottimizzato, da una piattaforma di servizi che fatica a fornire risposte nei tempi desiderati.
<br /><br />
In questo post mi soffermerò su questa seconda casistica, poichè i problemi di performance lato client sono molto complicati da individuare, e spesso sono strettamente legati alla tecnologia (o al framework client, nel caso di JavaScript) che si sta utilizzando.
<br /><br />
Ma ritorniamo rapidamente al nostro server. Immaginiamo di avere un backend basato su ASP.NET WebAPI: essendo sviluppatore .NET mi sembra la scelta più naturale. Abbiamo già innestato controller applicativi e le action che emettono correttamente i nostri flussi dati in JSON. Supponiamo di riuscire a tracciare un elenco delle request che una normale navigazione della nostra applicazione produce durante l'utilizzo della UI grafica. Questo tracciato di request (e opportuni verb, header e parametri) rappresenta una "sessione utente": questo è il dominio che dovremo andare a considerare per fare la nostra misurazione delle tempistiche di risposta.
<br /><br />
Un esempio parla più di mille teorie, si dice di solito...ed è vero! Quindi ipotizziamo di avere un classico portale di commercio elettronico che permette la navigazione del catalogo, la scelta di uno o più prodotti da mettere nel carello virtuale, l'accesso autenticato tramite username e password, e l'operazione di checkout con acquisto dei prodotti. Ignoriamo l'interfacciamento con i sistemi di pagamento tramite carta di credito: è troppo complicato e per nulla utile in questo caso...
<br />
Una normale sessione utente (o "user session") è composta da questi passi funzionali:
<ol>
<li>Visualizzazione della pagina principale del sistema e scaricamento della lista dei primi 10 prodotti più venduti presenti nello storage</li>
<li>Navigazione al catalogo e visualizzazione delle categorie merceologiche</li>
<li>Visualizzazione dell'elenco dei prodotti presenti nella categoria</li>
<li>Dettaglio della scheda di un prodotto, sue specifiche e descrizioni</li>
<li>Aggiunta di quel prodotto nel carrello elettronico</li>
<li>Inserimento di username e password utente (ipotiziamo che il profilo utente sia già stato creato)</li>
<li>Conferma dell'acquisto con la funzione di "checkout"</li>
</ol>
I passi riportati sopra rappresentano un comportamento "standard" di un utente del nostro portale, e - almeno in questa fase iniziale - non complichiamo troppo le cose pensando che ci possono (e ci saranno) delle naturali deviazioni rispetto questo specifico tracciato (es. navigazione più lunga, visualizzazione di più schede di prodotti, ecc.).
<br /><br />
Quello che conta è "simulare" grossomodo quello che comporta una navigazione "da manuale", per farsi un'idea di quante "user session" contemporanee il nostro sistema è in grado di supportare senza creare disservizi o impattare negativamente sulla user experience dell'utente.
<br /><br />
Mi rendo conto del fatto che, tutto quello che sto descrivendo, suona un po' "impreciso" e "ipotizzato"; ma vi posso assicurare che la predizione dei carichi a regime di una applicazione web, non è una scienza esatta. Le variabili sono migliaia e - cosa più importante - la componente fondamentale che farà pesare da una parte o dall'altra i vostri risultati e il <b>comportamento umano</b>, per definizione impredicibile ed incontrollabile. Quindi, il meglio che potete fare è cercare di approssimare con basso margine di errore, sperando che le eccezioni - che comunque ci saranno - non vadano a pesare in maniera troppo rilevante sul numero finale.
<br /><br />
Quello che bisogna fare da questo punto in poi è certamente più matematico, e meno da "chiromante" ;). Si tratta di mettere un numero (in millisecondi) di fianco ad ogni punto del precedente elenco. Come? Facile: rilevando l'orario corrente <b>prima</b> dell'esecuzione della request lato client, e <b>dopo</b>aver ricevuto la relativa response; la durata di ogni singolo step è presto misurata.
<br /><br />
E' fondamentale calcolare altri due indicatori : l'<b>average duration</b> di una request (la durata media), e il <b>throughput</b>, cioè il numero di richieste al secondo che la nostra service platform è in grado di erogare:
<ul>
<li>
[average duration]: tempo medio di durata, somma delle durate (in millisecondi) delle varie richieste presenti nel nostro scenario di "user session", diviso per il numero di richieste...che ve lo dico a fare? ;)
</li>
<li>
[throughput]: calcolato come <b>1000 / [average duration]</b>, dove il valore 1000 rappresenta il numero di millisecondi in un secondo, il tutto misurato in "richieste al secondo"
</li>
</ul>
L'obiettivo finale è capire quanti <b>utenti concorrenti</b> il nostro sistema è in grado di sostenere. E per fare ciò è necessario usare la <b><a href="https://en.wikipedia.org/wiki/Little%27s_law">Legge di Little</a></b>, che dice: il numero di utenti simultanei reali ("concurrent users") che un'applicazione è in grado di supportare è definibile con la formula:
<ul>
<li>[concurrent users] = [throughput] / λ</li>
</ul>
con il simbolo <b>λ</b> che rappresenta la media del valore di arrivo ("arrival rate") delle richieste. Questo "arrival rate" è l'inverso dell' <b>inter-arrival time</b>, cioè il tempo (misurato in millisecondi) che trascorre tra una richiesta e la successiva.
<br /><br />
E' giunto il momento di fare una considerazione estremamente importante: esiste una netta distinzione tra <b>Stress Test</b> e <b>Load Tests</b>. I primi sono mirati a trovare il <b>punto di rottura</b> di una applicazione, cioè a sovraccaricare un sistema al punto tale da raggiungere una situazione nella quale il servizio stesso non viene più erogato. I test di carico (o "Load Tests") sono invece mirati a misurare il numero massimo di utenti (o sessioni utente) che l'applicativo è in grado di sostenere in una porzione di tempo stabilita, erogando il servizio definito dal processo di business.
<br /><br />
I test di carico devono quindi rappresentare una stima (ovviamente simulata ed approssimata) di un utilizzo reale. E realisticamente parlando, è abbastanza <b>inverosimile</b> che le request alla piattaforma WebAPI arrivino in continuazione, senza sosta alcuna. Va quindi considerato un altro parametro (ancora un'altro...mi spiace) che rappresenta l'intervallo tra l'arrivo di una <b>response</b>, e l'inizio della <b>request</b> successiva: questo parametro è solitamente chiamato "<b>think time</b>".
<br /><br />
L'inter-arrival time che abbiamo citato qualche riga più sopra, è calcolato sommando la durata media dei una singola request/response al server, al think time. E il think time a sua volta è stabilito a tavolino - non calcolato - e basato su un'osservazione del comportamento di modelli simili in situazioni reali; il che vuol dire che la sua durata è solitamente rilevata su dati prelevati a campione durante l'uso di una applicazione.
<br /><br />
Ipotizziamo quindi che il "tempo di riflessione" (traduzione orribile, me ne rendo conto) sia di 5 secondi; ed è una casistica tutto sommato verosimile quella per cui possa arrivare - in media - una request ogni 5000 millesimi di secondo alla piattaforma WebAPI. Quindi, riassumendo:
<ul>
<li>[inter-arrival time] = [think time] + [average request duration]</li>
<li>[arrival rate] = 1 / [inter-arrival time]
</ul>
Il calcolo del numero di utenti - come abbiamo visto - è quindi strettamente legato alla durata di esecuzione della singola richiesta, e dal tempo di intervallo stimato tra una richiesta e l'altra. Ed essendo quest'ultimo una stima, non è possibile dare una risposta precisa ad un vostro eventuale cliente che vi chiede : "Quanti utenti è in grado di supportare la mia applicazione?". Potete rispondere solo avvalendovi di alcuni parametri dettati dall'esperienza (vostra o di altri prima di voi), misurando quanto è effettivamente misurabile, ma nulla di più.
<br /><br />
Ma anche solo misurare quel poco che è certo, non è una cosa banale. Ed è per questo che a metà 2013 ho iniziato un piccolo progetto, di cui ho giù parlato in <a href="http://zenprogramming-it.blogspot.it/2013/12/chrome-development-restful-stress.html">questo post</a>, chiamato <b>RESTful Stress</b>, disponibile sul <a href="https://chrome.google.com/webstore/detail/restful-stress/lljgneahfmgjmpglpbhmkangancgdgeb">Chrome Web Store</a> come Chrome App.
<br /><br />
Come si usa? Non è nulla di complicato...ma ne parleremo la prossima volta...
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com2tag:blogger.com,1999:blog-6432843868391268534.post-15204669552154084992015-04-11T14:13:00.000+02:002015-04-13T08:33:15.991+02:00Upload di un file e un form su una action ASP.NET Web Api con MultiPartFormDataContentLavorando con ASP.NET Web Api vi sarà sicuramente capitato di dover caricare lato server un file presente sul computer dell'utente. Se state usando un client basato su JavaScript - magari jQuery, se non volete farvi del male con XHR - è certamente una cosa piuttosto semplice: sono decide, per non dire centinaia, i controlli che si trovano in rete che già fanno quello che vi serve; basta trovare quello giusto che risponde alle vostre esigenze, aggiungere il riferimeto allo script, e....via!
<br /><br />
Se poi, come sovente accade, dovete passare anche dei dati "form" nella richiesta di upload - magari per associare il file caricato sul server ad una entità presente nella vostra base dati - l'operazione richiede qualche sforzo in più, pur rimanendo fattibile e molto documentata. Parlo sempre di JavaScript, eh...
<br /><br />
Ma se dovete fare questa cosa da .NET, magari un'applicazione WPF o una semplice console application, anche eseguire un semplice upload non è banalissimo. I post che si trovano in rete sono talvolta discordanti, e tendono a confondere il lettore...sopratutto se il lettore sono io ;) .
<br /><br />
Detto questo, dopo numerosi tentativi e un paio di orette perse, sono riuscito a trovare il "bandolo della matassa". Ma non perdiamoci in chiacchere e vediamo subito un po' di codice. Questa è la action Web API che ho realizzato lato server per un recente progetto (opportunamente semplificata, chiaramente):
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Results;
using ZenProgramming.MultipartSample.Api.Http;
using ZenProgramming.MultipartSample.Models;
namespace ZenProgramming.MultipartSample.Api.Controllers
{
/// <summary>
/// Controller for manage scans
/// </summary>
[RoutePrefix("api/Scans")]
public class ScansController : ApiController
{
[HttpPost]
[Route("PushScan")]
public async Task<IHttpActionResult> PushScan()
{
//Verifico che sia supportato un MIME-multipart dal client
if (!Request.Content.IsMimeMultipartContent())
return new StatusCodeResult(HttpStatusCode.UnsupportedMediaType, this);
//Utilizzo la cartella temporanea per salvare lo stream
string tempFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Temp");
//Recupero il provider "multipart" passando il percorso della folder temporanea
var provider = new MultipartFormDataStreamProvider(tempFolder);
try
{
//Leggo il contenuto dello stream remoto
await Request.Content.ReadAsMultipartAsync(provider);
}
catch (Exception exception)
{
//Ritorno l'errore dell'eccezione
return BadRequest(exception.Message);
}
//Recupero l'id dell'entità di riferimento
int documentId;
if (!Int32.TryParse(provider.FormData.Get("documentId"), out documentId))
return BadRequest("Form data does not contains a valid element with key 'documentId'.");
//Recupero il riferimento all'entità documento
Document document = ApplicationServiceLayer.GetDocument(documentId);
//Se non ho trovato l'elemento, not found
if (document == null)
return NotFound();
//Scorro tutti i files postati al server ed eseguo il salvataggio
foreach (MultipartFileData file in provider.FileData)
{
//Ripulisco il nome del file che contiene delle "virgolette"
string fileName = file.Headers.ContentDisposition.FileName.Replace("\"", "");
FileInfo fileInfo = new FileInfo(fileName);
//Recupero il contenuto del file
byte[] binary = File.ReadAllBytes(file.LocalFileName);
//Creo l'entità scan su cui depositare il binario
Scan scan = new Scan
{
Behavior = document.Behavior,
DocumentId = documentId,
LibraryId = document.LibraryId,
FileName = fileInfo.Name,
FileSize = binary.Length,
Operator = User.Identity.Name,
FileContent = binary
};
//Tento il salvataggio dello scan nel database
var validations = ApplicationServiceLayer.SaveScan(scan);
//Se ho errori di validazione, li accodo
foreach (var current in validations)
ModelState.AddModelError("", current.ErrorMessage);
//Se il modello non è valido, fallisco
if (!ModelState.IsValid)
return BadRequest(ModelState);
}
//Ritorno conferma per l'operazione
return Ok();
}
}
}
]]></script>
Battezzata la mia action come un "POST" e un nome auto-esplicativo, subito è da notare che il valore di uscita è un "Task<IHttpActionResult>": questo nel puro spirito di "asincronicità" che è richiesto ad un servizio che eroga dati. Comunque...il codice è abbastanza chiaro: faccio un paio di verifiche sul MIME del contenuto ricevuto, stabilisco il percorso della folder temporanea dove verrà scritto lo stream del file caricato in upload, quindi leggo i dati "form" dove è contenuto il mio identificativo per l'oggetto business a cui dovrò associare il file ricevuto (un generico "Document", in questo caso).
<br /><br />
E' importante che il metodo sia in grado di rispondere al client seguendo la <b>semantica HTTP</b> quindi ritornare un 404 "NotFound" se l'oggetto richiesto non esiste, un eventuale 401 "Unauthorized" se l'utente connesso al sistema non ha i diritti per compiere l'operazione, ecc. Ho volutamente "tagliato" il discorso delle permissions per non perdere il focus del post...
<br /><br />
In ultimo scorro i contenuti "FileData" del provider di gestione e recupero sia il contenuto binario del file (o dei files) contenuti nella request, sia il nome del file specificato dal client che ha inviato la richiesta stessa. Il resto è semplice: lettura del contenuto di stream e associazione ad una nuova entità "business" che conserverà il file (lo "Scan") e salvataggio della stessa su disco o sulla base dati previa validazione delle informazioni.
<br/><br/>
Una piccola nota a valle del salvataggio dell'oggetto "Scan". Il metodo "ApplicationServiceLayer.SaveScan" emette una lista di "ValidationResult" per ogni errore di salvataggio o validazione fallita sull'entità: è buona regola inviare queste validazioni al client con un codice 400 "BadRequest", aggiungendo alla response i messaggi di errore generati. Lato client li potrete facilmente intercettare e gestire (magari per mostrarli direttamente all'utente finale).
<br/><br/>
Una volta definita la action server, passiamo a quello che ci interessa di più: il client. Ma prima una rapida domanda: come gestiamo i differenti HTTP status codes (400, 401, 404, ecc) che vengono generati nel caso in cui <b>non tutto</b> andasse liscio? L'argomento è molto dibatutto, e la mia personalissima opinione è che in un ambiente come .NET, tutto quello che non va come dovrebbe andare, va tratto come un'<b>eccezione</b>.
<br/><br/>
Ma dobbiamo ricordarci anche che il client dovrebbe implementare il pattern "async/await" per non ledere la user experience dell'utente. Ho quindi implementato un'eccezione "non standard" in grado di recuperare quante più informazioni possibili dal server, in presenza di uno status code diverso da 200 "OK":
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
namespace ZenProgramming.MultipartSample.Client.Exceptions
{
/// <summary>
/// Represents an exception on Web API invoke
/// </summary>
public class WebApiException : Exception
{
/// <summary>
/// Web API response message
/// </summary>
public HttpResponseMessage Response { get; set; }
/// <summary>
/// Http status code on respons
/// </summary>
public HttpStatusCode StatusCode
{
get { return Response.StatusCode; }
}
/// <summary>
/// List of errors on response
/// </summary>
public IEnumerable<string> Errors
{
get { return Data.Values.Cast<string>().ToList(); }
}
/// <summary>
/// Message on exception
/// </summary>
public override string Message
{
get
{
//Ritorno il codice HTTP e l'elenco degli errori
return string.Format("StatusCode {0} ({1})\n{2}",
(int)StatusCode, StatusCode,
string.Join("\n", Errors));
}
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="response"></param>
public WebApiException(HttpResponseMessage response)
{
//Validazione argomenti
if (response == null) throw new ArgumentNullException("response");
//Imposto la response
Response = response;
}
}
}
]]></script>
Passiamo ora all'implementazione del client. Si, l'ho detto anche prima: ma stavolta tengo fede alla promessa...
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Net.Http;
using System.Threading.Tasks;
using ZenProgramming.MultipartSample.Client.Exceptions;
namespace ZenProgramming.MultipartSample.Client
{
public class ScansClient
{
/// <summary>
/// Push a scan file on server
/// </summary>
/// <param name="documentId">Document id</param>
/// <param name="fileName">File name</param>
/// <param name="fileContent">File content</param>
/// <returns>Returns task</returns>
public async Task PushScan(int? documentId, string fileName, byte[] fileContent)
{
//Validazione argomenti
if (documentId == null) throw new ArgumentNullException("documentId");
if (string.IsNullOrEmpty(fileName)) throw new ArgumentNullException("fileName");
if (fileContent == null) throw new ArgumentNullException("fileContent");
//Compongo il messaggio e imposto url e verb
HttpRequestMessage message = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("http://localhost:60000/api/Scans/PushScan")
};
//Creo un multi-part form data content
MultipartFormDataContent multiPartContent = new MultipartFormDataContent(
string.Format("----MyBoundary{0}", DateTime.Now.Ticks));
//Creo il contenuto per i binari, aggiungendo header e contenuto
ByteArrayContent byteArrayContent = new ByteArrayContent(fileContent);
byteArrayContent.Headers.Add("Content-Type", "application/octet-stream");
multiPartContent.Add(byteArrayContent, "File", fileName);
//Creo il valore del documento da inserire nell'header
StringContent formData = new StringContent(documentId.Value.ToString());
multiPartContent.Add(formData, "documentId");
//Imposto il contenuto del messaggio
message.Content = multiPartContent;
message.Headers.ExpectContinue = false;
//Utilizzo il client per fare la richiesta
using (HttpClient client = new HttpClient())
{
//Mando la request al server
var response = await client.SendAsync(message);
//Se il codice NON è di successo
if (!response.IsSuccessStatusCode)
{
//Recupero i dettagli in una eccezione tipizzata
var ex = GetWebApiException(response);
throw ex;
}
}
}
}
}
]]></script>
Il metodo deve essere estremamente semplice da utilizzare: bastarà istanziare la classe (che potrebbe tranquillamente essere statica), passare come parametri l'identificativo del documento a cui il file va associato, il nome del file e il suo contenuto in bytes...et voilà!
<br/><br/>
L'implementazione non è altrattanto banale: validazione dell'input (un "must" per un buon stile di programmazione), creazione dell' <b>HttpRequestMessage</b> - chiaramente con verb "POST" e indirizzo completo dell'endpoint della action che abbiamo lato server - e creazione del contenuto della request...la parte più "succosa".
<br/><br/>
Il corpo della richiesta va riempito con un <b>MultipartFormDataContent</b>: una comodissima classe .NET che permette di wrappare una serie di differenti informazioni da "impacchettare" ed inviare al server. Essa funge da scatola sia per i contenuti binari (il nostro file), sia per le informazioni di "form" (l'id del documento da associare). Basta crearne un'istanza passando una stringa di "boundary" (una sorta di separatore per gli elementi contenuti nel body della "multipart request"), quindi inserire i vari contenuti.
<br/><br/>
Il primo elemento da aggiungere al MultiPartFormDataContent è di tipo <b>ByteArrayContent</b>: servirà per contenere i dettagli del file che vogliamo inviare al server. Passando i bytes del contenuto e il nome del file, non dobbiamo dimenticare di importare il "Content-Type" nell'header del contenuto stesso: servirà al server per stabilire la "natura" dei dati che riceverà. Quindi aggiungiamo tutto l'elemento al "multipart" container specificando la stringa "File" come nome dell'elemento trasmesso.
<br/><br/>
In seconda battuta inseriamo un contenuto testuale con <b>StringContent</b>: anche in questo caso bisognerà specificare il valore dell'identificativo del documento come contenuto, delegando il "name" del contenuto stesso ("documentId") all'aggiunta di StringContent sul multipart.
<br/><br/>
Ora che il nostro body della request è pronto, è sufficiente aggiungerlo al messaggio creato in precedenza, istanziare il client HttpClient, quindi inviare il tutto lato server restando in attesa con la keyword "await". Come anticipato qualche riga sopra, la response potrebbe essere uno status code di successo (200 - OK) che determina un'operazione lato server andata completamente a buon fine, oppure uno dei codici di "fail" che abbiamo gestito nelle WebAPI.
<br/><br/>
In caso di fallimento è necessario far entrare in gioco l'eccezione custom che abbiamo realizzato prima: diamo in pasto la request al metodo <b>GetWebApiException</b>:
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Net.Http;
using System.Threading.Tasks;
using ZenProgramming.MultipartSample.Client.Exceptions;
namespace ZenProgramming.MultipartSample.Client
{
public class ScansClient
{
//...
/// <summary>
/// Get as Web API exception errors on model state
/// </summary>
/// <param name="response">Response</param>
/// <returns>Returns exception</returns>
private static WebApiException GetWebApiException(HttpResponseMessage response)
{
//Validazione argomenti
if (response == null) throw new ArgumentNullException("response");
//Leggo il contenuto nella response
var httpErrorObject = response.Content.ReadAsStringAsync().Result;
//Creo il tipo anomino che utilizzerò per il recupero degli errori nel model state
var anonymousErrorObject = new
{
message = "",
ModelState = new Dictionary<string, string[]>()
};
//Deserializzo il tutto nel modello (se ho un contenuto)
if (!string.IsNullOrEmpty(httpErrorObject))
anonymousErrorObject = JsonConvert.DeserializeAnonymousType(httpErrorObject, anonymousErrorObject);
//Creo una nuova struttura per l'eccezione
WebApiException ex = new WebApiException(response);
//A volte ci possono essere errori nel ModelState
if (anonymousErrorObject.ModelState != null)
{
//Recupero tutti gli errori presenti
var errors = anonymousErrorObject
.ModelState
.Select(kvp => string.Join(". ", kvp.Value))
.ToList();
//Scorro tutti gli errori recuperati e li wrappo in "Data"
for (int i = 0; i < errors.Count; i++)
ex.Data.Add(i, errors.ElementAt(i));
}
else
{
//Altre volte solo errori "diretti"
var error = JsonConvert.DeserializeObject<Dictionary<string, string>>(httpErrorObject);
//Scorro tutti gli elementi nel dizionario e li wrappo in "Data"
foreach (var kvp in error)
ex.Data.Add(kvp.Key, kvp.Value);
}
//Ritorno l'eccezione
return ex;
}
}
}
]]></script>
Il contenuto della response ricevuto dal server sarà deserializzato dal formato JSON (usando la meravigliosa libreria <b>Json.NET</b>), quindi saranno estratti tutti gli eventuali messaggi di errori presenti nel flusso ricevuto. L'implemetazione di WebApiException sarà in grado di gestire in maniera tipizzata le informazioni, di fatto rendendole usabili per la visualizzazione sul client.
<br /><br />
Come detto in precedenza, ritengo che uno status code non di successo debba essere gestito tramite un eccezione a livello client-side: sta quindi allo sviluppatore eseguire queste chiamate in blocchi try/catch, all'interno dei quali poter intercettare eccezioni di tipo <b>WebApiException</b>, e trattare il loro contenuto nella maniera più consona alla situazione.
<br /><br />
Spesso, anche cose semplici come l'upload di un file su un server, possono diventare complicate se non si conosce perfettamente il funzionamento del protocollo HTTP. Spero che questi appunti - perchè solo di questo si tratta - possano essere utili a qualche collega che - come il sottoscritto - ha speso inutilmente ore per un'inezia...
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-31531657150372783652015-03-12T20:46:00.001+01:002015-03-12T20:46:45.160+01:00Language Integrated Query con JavaScriptSecondo alcuni studi americani, già dal gennaio del 2014, l'utilizzo di Internet - intesa come volume di traffico - richiesto da dispositivi mobili (smartphones e tablet) ha superato la quantità richiesta da computer desktop e ambienti business (servers e quant'altro). Questo dato deve far pensare: si tratta di uno dei traguardi più significativi da quando la rete globale è entrata nella nostra quotidianità.
<br/><br/>
Una persona media fruisce la rete in maniera del tutto nuova, con una frequenza differente, durante tutto l'arco della giornata e non soltanto nelle ore lavorative. Internet ha raggiunto persone di ogni età, cultura e ceto sociale.
<br/><br/>
E' questo ha portato - e porterà sempre di più in futuro - alla necessità di realizzare strumenti più accessibili, che permettano la fruizione delle informazioni in maniera semplificata ed intuitiva. Questo, cercando di raggiungere più terminali, e quindi più persone possibili.
<br/><br/>
E' così che - da oltre quattro anni (dal lontano 2010) - tecnologie fortemente orientate al "client-side" sono diventate parte della mia vita di sviluppatore. HTML5 (nelle sue varie draft"), CSS3 e sopratutto JavaScript sono il pane quotidiano con cui è necessario confrontarsi per ottenere risultati efficaci.
<br/><br/>
Framework client side come EmberJs, KnockoutJs, ma sopratutto AngularJs (senza dubbio con maggiore potenzialità - IMHO) hanno monopolizzato lo sviluppo del "Presentation Layer", cercando di fornire una "struttura organizzata" ad un linguaggio come JS, che strutturato non è.
<br/><br/>
Ma questi framework - da soli - non sono sufficienti per coprire tutte le peculiarità di uno sviluppo orientato al client. Specialmente per uno sviluppatore come me - che viene dal mondo server - la mancanza di "facilities" di un framework robusto e completo come .NET, si sentono...eccome.
<br/><br/>
Tutto questo per dire cosa? Che ho voluto fare un esperimento, una prova, per capire se fosse possibile portare sul web uno dei miei "pezzi" preferiti di .NET. LINQ: "Language Integrated Query"...una lode a Microsoft e al suo ideatore!
<br/><br/>
E adesso mi direte: "Ma il porting di LINQ per JavaScript già esiste...". Verissimo...e in molte incarnazioni. Ma la mia premessa era quella di un esperimento, iniziato dalle fondamenta, che cerca di modellare un concetto come le <b>lambda expressions</b>, che in JavaScript, in un certo senso, esistono già dagli anni novanta.
<br/><br/>
E' così che qualche mese fa ho iniziato il mio progetto <a href="https://github.com/maurobussini/jslinq" target="_blank"><b>jslinq</b></a> (che fantasia, eh? :) ). Sono partito con "where" e "select", per arrivare a "count": e alla fine mi sono trovato per le mani una piccola libreria che mi aiuta tantissimo ogni giorno.
<br/><br/>
Il passaggio su <a href="http://www.github.com" target="_blank"><b>GitHub</b></a> è stato d'obbligo, così come la condivisione del codice sorgente. Recentemente la pubblicazione su <a href="https://www.npmjs.com" target="_blank"><b>NPM</b></a> (per metterlo a disposizione di <b>Node.js</b>) e <a href="http://bower.io" target="_blank"><b>Bower</b></a> (il package manager più diffuso per lo sviluppo client-side).
<br/><br/>
Ammettiamo di avere una struttura dati in formato JSON (con ogni probabilità scaricata da un WebApi lato server):
<script class="brush: js" type="syntaxhighlighter"><![CDATA[
var data = [
{ id: 1, name: "one", category: 'fruits', countries: ["Italy", "Austria"] },
{ id: 2, name: "two", category: 'vegetables', countries: ["Italy", "Germany"] },
{ id: 3, name: "three", category: 'vegetables', countries: ["Germany"] },
{ id: 4, name: "four", category: 'fruits', countries: ["Japan"] },
{ id: 5, name: "five", category: 'fruits', countries: ["Japan", "Italy"] }
];
]]></script>
L'idea è quella di poter fare tutte le trasformazioni, filtri, ordinamenti ("magheggi", in termine tecnico) che si può fare con una lista analoga server-side con LINQ. Dopo aver referenziato <b>jslinq</b> nella vostra pagina web - o nel modulo di Node.js - è sufficiente "dare in pasto" il nostro array di dati al costruttore, in questo modo:
<br/><br/>
<script class="brush: js" type="syntaxhighlighter"><![CDATA[
var queryObj = jslinq(data);
]]></script>
Da qui è tutto facile ed intuitivo:
<script class="brush: js" type="syntaxhighlighter"><![CDATA[
var result = queryObj
.where(function(el){
return el.name == 'two';
})
.toList();
]]></script>
Con "where" possiamo applicare un filtro sui dati, simulando di fatto la lambda expression che avremmo fatto con LINQ con una semplice "function", il cui unico parametro è l'istanza di un singolo elemento presente nell'array di dati. Si applica la condizione di filtro - in questo caso imponiamo che la proprietà "name" del singolo elemento sia uguale a "two", e si ritorna il risultato dell'operazione di confronto come uscita della "function" stessa. Facile, no?
<br/><br/>
Il sistema di <b>jslinq</b> è implementato come "chainable" ("concatenabile") e segue il medesimo principio delle "fluent" e di <b>jQuery</b>, per intenderci (che oltretutto è il medesimo di LINQ). Conseguenza di ciò il risultato della funzione "where" è a sua volta un oggetto "jslinq" che deve essere elaborato per ottenere un dato valido: questa elaborazione del valore di uscita viene fatto con il metodo "toList", che emette solo gli elementi che sono stati precedentemente filtrati dalla clausola "where".
<br/><br/>
Ammettiamo di voler ragguppare i dati originali per
<script class="brush: js" type="syntaxhighlighter"><![CDATA[
var result = queryObj
.groupBy(function(el){
return el.category;
})
.toList();
]]></script>
Otterremo una nuova struttura delle informazioni che riporterà come "key" il campo (o i campi) che è stato selezionato per il "groupBy", il conteggio degli elementi contenuti in ciascun gruppo, e la lista degli elementi opportunamente classificati. Questo il risultato della "function" applicata poco sopra:
<script class="brush: js" type="syntaxhighlighter"><![CDATA[
result = [
{ key: 'vegetables', count: 2, elements: [...] },
{ key: 'fruits', count: 3, elements: [...] },
];
]]></script>
Attualmente <b>jslinq</b> è alla versione <b>1.0.12</b> che personalmente considero stabile per un uso in ambiente di Produzione. Contiene più di una ventina di operatori, molti dei quali ricalcano il comportamento di LINQ ("count", "max", "min", "singleOrDefault", ecc...). Ma naturalmente le esigenze quotidiane hanno portato ad arricchire sempre di più la libreria di nuove funzionalità ("intersect", "substract", "remove", ecc...), che pur nella loro banalità, permettono di ridurre al massimo il codice JavaScript che è necessario scrivere per operare su array di elementi.
<br/><br/>
Quando ci si trova a lavorare rispettando alla lettera (o quasi) il principio di "Separation of Concerns", avendo accesso al workflow applicativo solo attraverso delle API che emetto dati JSON, si incorre spesso nella necessità di "raffinare" i dati sul client. Questo per alleggerire il server ed evitare di creare dei metodi API troppo complessi nel loro utilizzo, oppure costruire una moltitudine di differenti chiamate che fanno quasi la stessa cosa. E' questo il momento in cui <b>jslinq</b> può essere d'aiuto, delegando al client stesso parte di una elaborazione "minimale" che diminuisce la complessità del server, e pesa pochissimo sul client.
<br/><br/>
Bon...questo è quanto. Fateci un giro! ;)
Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com3tag:blogger.com,1999:blog-6432843868391268534.post-49893026245489786552014-12-13T22:35:00.000+01:002014-12-13T22:35:00.325+01:00Authentication vs AuthorizationAncora una volta mi trovo in mezzo ad una diatriba; se vogliamo un dilemma semantico, o semplicemente due concetti che vengono erroneamente usati in maniera interscambiabile nel comune linguaggio tecnico.
<br /><br />
Mi sto riferendo alle due parole <b>Authentication</b> e <b>Authorization</b> che da molti anni calcano la scena IT per dei sistemi più o meno complessi, web e desktop.
<br /><br />
La sicurezza in questi ultimi tempi - come mai in passato - ha assunto un ruolo non solo importante, ma <b>fondamentale</b> per ogni architettura che si rispetti; tanto, che non solo è basilare creare e nutrire il dato, ma preservarlo incontaminato, sicuro, protetto proporzionalmente al suo valore.
<br /><br />
E' così che, ogni due/tre anni nascono nuovi tecniche per difendere l'informazione, che vanno a braccetto con altrettante tecniche di "hacking" - perdonatemi il termine non propriamente preciso - per forzare o addirittura distruggere le informazioni stesse.
<br /><br />
Ma oggi non sono qui a parlare relativamente a specifiche di sicurezza informatica - ci sono colleghi di rara bravura che potrebbero facilmente eclissarmi con "la lingua legata dietro alla schiena" - ma per condividere un mio personale pensiero sull'argomento.
<br /><br />
Partiamo con il dire che "autenticazione" ed "autorizzazione" sono due termini ben distinti, ortogonali, che rappresentano due aspetti egualmente importanti del processo di security di una piattaforma applicativa. In breve:
<ul>
<li>
Authentication - Rappresenta in qualche modo "core" del processo di protezione di un dato. E' il meccanismo che permette di <b>identificare</b> un particolare utente del sistema, sulla base di una o più credenziali di accesso fornite al momento del primo contatto con la piattaforma (più precisamente quando si accedere ad una funzione o dato protetto). Esso risponde alla semplice domanda: <i>"E' possibile identificare univocamente l'operatore che sta davanti al computer? E, se si, a quale utente registrato corrisponde?"</i>.
</li>
<li>
Authorization - E' il processo di assegnazione di un permesso di esecuzione - ad un utente accreditato - necessario per portare a termine una certa azione nel contesto della piattaforma applicativa in questione. La domanda in questo caso è altrettanto banale: <i>"Che cosa può fare, nel contesto applicativo, l'utente XYZ che ho riconosciuto durante il processo di autenticazione, sui dati e sulle strutture del sistema?"</i>
</li>
</ul>
Come detto i due concetti sono nettamente separati nel loro significato; ma è evidente che il processo di autorizzazione non può in nessun modo prescindere da quello di autenticazione; come potrei altrimenti assegnare un diritto a qualcuno di cui non conosco l'identità?
<br /><br />
L'identificazione, solitamente, si svolge attraverso l'uso delle tre classiche metodologie ampiamente descritte in letteratura, e si basa sempre su un <b>input proveniente dall'operatore</b> stesso che chiede di essere riconosciuto:
<ul>
<li>
Un input sulla base di <b>qualche cosa che l'utente <u>sa</u></b>; per esempio la classica username e password, un codice PIN, o qualche cosa di più "esotico".
</li>
<li>
Un input sulla base di <b>qualche cosa che l'utente <u>ha</u></b>; per esempio un token, una smartcard, oppure una chiave hardware di qualche genere.
</li>
<li>
Un input sulla base di <b>qualche cosa che l'utente <u>è</u></b>; per esempio utilizzando dei sensori biometrici come un lettore di impronte digitali o - se vogliamo fare una figata - un sistema di scansione retinea. E magari fermiamoci qui perchè rischiamo di cadere nella fantascienza...
</li>
</ul>
Il motivo per cui questo due termini sono sempre finiti nello stesso calderone, è dovuto al fatto che <b>insieme</b> rappresentano il "core" del layer di security di un sistema informativo. E, spesso e volentieri, sono sempre stati architetturalmente "annegati" nel sistema stesso che ne andava a beneficiare.
<br /><br />
Riagganciandomi al mio <a href="http://zenprogramming-it.blogspot.it/2014/09/progettazione-da-un-approccio.html" target="_blank">precedente post</a>, mi sembra doveroso segnalare come, sempre di più, si senta la necessità di disaccoppiare il layer di sicurezza - in architetture service-oriented - dal resto dell'applicazione; questo in concomitanza con il proliferare di sistemi ed applicazioni, ciascuno dei quali avrebbe una propria banca dati utenti, eterogenea e difficilmente manutenibile. Inoltre, dal punto di vista della persona fisica, l'operatore, il numero di username/password che si è costretti a ricordare diventa sempre maggiore con generale disagio e difficoltà di utilizzo.
<br /><br />
Pensate al trattamento che i grandi competitor come Google, Microsoft (per citare solo quelli che conosco meglio) riservano allo strato identificativo, cioè l'autenticazione. Una identità "Google" è centralizzata, è fornita come servizio a se stante, e sempre (<b>sempre</b>) è isolata dal sistema sulla quale sarà fruita. In parole povere il concetto è questo: "Utente, forniscimi le tue credenziali di accesso Google, ed io - servizio di autenticazione - provvedo al tuo riconoscimento e preparo il terreno per le altre applicazioni presenti nel mio ecosistema che vorrai utilizzare". In pratica i sistemi <b>federati</b> si dovranno "fidare" di una entità superpartes (il servizio di "Auth") per evitare di implementare i meccanismi di "identity" all'interno di ciascun differente applicativo.
<br /><br />
Ma attenzione: qui stiamo parlando di <b>identificazione</b>, quindi dell'autenticazione di un particolare utente; non siamo minimamente accennando al processo di <b>autorizzazione</b> all'uso di una particolare risorsa o funzionalità.
<br /><br />
Scorporare e, di fatto, centralizzare il processo di autorizzazione sarebbe non poco dispendioso, e necessiterebbe di un livello di <b>astrazione</b> non sempre semplice da raggiungere. Pensate di dover generalizzare i permessi di lettura, scrittura ed eliminazione da una anagrafica di prodotti per il software ABC, centralizzando il tutto sul server di autorizzazione; sopratutto considerando che quest'ultimo è assolutamente agnostico al processo funzionale svolto dal sistema esterno. Si rischierebbe facilmente di portare della logica di business di ABC all'interno del sistema di "identity": e non è certo quello lo scopo che vogliamo raggiungere.
<br /><br />
Personalmente - e chiaramente si può essere d'accordo o meno con questa affermazione - penso che l'autenticazione vada trattata a livello globale, centralizzata ed accessibile dal sistema di "Auth" attraverso dei servizi web (ove possibile, chiaramente). Ma l'autorizzazione è bene che venga implementata all'interno dell'applicazione stessa che farà uso dei permessi assegnati all'utente in questione; certo, fidandosi di "Auth" per quanto riguarda il riconoscimento dell'operatore in questione, ma preservando all'interno del proprio database (per farla semplice) il concetto di "profilo utente". Quest'ultimo accoppierà l'identità digitale con le <b>grant</b> assegnate secondo una logica funzionale ben definita e nota nel contesto d'uso.
<br /><br />
Ultimo "tip", prima di chiudere. Da qualche mese mi sto divertendo (per così dire) con il protocollo "OAuth2" e le sue incarnazioni .NET. Il framework <b>OWIN</b> (Open Web Interface for .NET) mi sembra estremamente versatile da questo punto di vista; permette di interfacciare provider esterni con estrema semplicità (Google, Twitter, Facebook, ecc), realizzare le basi per i propri server di autenticazione, lavorare con chimate CORS (Cross-Origin Resource Sharing), e approcciare un processo identificativo basato su token (Bearer, JWT) che viaggiano nell'header delle request.
<br /><br />
L'integrazione con framework client-side (AngularJs, di cui sono un grandissimo fan) è tutto sommato abbordabile, e permette di realizzare sistemi moderni, flessibili e manutenibili nel corso del tempo; l'integrazione in piattaforme mobile è anch'essa gestibile senza un numero eccessivo di imprecazioni "developer-side".
<br /><br />
Ma la chiave resta sempre il <b>disaccoppiamento</b>. E ancora una volta mi domando: <i>"Per quale motivo, in ogni singola applicazione che realizzo, devo incorporare (per copia o riferimento, of course) un framework di gestione degli utenti, quando posso delegare questa noiosa e delicata parte ad un sistema esterno la cui unica responsabilità è certificarmi l'identità di un operatore?"</i>.
<br /><br />
La risposta è che non ho motivo di farlo. O meglio...non mi viene in mente nessun motivo "sano"...Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-53703555935901801742014-09-27T14:47:00.000+02:002014-11-05T21:02:16.442+01:00Progettazione: da un approccio tradizionale ad uno più modulareOrmai è parecchio che non bloggo più: vuoi per scadenze importanti e molto (troppo) ravvicinate, vuoi per necessità di dedicare più tempo alla famiglia. Resta il fatto: sono "assente" da qualche mese, ma non sono stato certo con le mani in mano ;)...
<br /><br />
Da parecchio tempo sto cercando di dare un'impronta differente ai progetti a cui lavoro. Che siano di piccole dimensioni oppure più corposi, la necessità è quella di avere sistemi che siano facilmente manutenibili, efficienti e il più possibile flessibili; senza però impattare sulle prime due caratteristiche desiderate.
<br /><br />
Il "mondo a servizi" disaccoppia sempre di più la user interface dagli strati applicativi dedicati alla persistenza e all'elaborazione. La tendenza, anche dei grandi competitors presenti sul mercato, è quella di realizzare piattaforme "internet-based" che siano autoconsistenti ed agnostiche; in pratica capaci di erogare un servizio e una funzione, indipendentemente da quelli che saranno i device e gli ambienti operativi sui quali il servizio stesso sarà fruito. E' così che le varie Google API e Microsoft Azure Services (solo per citarne alcuni) premono sempre più in questa direzione: "Io ti fornisco un servizio che fa questa cosa, ma alla presentazione e all'integrazione ci devi pensare tu, sviluppatore".
<br /><br />
Personalmente lo considero un vantaggio, perchè l'interfaccia utente che va bene per tutti i possibili utilizzatori e tutti i possibili terminali, è mera un'utopia. Ci sarà sempre il "rompimaroni" (passatemi il termine tecnico) che vorrà avere qualche cosa di diverso, a cui non interesserà quella funzione, oppure che vorrà dare maggior risalto all'altra solo perchè la utilizza più di frequente. In un mondo dove l'utente è messo al primo posto, il disaccoppiamento e la modularità devono essere un "must", mettendo definitivamente da parte le applicazioni monolitiche a favore di una miriade di micro applicazioni, e micro-servizi, in grado di parlare tra loro per raggiungere l'obiettivo comune.
<br /><br />
Dal punto di vista di un software architect - mi piace pensare di esserlo, anche se sono a tutti gli effetti uno sviluppatore, a cui piace mettere le mani in pasta - la modularità è cosa buona e giusta. Che permette in primis non solo un incremento notevole della manutenibilità dell'intero apparato, ma anche la possibilità di "svecchiare" in maniera periodica uno o più componenti che arrancano, o che magari semplicemente necessitano di una serie di miglioramenti. Aggiungere nuove funzionalità è poi infinitamente più semplice, perchè l'architettura è pensata per essere in continua espansione, e i punti di ingresso per il flusso applicativo sono una moltitudine.
<br /><br />
Ed è così che mi sono trovato di fronte ad un'applicazione, anche piuttosto semplice, che fino a qualche anno fa avrei organizzato - secondo la tradizione - con un database più relativo OR/M, alcune classi per la gestione del flusso applicativo, e la parte di presentazione con il classico ASP.NET WebForms. Ho tentato di approcciarla in maniera differente, stravolgendo quelli che sono i concetti che mi hanno guidato negli ultimi 10 anni.
<br /><br />
Non sono libero di divulgare i contenuti funzionali del sistema - c'è la pena di morte, mi dicono - ma posso inventarmi uno scenario che sia il più analogo possibile dal punto di vista tecnico. Ammettiamo di avere un portale di e-commerce, con una serie di prodotti e loro caratteristiche dettagliate; naturalmente c'è la possibilità di fare acquisti dietro verifica della giacenza in magazzino, e visualizzazione di una eventuale data di nuova disponibilità del prodotto nel caso un cui l'elemento fosse esaurito. Il modulo "magazzino" non è per niente semplice: gestisce ingresso ed uscita merce, ordini dei fornitori e dei clienti...e tutte quelle belle cosine di cui non sto a parlarvi - anche perchè non le conosco, visto che è solo un esempio ;).
<br /><br />
A fronte di una semplice ricerca, penso possiare immaginare la schermata dei risultati che dovrebbe apparire all'utente del portale: una banale lista di prodotti - "of course" corrispondenti ai criteri di ricerca - accando a ciascuno dei quali è riportato il pulsantino "Compra", oppure la suddetta data di prossima disponibilità per giacenza a magazzino uguale a zero. Bene, partiamo da qui...
<br /><br />
Un approccio tradizionale - la faccio semplice - prevederebbe una query in "join" tra la tabella dei prodotti e quelle coinvolte nel modulo magazzino (ordini, giacenze, ecc). La complessità della query di estrazione chiaramente dipende dalla complessità del relativo modulo "owner" dei dati (il magazzino stesso). Questa dipendenza - a tutti gli effetti - "lega" il portale di e-commerce a quest'ultimo, impendendo quindi il concetto di modulatità e "separation of concerns" che abbiamo auspicato. Inoltre (cosa non di poco conto) lo sforzo elaboravo della base dati per estrarre il semplice dato di "quantità disponibile" è considerevole ed impatta negativamente sul tempo che trascorre tra la "request" dell'utente (submit dei criteri di ricerca prodotti) e la response del server (visualizzazione dei risultati).
<br /><br />
Ora mi domando: è veramente necessario che ogni volta che faccio un ricerca di prodotti, le informazioni siano calcolate in tempo reale? Se applico due volte lo stesso criterio di ricerca, ottenendo gli stessi risultati, è giusto che il database sia stressato due volte, andando ad impattare sulla "user experience" che viene percepita dall'utente? Il processo è ottimizzato in tutto e per tutto? E' scalabile? Per tutte queste domande, la risposta che mi do - ed è solo la mia opinione personale - è no.
<br /><br />
Passiamo ai dettagli tecnici, dicendo subito che l'OR/M c'è ancora: non sono tanto pazzo nel 2014 da mettere ancora le mani nelle query SQL "raw", se non è strettamente necessario. Ma il disegno della base di dati, a fronte delle precedenti considerazioni non è lo stesso che avrei "buttato giù" seguendo il modello tradizionale. Come molti sviluppatori, sono cresciuto con il concetto di schema di dati "normalizzato", cioè ottimizzato per fare in modo che la stessa informazione non fosse presente in due diverse locazioni del sistema di persistenza. Questo concetto - giustissimo nella maggior parte delle situazioni - talvolta va a cozzare con quella che è la caratteristica principare desiderata dai sistemi moderni: la scalabilità e la reattività.
<br /><br />
Tornando alla pagina di risultati citata poco fa, perchè devo attendere - sparo un numero casaccio - 350 ms per calcolare le giacenze di magazzino di una decina di prodotti che corrispondono ai criteri di ricerca, quando posso trovare un modo per erogare lo stesso dato in 35 ms?
<br /><br />
Il tutto si ottiene con il concetto di "dato stale" (letteralmente tradotto, sarebbe "dato in stallo"). Si tratta di una pratica molto disprezzata in passato, che si sta rivalutando parecchio negli ultimi tempi nel campo dei "BigData" e, in generale, in tutti i settore in cui la mole di dati è talmente consistente da non permettere di poter essere processata in real-time, soprattutto da un singolo processo applicativo. Si basa su un concetto estremamente semplice, una domanda, fondamentalmente: è necessario che questo particolare dato sia aggiornato esattamente nell'istante in cui si va a consumare?
<br /><br />
Nel nostro caso, possiamo girare la domanda: quando il valore di disponibilità/indisponibilità di un prodotto varia? La risposta è ancora una volta banale: varia quando viene venduto un prodotto, oppure quando si registra una nuova entrata merce nel modulo magazzino. Per quel particolare articolo è necessario ricalcolare il numero di elementi disponibili; e ancora, se questa quantità calcolata è zero, deve essere interrogata un'altra area del "magazzino" per conoscere la data di disponibilità sulla base delle previsioni di consegna della nuova merce.
<br /><br />
E' semplice capire come questi due dati (la quantità e l'eventuale data di prossima disponibilità), rappresentano una sorta di "ridondanza" di informazione all'interno del sistema; la loro nuova collocazione sarà in due colonne aggiuntive sulla tabella dei prodotti, diventando di fatto parte dell'informazione atomica del record di prodotto stesso. Questo trasforma, di fatto, un database "normalizzato" in uno "non normalizzato", proprio perchè il medesimo dato è ottenibile eseguendo due differenti processi estrattivi.
<br /><br />
Con questa modifica della struttura del nostro database, è possibile realizzare una estrazione dei risultati di ricerca con una semplice query sulla tabella dei prodotti, senza interrogare un'altra decina di tabelle applicative per ottenere la stessa informazione. L'accortezza che deve essere intrapresa, naturalmente, è assicurarsi che venga in messo in moto il processo "update" dell'informazione ridondata allo scatenarsi degli eventi descritti in precedenza.
<br /><br />
Un esempio molto noto di "dato stale" è presente su Twitter. Quando inviate una richiesta di "follow" di un particolare utente, scatenate un evento asincrono della piattaforma applicativa che innesca la rivalutazione, sia del vostro profilo (al quale dovrà essere applicato un incremento del contatore di persone che seguite), sia del profilo della persona destinataria del "follow" (al quale, per simmetria, dovrà essere incrementato il contatore dei followers). L'aggiornamento dei due contatori non nè immediato, e certamente non è avviato come processo sincrono dal servizio API di Twitter nel momento in cui cliccate sul pulsante presente sull'interfaccia utente della vostra app. E se così fosse, immaginatevi che a quale immenso sforzo computazionale sarebbero sottoposti i web server di Twitter per insistere sui layer applicativi, e ancora sulle basi di dati sottostanti (anche se queste ultime, chiaramente, non hanno un database engine relazionale, ma qualche cosa di più adatto allo scopo).
<br /><br />
Ritornando al nostro esempio del sito di e-commerce è necessario prendere in considerazione questa possibile pericolosa situazione. L'utente "A" apre il suo browser, esegue una ricerca dei prodotti, e visualizza a video i risultati (comprensivi delle nuove informazioni di disponibilità lette direttamente dalla tabella di anagrafica). Un secondo utente "B", esegue la medesima ricerca e clicca sul pulsante "Compra" per uno dei prodotti con disponibilità 1 elemento. L'utente "A" (la fortuna è cieca, ma la sfiga ci vede benissimo) mette nel carrello lo stesso prodotto che però non ha disponibilità nonostante il dato di giacenza che ha visualizzato riportasse 1 unità a magazzino. E' chiaro che solo un meccanismo di compensazione è in grado di risolvere la questione, magari notificando al secondo utente che il prodotto non è più disponibile oppure che il suo ordine è stato annullato.
<br /><br />
Diciamo subito che il concetto di "stale data", cioè dato non aggiornato, potrebbe portare a situazione applicative non previste; e in generale non è una pratica che possibile mettere in atto in ogni situazione, se non intervenendo in maniera "compensativa", e prendendo in considerazione tutti gli scenari che ne possono derivare. Ma in certe situazioni è meglio avere un dato che non rappresenta la situazione reale nel momento della richiesta, piuttosto che essere necessari ad attendere l'elaborazione del dato stesso per poter fruire l'intero contenuto. In generale, in una architettura a servizi, avere un componente che risponde ad una richiesta esterna in un periodo di tempo non accettabile, rappresenta un enorme collo di bottiglia che impatta sull'intero apparato e su come il sistema viene percepito dall'utente finale.
<br /><br />
Finisco quindi ricollegandomi alla premessa di questo post. Un mondo a servizi, dove le architetture sono modulari e ciascuna entità vive di vita propria collaborando con le altre, porta a pensare in maniera molto diversa le soluzioni che studiamo; libere da schemi, certezze che ci hanno guidato per anni, ma dando più spazio all'immaginazione e agli obiettivi: "l'utente al primo posto", se vogliamo ridurre il tutto ad una sola frase.
<br /><br />
Insomma...pensate in maniera differente (cit.) ;)
<br />
M.
Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-41291232404193952822014-03-22T13:34:00.000+01:002014-03-22T13:34:00.603+01:00Throttling monitor con ASP.NET MVCIl "throttling" spiegato in due parole? Capacità di un sistema informativo di tracciare l'occorrenza delle chiamate ad un particolare risorsa (o una funzione) disponibile nel sistema stesso; identificando il chiamante ed, eventualmente, imponendo delle limitazione all'utilizzo della risorsa in funzione di alcune policy prestabilite.
<br />
<br />
A cosa serve e come è possibile sfruttarlo? Il campo di applicabilità è certamente molto vasto, ma è una tecnica maggiormente utilizzata per la sicurezza dei sistemi, e la misurazione della responsività degli stessi a fronte di un elevato numero di richieste. Per esempio è possibile monitorare il numero di invocazioni ad un particolare metodo esposto da un vostro servizio web, e la frequenza con cui avvengono. Questo, per prevenire attacchi esterni, provenienti da malintenzionati che vogliono mettere in crisi il servizio che offrite.
<br />
<br />
Ultimamente mi è capitato di dover realizzare una serie di servizi - esposti pubblicamente su rete internet - che fossero fruiti da una discreta quantità di client remoti. La difficoltà di implementazione non era particolarmente elevata (si è trattato di erogare alcune liste di informazioni, o poco più), ma la frequenza con cui sarebbe stato utilizzato - devo ammetterlo - ha destato diverse perplessità.
<br />
<br />
Queste (tutte perfettamente motivate, ve lo garantisco) stavano nel fatto che un eccessivo sfruttamento delle risorse avrebbe potuto rendere il sistema instabile, confluendo nei casi più gravi anche in una indisponibilità del servizio per l'utente finale.
<br />
<br />
Prima di mettere in campo il classico "cannone per sparare alle mosche", ho preferito accertarmi che le preoccupazioni sollevate (ripeto, giustamente) dal cliente, fossero giustificate. Ho quindi realizzato un rudimentale sistema di monitoraggio sfruttando gli ActionFilter di ASP.NET MVC.
<br />
<br />
L'idea è semplice. Ho una serie di controller e una serie di action. Usando un action filter posso identificare il client chiamante, quindi verificare quante volte (nell'arco di un periodo temporale prestabilito) questo client eseguiva la medesima chiamata al servizio.
<br />
<br />
Il tracciamento confluisce in una banalissima "in-memory" cache che conserva una mappatura tra il client IP che ha eseguito l'invocazione, e il numero di invocazioni nei vari "intervalli temporali" che ho stabilito in fase di design. E avere la possibilità di verificare (in ogni istante) se un particolare IP client aveva invocato un certo numero di volte il nostro server nell'ultimo secondo (o minuto, ora, giorno, etc.), vi assicuro che è un'informazione degna di nota.
<br />
<br />
L'aggiunta di una funzionalità di "negazione del servizio" a fronte di un numero elevato di richieste da parte di un unico client, è la ciliegina sulla torta che ci permette di far fronte ad una consistente percentuale di attacchi mirati al "denial of service", usando solo pochissime righe di codice.
<br />
<br />
Partiamo dall'utilizzo del nostro filtro di throttling:
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System.Web.Mvc;
using ZenProgramming.Chakra.Mvc.Filters.Throttling;
using ZenProgramming.Chakra.Mvc.Filters.Throttling.Enums;
namespace ThrottlingSample.Controllers
{
[ThrottleDetector]
[ThrottlePolicy(ThrottleScope.SingleClient, ThrottlePeriod.Second, 2)]
[ThrottlePolicy(ThrottleScope.SingleClient, ThrottlePeriod.Minute, 10)]
[ThrottlePolicy(ThrottleScope.Global, ThrottlePeriod.Day, 1000)]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
[ThrottlePolicy(ThrottleScope.Global, ThrottlePeriod.Day, 10)]
public ActionResult Backup()
{
return new EmptyResult();
}
}
}
]]></script>
L'ActionFilter che compie il tracciamento e il conteggio delle richieste è "ThrottleDetectorAttribute"; esso, come la maggior parte degli action filter, è applicabile sia sul controller che sull'action di riferimento. L'incremento dei contatori sarà comunque eseguito per ogni singola action, indipendentemente dai parametri passati alla stessa (se volete qualche cosa di più specifico, è comunque possibile eseguire una customizzazione del comportamento).
<br />
<br />
Mentre "detector" può essere applicato una sola volta, "ThrottlePolicyAttribute" è applicabile in maniera multipla a seconda delle esigenze funzionali. Il suo significato è auto-esplicativo: genera e registra una policy di restrizione sulla risorsa (action) sulla quale è applicato. Nell'esempio, limita l'uso di tutte le action presenti nel nostro controller "HomeController" ad un massimo di 2 richieste al secondo, 10 richieste al minuto e 1000 richieste al giorno (per ciascuna differente action).
<br />
<br />
L'enumerazione specificata "ThrottleScope" discrimina se applicare una politica globale ("ThrottleScope.Global", dove il numero di richieste tollerate dalla policy è conteggiato come somma del totale delle richieste provenienti da diversi client), oppure specifica ("ThrottleScope.SingleClient", con il numero di limite di tolleranza specifico per ogni singolo differente indirizzo IP client richiedente).
<br />
<br />
Ma cosa succede quando una delle policy viene violata? Semplice: il servizio viene negato, e sul client viene ritornato un messaggio di errore di "throttling policy violata", con codice HTTP 503 ("Service Unavailable").
<br />
<br />
E' possibile eseguire un override sia del metodo di rilevamento di violazione della policy, sia dell'incremento delle richieste in ingresso: tracciare ogni singola interazione può risultare fondamentale per ottenere un monitoraggio (magari grafico) riguardo l'utilizzo del vostro servizio.
<br />
<br />
Gli action filter descritti sono disponibili nella nuova versione di <a href="http://www.nuget.org/packages/Chakra.Mvc">Chakra.Mvc</a>, scaricabile direttamente da <a href="http://www.nuget.org/packages/Chakra.Mvc">NuGet</a>.
<br />
Fateci un giro... ;)
<br />
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-43090350988048537402014-02-08T11:33:00.000+01:002014-02-08T11:33:00.558+01:00ASP.NET: WebForms vs MVCQuanto Microsoft, nel lontano 2009, ha rilasciato ASP.NET MVC, mi sono chiesto il motivo per cui è stato
scelto di mettere a disposizione un framework web che affiancasse ASP.NET WebForms. Ad una prima occhiata
non sono riuscito a vedere i vantaggi della nuova tecnologia, rispetto ad un'altra, solida, ben collaudata
e diffusa come WebForms.
<br />
<br />
E' da sottolineare che il grande merito di quest'ultima, all'inizio del nuovo millennio, è stato avvicinare
una massa di sviluppatori desktop (la maggior parte dei quali legati al buon vecchio Visual Basic 6.0), al mondo web.
Il paradigma di programmazione che veniva proposto - nella sua semplicità concettuale - era geniale: simulare
il modello "ad eventi" (tipico dei desktop, appunto) applicato al web.
<br />
<br />
La magia realizzata da "postback" e "viewstate" ha quindi permesso, solo con l'introduzione di qualche nuovo
concetto, di aumentare esponenzialmente il numero di web-developers Microsoft, quindi iniettare nuova linfa
produttiva nelle vene di <b>Internet</b>, che proprio in quegli anni ha conosciuto il suo grande boom.
<br />
<br />
Nell'immaginario collettivo, WebForms, è sempre stato sinonimo di grande <b>produttività</b>, con tempi di sviluppo ridotti,
e buona qualità del prodotto finale; anche per sviluppatori con una conoscenza non approfondita delle problematiche
legate al web.
<br />
<br />
Per quasi un decennio, WebForms, ha rappresentato l'unico modo di fare "web" su .NET. Ma non tanto una "scelta obbligata":
semplicemente perchè non si è mai effettivamente sentita la necessità di cercare altre vie, poichè le esigenze
degli utenti non sono cambiate così radicalmente da giustificare una vera rivoluzione. Con i vari framework e
service packs, sono state rilasciate implementazioni che hanno sempre più semplificato la vita dello sviluppatore
migliorando la qualità del prodotto finale; ma le basi su cui costruire tutta l'intelaiatura è rimasta sostanzialmente la stessa.
<br />
<br />
Ma negli ultimi 4-5 anni, con la diffusione di Internet in mobilità, e l'introduzione sul mercato di numerosi device
caratterizzati da specifiche eterogenee, hanno iniziato a sentirsi sempre più forti i temi del "controllo"
sull'output generato, quello della "ottimizzazione" dei dati sul canale e, non ultimo, quello della sicurezza sui dati.
<br />
<br />
Benchè "WebForms", nelle sue ultime versioni, rispondesse efficacemente a queste nuove problematiche - e vi risponde
tutt'ora - è evidente che la produttività, che rappresentava il punto di forza di questo framework, tende a venire
un po' se si da molta importanza a queste problematiche. Le "best practices" da mettere in atto per ottimizzare il
dialogo client-server e migliorare l'output di uscita, sono molteplici; e molte di esse richiedono delle conoscenze
approfondite sia del protocollo HTTP, che di WebForms stesso.
<br />
<br />
Altra importantissimo aspetto che non va sottovalutato è come la rete sia diventata sempre più "orientata ai servizi" e
meno alle applicazioni "chiavi in mano". Molti device fruiscono Internet utilizzando applicazioni native, che sfruttano la rete
con il solo scopo di attingere dati nudi e crudi (in formato JSON o XML), demandando la parte di interazione utente e
"presentation" a dei moduli locali (le "App").
<br />
<br />
Da questo punto di vista, a mio parere, ASP.NET MVC ha qualche cosa in più. Diversamente da WebForms, che tende a simulare
un ambiente <b>state-full</b> usando viewstate e postback, l'incarnazione Microsoft di Model-View-Controller abbraccia
completamente la natura <b>state-less</b> del protocollo HTTP, e sfrutta le specifiche sulle quali è stato concepito
fino all'ultima risorsa.
<br />
<br />
Si ha un controllo completo sull'output emesso per la renderizzazione client (diversamente da WebForms, che viene "infarcito"
di strutture atte a preservare lo stato della pagina). E' possibile gestire granularmente l'accesso alla singola risorsa
("Action") verificando i parametri in ingresso nella "request", e fornendo un'adeguata "response". Non ultimo, la natura
modulare con cui è stato concepito ASP.NET MVC, va nell'ottica di realizzare applicazioni altamente
testabili e maggiormente manutenibili. Ogni singola porzione applicativa (detta anche "unità") può beneficiare della
filosofia con cui il sistema è stato concepito il sistema, il "SoC" (Separation of Concerns), e una struttura organizzativa
di progetto che rende semplice intervenire chirurgicamente sul sistema anche in fase di maintenance.
<br />
<br />
Va comunque detto che MVC, usato in maniera del tutto "nativa", è piuttosto ostico da apprendere ed applicare; ma usandolo
in combinazione con dei framework client side (cito solo "KnockoutJs", "DurandalJs" o "AngularJs") rappresenta una vera
macchina da guerra. Applicazioni prima immaginabili solo in ambiente desktop, sono realtà pubblicate sulla rete; ed MVC è
il perfetto backend per incorporare tutta la potenza "server-side" di questi applicativi.
<br />
<br />
Chiaramente il tema "WebForms vs MVC" è tutt'ora aperto e caldo. Alcuni di voi saranno d'accordo con il mio
pensiero, altri meno. In un modo o nell'altro si arriva sempre alla classica "guerra di religione" che porta spesso come
argomentazioni dialettiche, le personali preferenze per una o l'altra tecnologia. Ma quello che conta veramente è il risultato: come lo si
raggiunge ha poca importanza. Ne ha molta invece la qualità e la manutenibilità del sistema.
<br />
Per il resto...a voi l'ardua sentenza...
<br />
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-14418592574004642002014-01-25T19:01:00.000+01:002014-02-05T21:41:32.594+01:00ActionFilter: tracciamento di request e response di un metodo MVCSviluppando una SPA (Single Page Application) ci si trova spesso a doversi confrontare con problemi di serializzazione e deserializzazione
di dati in formato JSON (Javascript Object Notation), da e verso il server remoto. La tecnologia che ho scelto per il mio server-side
backend è ASP.NET MVC: a mio parare la scelta più naturale e sensata per uno sviluppatore che ha un forte background .NET.
<br />
<br />
In fase di sviluppo, una delle attività che maggiormente fanno perdere tempo, è proprio comprendere perchè questo o quel metodo
non risponde correttamente: errore nel passaggio dei parametri, problemi di formattazione, variabili e fields passati con
la denominazione errata, etc. E la problematica si ingigantisce quando ci si trova a sviluppare il backend per dei colleghi che si occuperanno
solo della parte client dell'applicativo: due mondi completamente isolati, dove la firma dei metodi server rappresenta l'unico "contratto" a
garanzia delle due parti.
<br />
<br />
Ma i contratti e gli schemi, in una architettura RESTful, sono equiparabili a carta straccia; è per questo motivo che, dopo numerose ore
spese ad analizzare (manualmente) le chieste client in ingresso nella mia piattaforma di servizi, mi sono deciso ad
intraprende una strada che mi garantisse un tracciamento delle "request" e "response" anche in modalità non interattiva.
<br />
<br />
Scrivere un pezzo di codice in grado di innestarsi nella pipeline di ASP.NET MVC, non è poi così difficile grazie alla potenza degli
"ActionFilter". Tantopiù che, nel framework Microsoft, questi ultimi sono stati concepiti con filosofia "AOP" (Aspect Oriented Programming) tali da
poter essere applicati come semplici marcatori sopra i controller (o sulle action) sulle quali il comportamento è desiderato.
<br />
<br />
Partiamo con la definizione di una nuova classe, che con poca fantasia chiamiamo "TraceActionAttribute", derivante direttamente dal
tipo base "ActionFilterAttribute". E' importante notare che l'infrastruttura della classe base ci permette di intercettare quattro
diversi "eventi" (tramite override di metodi "virtual") che si verificano con la richiesta di una particolare action:
<br />
<ul>
<li><b>OnActionExecuting</b> : sollevato <u>prima</u> dell'invocazione di una action su un controller</li>
<li><b>OnResultExecuting</b> : sollevato <u>prima</u> della generazione di un risultato di una action</li>
<li><b>OnResultExecuted</b> : sollevato <u>dopo</u> la generazione di un risultato di una action</li>
<li><b>OnActionExecuted</b> : sollevato <u>dopo</u> l'esecuzione completa della action</li>
</ul>
<br />
In questa particolare implementazione ho scelto eseguire l'override dei due soli metodi di elaborazione della action: "OnActionExecuting"
per tracciare la request proveniente dal client remoto, e "OnActionExecuted" per la response, analizzando l'emissione del risultato
al chiamante. Inoltre, poichè capita spesso che un particolare client richieda autorizzazione (Windows, Forms, Basic, etc.), ho ritenuto
comodo ottenere anche questi dati per tracciare una panoramica più completa ed esaustiva.
<br />
<br />
Su "OnActionExecuting", usando il contesto di esecuzione del filtro, recupero le informazioni relative al controller ed action invocate,
le credenziali dell'utente (chiaramente se una autenticazione è stata specificata) e gli eventuali parametri della action stessa; il
tutto viene memorizzato in proprietà "protected" globali al filtro per poter comodamente essere consumate anche in seguito.
<br />
<br />
Su "OnActionExecuted" invece mi sono concentrato sul recupero del risultato di elaborazione della action. Se si dovesse trattare di un
"ActionResult" di tipo "Json" (type "JsonResult"), eseguiremo un tracciamento più dettagliato, serializzando i valori tramite i preziosi
servigi nella fantastica libreria "Json.Net".
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Web;
using System.Web.Mvc;
using Newtonsoft.Json;
namespace ZenProgramming.Chakra.Mvc.Filters
{
/// <summary>
/// Trace authentication, parameters and result of a request
/// </summary>
public class TraceActionAttribute : ActionFilterAttribute
{
#region Public properties
/// <summary>
/// Enable request trace
/// </summary>
public bool EnableRequestTrace { get; set; }
/// <summary>
/// Enable response trace
/// </summary>
public bool EnableResponseTrace { get; set; }
#endregion
#region Protected properties
/// <summary>
/// Is user authenticated on action
/// </summary>
protected bool IsAuthenticated { get; private set; }
/// <summary>
/// Type of authentication
/// </summary>
protected string AuthenticationType { get; private set; }
/// <summary>
/// Authenticated username
/// </summary>
protected string AutheticatedUsername { get; private set; }
/// <summary>
/// Http verb
/// </summary>
protected string Verb { get; private set; }
/// <summary>
/// Controller name
/// </summary>
protected string ControllerName { get; private set; }
/// <summary>
/// Action name
/// </summary>
protected string ActionName { get; private set; }
/// <summary>
/// Dictionary of parameters
/// </summary>
protected IDictionary<string, object> Parameters { get; private set; }
/// <summary>
/// Result of action
/// </summary>
protected ActionResult Result { get; private set; }
/// <summary>
/// Raised exception
/// </summary>
protected Exception Exception { get; private set; }
#endregion
/// <summary>
/// Constructor
/// </summary>
public TraceActionAttribute()
{
//Attivo tutti i trace di default
EnableRequestTrace = true;
EnableResponseTrace = true;
}
/// <summary>
/// Execute before action is invoked
/// </summary>
/// <param name="filterContext">Filter context</param>
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//Validazione argomenti
if (filterContext == null) throw new ArgumentNullException("filterContext");
//Richiamo la funzione base
base.OnActionExecuting(filterContext);
//Imposto l'autenticazione ricevuta
IsAuthenticated = HttpContext.Current.User.Identity.IsAuthenticated;
AuthenticationType = HttpContext.Current.User.Identity.AuthenticationType;
AutheticatedUsername = HttpContext.Current.User.Identity.Name;
//Imposto le informazioni della request
Verb = filterContext.HttpContext.Request.HttpMethod;
ControllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
ActionName = filterContext.ActionDescriptor.ActionName;
Parameters = filterContext.ActionParameters;
//Traccio la request (se richiesto)
if (EnableRequestTrace)
TraceRequest(GetAuthenticationTrace(), GetRequestTrace());
}
/// <summary>
/// Executed when action is completed
/// </summary>
/// <param name="filterContext">Filter context</param>
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
//Validazione argomenti
if (filterContext == null) throw new ArgumentNullException("filterContext");
//Valorizzo il risultato
Exception = filterContext.Exception;
Result = filterContext.Result;
//Traccio il response (se richiesto)
if (EnableResponseTrace)
TraceResponse(GetResultTrace());
//Richiamo la funzione base
base.OnActionExecuted(filterContext);
}
/// <summary>
/// Get string trace of request
/// </summary>
/// <returns>Returns string</returns>
protected string GetRequestTrace()
{
//Eseguo la serializzazione dei parametri
string jsonParam = JsonConvert.SerializeObject(Parameters, new JsonSerializerSettings
{
DateFormatHandling = DateFormatHandling.IsoDateFormat,
Formatting = Formatting.None
});
//Formatto i valori del verb, controller e azione
return string.Format("{0} -> {1}/{2}({3})", Verb, ControllerName, ActionName, jsonParam);
}
/// <summary>
/// Get string trace of authentication
/// </summary>
/// <returns>Returns string</returns>
protected string GetAuthenticationTrace()
{
//Se non ho autenticazione
if (!IsAuthenticated)
return "Anonyous";
//Ritorno il nome utente e tipologia di autenticazione
return string.Format("{0} [{1}]", AutheticatedUsername, AuthenticationType);
}
/// <summary>
/// Get string trace of result
/// </summary>
/// <returns>Returns string</returns>
protected string GetResultTrace()
{
//Validazione oggetti
if (Result == null) throw new NullReferenceException("Result is invalid.");
//Traccio il tipo di result della action
string trace = Result.GetType().Name;
//Se ho una eccezione, la accodo
if (Exception != null)
{
//Accodo la serializzazione del risultato
trace = string.Format("{0} -> exception : {1}", trace, Exception.Message);
}
//Se è di tipo Json, serializzo il contenuto
JsonResult jsonResult = Result as JsonResult;
if (jsonResult != null)
{
//Eseguo la serializzazione dei dati
string jsonData = JsonConvert.SerializeObject(jsonResult.Data, new JsonSerializerSettings
{
DateFormatHandling = DateFormatHandling.IsoDateFormat,
Formatting = Formatting.None
});
//Accodo la serializzazione del risultato
trace = string.Format("{0} -> result : {1}", trace, jsonData);
}
//Emetto il risultato
return trace;
}
/// <summary>
/// Trace request of action
/// </summary>
/// <param name="authenticationTrace">Authentication trace</param>
/// <param name="requestTrace">Request trace</param>
protected virtual void TraceRequest(string authenticationTrace, string requestTrace)
{
//Di base eseguo il tracciamento sulla console
Debug.WriteLine("[{0}] Request : {1} | '{2}'", GetType().Name,
authenticationTrace, requestTrace);
}
/// <summary>
/// Trace response of action
/// </summary>
/// <param name="resultTrace">Result trace</param>
protected virtual void TraceResponse(string resultTrace)
{
//Di base eseguo il tracciamento sulla console
Debug.WriteLine("[{0}] Response : {1}", GetType().Name, resultTrace);
}
}
}
]]></script>
Due metodi virtuali, "TraceRequest" e "TraceResponse", si prenderanno l'onere di eseguire il tracciamento effettivo delle due fasi
dell'invocazione. In questo caso non ho voluto inserire alcuna dipendenza nel "filter" (a parte, naturalmente, quella di Json.Net):
il trace avviene nell'output console dell'ambiente di sviluppo; ma il target può essere facilmente essere variato derivando la
classe, in modo che si ridirezioni l'azione di scrittura su un file di log, oppure sulla base dati applicativa (secondo vostre esigenze).
<br />
<br />
Per controllare in maniera più "granulare" gli eventi, ho ritenuto utile applicare due proprietà pubbliche sulla classe "TraceActionAttribute":
"EnableRequestTrace", che abilità/disabilità il tracciamento dell'evento di inizio richiesta, e "EnableResponseTrace"
che ha la medesima funzione sulla risposta del server.
<br />
<br />
L'utilizzo? Niente di più banale: basta applicare l'attributo "TraceAction" sulla singola action che si vuole monitorare, o sull'interno
controller, opportunamente impostando i flag di configurazione (che, lo ricordo, di default sono attivati).
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Web.Mvc;
using ZenProgramming.Chakra.Mvc.Filters;
namespace ZenProgramming.Mvc.Filters.Controllers
{
[TraceAction]
public class FilesController : Controller
{
public JsonResult GetSampleData(string value)
{
var sample = new
{
Code = "001",
Registration = DateTime.Now
};
return Json(sample);
}
}
}
]]></script>
L'implementazione è ancora un po' grezza; ma funziona e mi sta facendo risparmiare un sacco di tempo. Per comodità ho
raccolto questa (e altre) piccole utilità in un progettino in hosting su NuGet:
<a href="https://www.nuget.org/packages/Chakra.Mvc">Chakra.Mvc</a>.
<br />
<a href="https://www.nuget.org/packages/Chakra.Mvc">
<img border="0" src="http://4.bp.blogspot.com/-P-xOEJTf7mk/UvKhKIn0n0I/AAAAAAAABSE/oQr8kJkqxhY/s1600/chakra_logo.png" height="100" style="float:right" />
</a>
<br />
Potete pensare di esporre i log registrati da "TraceAction" direttamente su un particolare metodo ad uso diagnostica
(es. "Diagnostics/RecentLogs"): i colleghi che utilizzeranno la vostra piattaforma di "WebAPI" saranno in grado di fare
autodiagnostica delle chiamate che stanno utilizzando, riducendo al minimo il vostro coinvolgimento...con notevole
riduzione dei travasi di bile per tutti i componenti del team!
<br />
M.<br />Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-46046709067340235062013-12-28T16:14:00.000+01:002013-12-28T16:14:00.438+01:00Chrome Development : RESTful StressNegli ultimi mesi sto lavorando sempre più spesso nello sviluppo di applicazioni "service oriented". Sviluppare piattaforme di servizi web è sicuramente una delle cose che preferisco, perchè mi permette di lavorare molto a livello funzionale ed architetturale, e meno sulla parte front-end; quest'ultima...beh, diciamo che non è "nelle mie corde". Mi considero una capra (con tutto il rispetto per gli ovini) nella realizzazione di interfacce uomo macchina e, in generale, in tutto quello che va sotto la definizione di "user experience". In realtà con il "gusto per l'estetica" ci devi nascere; se non se ne è dotati, meglio limitarsi delle cose semplici ed essenziali, lasciando la gloria ai colleghi "web designer"...
<br/>
<br/>
Quello che è fondamentale tenere in considerazione quando si lavora ad una piattaforma di puri servizi web, è che è necessario esporre pubblicamente solo le informazioni e le operazioni strettamente necessarie al corretto funzionamento del sistema. I metodi a disposizione dell'utente fruitore finale, devono essere auto-consistenti, atomici, ed esprimere un flusso funzionale completo compreso tra "request" del client, e la "response" del server.
<br/>
<br/>
Ma questo non è post di natura "filosofica", quindi veniamo al sodo dell'argomento. Come possiamo "simulare" un'invocazione da un client remoto, recuperare gli eventuali input inviati, e verificare la response emessa dal nostro servizio? Facile: basta creare un semplice client ed eseguire la chiamata all'indirizzo sul quale il servizio è esposto.
<br/>
<br/>
Verissimo: è molto facile. Ma quando siete ancora in fase di sviluppo del vostro web service, e vi trovare a dover modificare di continuo i "contratti" dello stesso (nomi dei metodi, dei parametri, o semantica degli stessi), dovete anche essere pronti a modificare il vostro piccolo client per rispettare i cambiamenti operati sul server. Lavoro lungo, tedioso, prono ad errori e certamente molto noioso.
<br/>
<br/>
Meglio utilizzare uno strumento esterno, magari realizzato per venire incontro a queste particolari esigenze. Sul mercato ce ne sono di ottimi: il primo a cui ho rivolto la mia attenzione è "Postman", una estensione di Google Chrome che permette di fare tutto quello che ci serve, in maniera tuttosommato immediata e semplice.
<br/>
<br/>
Meravigliosa: funziona benissimo e fa quello che mi serve. Questo finchè non mi sono trovato a dovermi confrontare con una esigenza un po' "particolare". Ho un servizio web <a href="http://it.wikipedia.org/wiki/Representational_State_Transfer">RESTful</a> che funziona senza problemi; tuttavia noto che i tempi di risposta, a partità di input, cambiano al passare del tempo. Avevo bisogno di iterare la stessa richiesta nel corso di un periodo di tempo piuttosto lungo (un'ora nel mio caso), ma non avevo assolutamente voglia di cliccare il pulsante "Send" di Postman ogni 5 secondi per tutto quel tempo.
<br/>
<br/>
Dopo una breve (ed infruttuosa) ricerca su internet, non ho trovato nulla che facesse al caso mio. Per carità: questo strumento ci sarà pure, ma semplicemente non l'ho scovato...oppure diciamo che avevo voglia di capire come funzionavano le "Extensions" e le "Packaged Apps" di Google Chrome, e ho iniziato a lavorare al mio piccolo progetto.
<br/>
<br/>
Dopo una breve documentazione sul funzionamento della piattaforma "Chrome", ho iniziato a lavorare sulla mia estensione usando tecnologie "client-side" che ben conosco: jQuery, KnockoutJs, DurandalJs e, per la parte grafica (ricordiamoci che sono solo un "ovino" con il pollice opponibile) Bootstrap e Font Awesome.
<br/>
<br/>
L'itegrazione con le API native di Chrome funziona senza problemi, e tutte e tecnologie adottate asservono al mio scopo; nel giro di qualche ora la prima "early version" di <b><a href="https://chrome.google.com/webstore/detail/restful-stress/lljgneahfmgjmpglpbhmkangancgdgeb">RESTfull Stress</a></b> è pronta e pubblicata sul Chrome Store sotto forma di "Extension".
<br/>
<br/>
Come recita il "bugiardino" pubblicato, l'utilizzo è molto semplice: basta inserire l'indirizzo del servizio web da "stressare", selezionare il "verb HTTP" ("GET, "POST", "PUT" o "DELETE"), impostare il numero di iterazioni e l'intervallo tra le stesse, e cliccare sul pulsante "Start".
<br/>
<br/>
Un pannello di feedback visualizza il response del server, calcolando i tempi di esecuzione di ciascuna chiamata, e permettendomi di monitorare l'andamento di ogni singola esecuzione e le sue oscillazioni temporali.
<br/>
<br/>
Tutto bene fino alla versione 0.0.0.3. Poi, la mia voglia di seguire i consigli di Google che raccomandava l'utilizzo di <b>AngularJs</b> per lo sviluppo su Chrome, mi ha messo nei guai, anche se poi ne sono uscito. Ma questa è un'altra storia...
<br/>
<br/>
Buon anno a tutti!
<br/>
M.
Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com3tag:blogger.com,1999:blog-6432843868391268534.post-79170053135994593692013-11-30T11:34:00.000+01:002013-11-30T11:34:01.055+01:00IUserType: serializzazione di un oggetto singoloPer completezza di informazione mi sembra utile fare una piccola integrazione al mio <a href="http://zenprogramming-it.blogspot.it/2013/11/nhibernate-iusertype-in-combinazione.html">ultimo post</a> relativo ai custom "IUserType" di NHibernate. Quella particolare implementazione prendeva in esame una lista di oggetti da serializzare in JSON; tuttavia le situazione in cui è necessario salvare un tipo complesso "singolo", sono altrettanto significative. Per questo motivo, senza ulteriori indugi vi lascio il codice della classe "JsonObjectTypeBase di T" che permette il suo corretto trattamento.
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using NHibernate.SqlTypes;
using System;
using System.Data;
using Newtonsoft.Json;
using NHibernate;
using NHibernate.SqlTypes;
using NHibernate.UserTypes;
namespace ZenProgramming.JsonUserType.Types
{
/// <summary>
/// Represents abstract class for NHibernate user type that
/// allow storage of single object serializable in Json format
/// </summary>
public abstract class JsonObjectTypeBase<TPoco> : IUserType
where TPoco : class, new()
{
#region Public properties
/// <summary>
/// Specifify if is mutable
/// </summary>
public bool IsMutable
{
get { return false; }
}
/// <summary>
/// List of SQL types for implementation
/// </summary>
public SqlType[] SqlTypes
{
get
{
//We store our Uri in a single column in the database that can contain a string
SqlType[] types = new SqlType[1];
types[0] = new SqlType(DbType.String);
return types;
}
}
/// <summary>
/// Returns type of custom user type
/// </summary>
public Type ReturnedType
{
get { return typeof(TPoco); }
}
#endregion
/// <summary>
/// Verify equality of data
/// </summary>
/// <param name="x">First operand</param>
/// <param name="y">Second operand</param>
/// <returns>Returns true if is equal</returns>
public new bool Equals(object x, object y)
{
//Eseguo la verifica tra i due riferimenti
if (ReferenceEquals(x, y))
return true;
//Utilizzo l'implementazione di Equals del behavior
return x != null && x.Equals(y);
}
/// <summary>
/// Generate hash code for objet
/// </summary>
/// <param name="x">Source object</param>
/// <returns>Returns hash code</returns>
public int GetHashCode(object x)
{
//Se l'oggetto è nullo, l'hashcode è 0
if (x == null) return 0;
//Se l'oggetto non è un TPoco, emetto eccezione
if (!(x is TPoco))
throw new InvalidCastException(string.Format("Unable to cast to type '{0}'.", typeof(TPoco)));
//Altrimenti ritorno l'hashcode
return (x as TPoco).GetHashCode();
}
/// <summary>
/// Execute "safe" get of value of type
/// </summary>
/// <param name="rs">Reader</param>
/// <param name="names">Names of properties</param>
/// <param name="owner">Owner</param>
/// <returns>Returns value</returns>
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
//Recupero il valore dal reader (come stringa)
string json = (string)NHibernateUtil.String.NullSafeGet(rs, names[0]);
//Se il valore è nullo, emetto una lista nulla
if (string.IsNullOrEmpty(json)) return new TPoco();
//Eseguo la deserializzazione del json nel tipo specificato
TPoco element = JsonConvert.DeserializeObject<TPoco>(json);
//Mando in uscita l'istanza
return element;
}
/// <summary>
/// Execute a "safe" set of value
/// </summary>
/// <param name="cmd">Command</param>
/// <param name="value">Value</param>
/// <param name="index">Index</param>
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
//Se il valore è nullo, archivio un valore nullo nel reader
if (value == null)
{
//Imposto il valore null nel command
NHibernateUtil.String.NullSafeSet(cmd, null, index);
}
else
{
//Se l'oggetto non è un elemento valido, emetto eccezione
if (!(value is TPoco))
throw new InvalidCastException(string.Format("Unable to cast to type '{0}'.", typeof(TPoco)));
//Eseguo la serializzazione dell'elemento
string json = JsonConvert.SerializeObject(value as TPoco);
//Imposto il json nel command
NHibernateUtil.String.NullSafeSet(cmd, json, index);
}
}
/// <summary>
/// Executes a deep copy of the behavior
/// </summary>
/// <param name="value">Value to copy</param>
/// <returns>Returns copy of value</returns>
public object DeepCopy(object value)
{
//Se il valore da copiare è nullo, ritorno null
if (value == null) return null;
//Se l'oggetto non è un valido, emetto eccezione
if (!(value is TPoco))
throw new InvalidCastException(string.Format("Unable to cast to type '{0}'.", typeof(TPoco)));
//Eseguo il casting alla persistenza
TPoco original = value as TPoco;
//Eseguo una copia della persistenza
TPoco instance = DeepCopy(original);
//Mando in uscita la nuova instanza
return instance;
}
/// <summary>
/// Execute a deep copy of original object on a new one
/// </summary>
/// <param name="original">Original</param>
/// <returns>Returns a copy</returns>
protected abstract TPoco DeepCopy(TPoco original);
/// <summary>
/// Replace original object with the target
/// </summary>
/// <param name="original">Original</param>
/// <param name="target">Target</param>
/// <param name="owner">Owner</param>
/// <returns>Returns replace object</returns>
public object Replace(object original, object target, object owner)
{
//Poichè l'oggettè è immutabile, ritorno l'originale
return original;
}
/// <summary>
/// Execute assembling of object
/// </summary>
/// <param name="cached">Cached object</param>
/// <param name="owner">Owner</param>
/// <returns>Returns assembled object</returns>
public object Assemble(object cached, object owner)
{
//Usato per caching; poichè l'oggettè è immutabile, ritorno l'originale
return cached;
}
/// <summary>
/// Execute disassemble of object
/// </summary>
/// <param name="value">Value</param>
/// <returns>Returns disassembled object</returns>
public object Disassemble(object value)
{
//Usato per caching; poichè l'oggettè è immutabile, ritorno l'originale
return value;
}
}
}
]]></script>
Aggiungiamo al modello "GeoLocation" di esempio una ulteriore proprietà "Data", di tipo "LocationData" che servirà per integrare delle informazioni addizionali sull'oggetto principale.
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System.Collections.Generic;
namespace ZenProgramming.JsonUserType
{
public class GeoLocation
{
public virtual int? Id { get; set; }
public virtual string Name { get; set; }
public virtual double Latitude { get; set; }
public virtual double Longitude { get; set; }
public virtual IList<RemoteResource> RemoteResources { get; set; }
public virtual LocationData Data { get; set; }
}
}
]]></script>
Chiaramente la situazione è sempre la medesima del precedente articolo: non si vogliono eseguire operazioni di filtro su queste informazioni, ma semplicemente visualizzarle a titolo informativo.
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
namespace ZenProgramming.JsonUserType
{
public class LocationData
{
public string Region { get; set; }
public int Population { get; set; }
public bool IsActive { get; set; }
}
}
]]></script>
La mappatura NHibernate dell'oggetto dovrà essere anch'essa integrata della nuova proprietà (anche in questo caso utilizziamo un campo di testo con lunghezza non predeterminata); il tutto dovrà essere trattato con il tipo custom che andremo a specializzare.
<script class="brush: xml" type="syntaxhighlighter"><![CDATA[
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="JsonUserType"
namespace="ZenProgramming.JsonUserType">
<class name="GeoLocation" table="GeoLocations">
<id name="Id" column="Id">
<generator class="hilo"/>
</id>
<property name="Name" length="255" not-null="true" />
<property name="Latitude" not-null="true" />
<property name="Longitude" not-null="true" />
<property name="RemoteResources" not-null="true" type="ZenProgramming.JsonUserType.Types.RemoteResourceListType, JsonUserType" />
<property name="Data" not-null="true" type="ZenProgramming.JsonUserType.Types.LocationDataType, JsonUserType" />
</class>
</hibernate-mapping>
]]></script>
Per concludere, riporto la specializzazione del tipo generico "JsonObjectTypeBase" sopra proposto:
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
namespace ZenProgramming.JsonUserType.Types
{
/// <summary>
/// Represents NHibernate custom type to store an
/// element of type 'LocationData' as JSON
/// </summary>
public class LocationDataType : JsonObjectTypeBase<LocationData>
{
/// <summary>
/// Execute a deep copy of original object on a new one
/// </summary>
/// <param name="original">Original</param>
/// <returns>Returns a copy</returns>
protected override LocationData DeepCopy(LocationData original)
{
//Se l'oggetto originale è nullo, ritorno null
if (original == null)
return null;
//Creo una nuova istanza e copio i valori
return new LocationData
{
Region = original.Region,
Population = original.Population,
IsActive = original.IsActive
};
}
}
}
]]></script>
Anche in questo caso, il metodo che è necessario implementare è "DeepCopy", tramite il quale è NHibernate è in grado di eseguire una copia dell'oggetto trattato.
<br />
<br />
Chiudo con una piccola riflessione sul motivo per cui un approccio di questo genere potrebbe essere più "snello" rispetto alla classica aggiunta di nuove colonne nella base dati. Sovente capita di sviluppare applicativi, più o meno complessi, dove alcuni oggetti sono corredati di una serie di informazioni addizionali che possono essere utili o meno. Sopratutto in fase prototipale, si parte con un subset di dati minimo (magari solo con le informazioni essenziali); con il passare del tempo i clienti si accorgono di aver bisogno di "questo" o "quel" dato in più, variando il set delle informazioni con elevata frequenza.
<br />
<br />
Il tutto corrisponde a numerose implementazioni che vanno a "toccare" diversi strati applicativi: presentazione, servizio, logica di business, persistenza e, chiaramente, la base dati, certamente la componente più delicata. Approcciare in maniera "lite" questo cambio di requisiti non esenta dal mettere mano al sistema, ma almeno evita che una componente fondamentale come il database sia impattata dalle frequenti evoluzioni, e sia più gestibile una integrazione e un deploy in ambiente di Produzione.
<br />
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-78424965394548321742013-11-09T16:17:00.000+01:002013-11-26T18:49:04.861+01:00NHibernate IUserType in combinazione con JSON.NETNel precedente <a href="http://zenprogramming-it.blogspot.it/2013/10/nhibernate-e-json-semplice-e-funzionale.html">articolo</a> ci siamo lasciati con la definizione del nostro dominio applicativo. Abbiamo introdotto l'interfaccia "IUserType" di NHibernate, utilizzata per "insegnare" all'OR/M come trattare tipi di dati non nativi e persisterli sulla base dati.
<br />
<br />
Non è mia intenzione spiegare tutte le peculiarità e le sfaccettature dell'utilizzo di IUserType: ci sono moltissimi tutorial in rete (molti dei quali veramente ben scritti) che spiegano in dettaglio ogni singolo aspetto. Tuttavia, per completezza di informazione ripoterò una brevissimo estratto della definizione dell'interfaccia nuda e cruda:
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using NHibernate.SqlTypes;
using System;
using System.Data;
namespace NHibernate.UserTypes
{
public interface IUserType
{
bool IsMutable { get; }
Type ReturnedType { get; }
SqlType[] SqlTypes { get; }
object Assemble(object cached, object owner);
object DeepCopy(object value);
object Disassemble(object value);
bool Equals(object x, object y);
int GetHashCode(object x);
object NullSafeGet(IDataReader rs, string[] names, object owner);
void NullSafeSet(IDbCommand cmd, object value, int index);
object Replace(object original, object target, object owner);
}
}
]]></script>
Essenzialmente, quello che deve essere fornito all'OR/M, per renderlo in grado di trattare il dato, è la specifica del tipo di colonna su cui dovrà persistere l'informazione stessa. Nel nostro caso l'obiettivo è salvare un oggetto strutturato in formato JSON sulla base dati: quale miglior (e conveniente) scelta se non un campo "stringa" senza limite sul numero di caratteri?
<br/>
<br/>
Per Microsoft SQL Server verrà quindi utilizzata una colonna NVARCHAR(MAX), per Oracle un CLOB (o tipo analogo), e via dicendo... Il tipo "astratto", nativo di NHibernate, che fungerà a nostro scopo è un "StringClobSqlType"; ma poichè l'interfaccia di definizione dei tipi ("SqlTypes") permette di esplicitare più di un riferimento, la mappatura deve essere ritornata in un array di elementi. E' inoltre fondamentale specificare, come ritorno della proprietà "ReturnType" il tipo dell'elemento di uscita; per intenderci, il tipo della proprietà che sarà esposta sul modello dati ("IList di RemoteResource").
<br /><br />
Una volta specificato l'input e output dei tipi che andremo a trattare, quello che manca da fare è specificare il modo con cui NHibernate deve manipolare e trasformare le informazioni per convertire una lista di oggetti nel tipo stringa su cui abbiamo deciso di salvare il JSON. I metodi "NullSafeGet" e "NullSafeSet" fungono a questo scopo: usando un "DataReader" e un "DbCommand" dobbiamo codificare il passaggio di serializzazione e deserializzazione delle informazioni, assicurandoci che tutte le possibili casistiche siano contemplate e gestite (valori nulli, mancanza di informazioni, validità dei cast, etc.).
<br /><br />
L'argomento, essendo di natura piuttosto teorica, è difficile da illustrare a parole. Aggiungeteci pure il fatto che ho voluto generalizzare l'implementazione e costruire uno "usertype" riutilizzabile per diverse situazioni. Come spesso succede, qualche riga di codice è molto più esplicativa di un fiume di inutili sproloqui...quindi a seguire riporto il contenuto della classe astratta che implementa "IUserType".
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Collections.Generic;
using System.Data;
using Newtonsoft.Json;
using NHibernate;
using NHibernate.SqlTypes;
using NHibernate.UserTypes;
namespace ZenProgramming.JsonUserType.Types
{
/// <summary>
/// Represents abstract class for NHibernate user type that
/// allows storage of array of objects serializable in Json format
/// </summary>
public abstract class JsonArrayTypeBase<TPoco> : IUserType
where TPoco : class, new()
{
#region Public properties
/// <summary>
/// Specifify if it's mutable
/// </summary>
public bool IsMutable
{
get { return false; }
}
/// <summary>
/// List of SQL types for implementation
/// </summary>
public SqlType[] SqlTypes
{
get
{
return new SqlType[] { new StringClobSqlType() };
//We store our Uri in a single column in the database that can contain a string
//SqlType[] types = new SqlType[1];
//types[0] = new SqlType(DbType.String);
//return types;
}
}
/// <summary>
/// Returns type of custom user type
/// </summary>
public Type ReturnedType
{
get { return typeof(IList<TPoco>); }
}
#endregion
/// <summary>
/// Verify equality of data
/// </summary>
/// <param name="x">First operand</param>
/// <param name="y">Second operand</param>
/// <returns>Returns true if is equal</returns>
public new bool Equals(object x, object y)
{
//Eseguo la verifica tra i due riferimenti
if (ReferenceEquals(x, y))
return true;
//Utilizzo l'implementazione di Equals del behavior
return x != null && x.Equals(y);
}
/// <summary>
/// Generate hash code for objet
/// </summary>
/// <param name="x">Source object</param>
/// <returns>Returns hash code</returns>
public int GetHashCode(object x)
{
//Se l'oggetto è nullo, l'hashcode è 0
if (x == null) return 0;
//Se l'oggetto non è una lista, emetto eccezione
if (!(x is IList<TPoco>))
throw new InvalidCastException(string.Format("Unable to cast to type '{0}'.", typeof(IList<TPoco>)));
//Altrimenti ritorno l'hashcode
return (x as IList<TPoco>).GetHashCode();
}
/// <summary>
/// Execute "safe" get of value of type
/// </summary>
/// <param name="rs">Reader</param>
/// <param name="names">Names of properties</param>
/// <param name="owner">Owner</param>
/// <returns>Returns value</returns>
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
//Recupero il valore dal reader (come stringa)
string json = (string)NHibernateUtil.String.NullSafeGet(rs, names[0]);
//Se il valore è nullo, emetto una lista nulla
if (string.IsNullOrEmpty(json)) return new List<TPoco>();
//Eseguo la deserializzazione del json in una lista
IList<TPoco> elements = JsonConvert.DeserializeObject<List<TPoco>>(json);
//Mando in uscita l'istanza
return elements;
}
/// <summary>
/// Execute a "safe" set of value
/// </summary>
/// <param name="cmd">Command</param>
/// <param name="value">Value</param>
/// <param name="index">Index</param>
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
//Se il valore è nullo, archivio un valore nullo nel reader
if (value == null)
{
//Imposto il valore null nel command
NHibernateUtil.String.NullSafeSet(cmd, null, index);
}
else
{
//Se l'oggetto non è una lista di elementi, emetto eccezione
if (!(value is IList<TPoco>))
throw new InvalidCastException(string.Format("Unable to cast to type '{0}'.", typeof(IList<TPoco>)));
//Eseguo la serializzazione dell'elemento
string json = JsonConvert.SerializeObject(value as IList<TPoco>);
//Imposto il json nel command
NHibernateUtil.String.NullSafeSet(cmd, json, index);
}
}
/// <summary>
/// Executes a deep copy of the behavior
/// </summary>
/// <param name="value">Value to copy</param>
/// <returns>Returns copy of value</returns>
public object DeepCopy(object value)
{
//Se il valore da copiare è nullo, ritorno null
if (value == null) return null;
//Se l'oggetto non è un bahavior, emetto eccezione
if (!(value is IList<TPoco>))
throw new InvalidCastException(string.Format("Unable to cast to type '{0}'.", typeof(IList<TPoco>)));
//Creo una nuova lista di elementi e casto quella precendente
IList<TPoco> source = value as IList<TPoco>;
IList<TPoco> target = new List<TPoco>();
//Scorro la sorgente dei dati e compongo nuovi valori
foreach (TPoco currentSource in source)
{
//Creo una nuova destinazione
TPoco targetElement = DeepCopy(currentSource);
//Aggiungo l'elemento alla lista
target.Add(targetElement);
}
//Mando in uscita la lista copiata
return target;
}
/// <summary>
/// Execute a deep copy of original object on a new one
/// </summary>
/// <param name="original">Original</param>
/// <returns>Returns a copy</returns>
protected abstract TPoco DeepCopy(TPoco original);
/// <summary>
/// Replace original object with the target
/// </summary>
/// <param name="original">Original</param>
/// <param name="target">Target</param>
/// <param name="owner">Owner</param>
/// <returns>Returns replace object</returns>
public object Replace(object original, object target, object owner)
{
//Poichè l'oggetto è immutabile, ritorno l'originale
return original;
}
/// <summary>
/// Execute assembling of object
/// </summary>
/// <param name="cached">Cached object</param>
/// <param name="owner">Owner</param>
/// <returns>Returns assembled object</returns>
public object Assemble(object cached, object owner)
{
//Usato per caching; poichè l'oggetto è immutabile, ritorno l'originale
return cached;
}
/// <summary>
/// Execute disassemble of object
/// </summary>
/// <param name="value">Value</param>
/// <returns>Returns disassembled object</returns>
public object Disassemble(object value)
{
//Usato per caching; poichè l'oggetto è immutabile, ritorno l'originale
return value;
}
}
}
]]></script>
Chiaramente, al fine di utilizzare il nostro user type custom, la classe astratta va specializzata, specificando come parametro generico del tipo l'elemento singolo della lista di oggetti da "trasformare": la classe "RemoteResource".
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
namespace ZenProgramming.JsonUserType.Types
{
/// <summary>
/// Represents NHibernate custom type to store list
/// of elements 'RemoteResource' as JSON
/// </summary>
public class RemoteResourceListType : JsonArrayTypeBase<RemoteResource>
{
/// <summary>
/// Execute a deep copy of original object on a new one
/// </summary>
/// <param name="original">Original</param>
/// <returns>Returns a copy</returns>
protected override RemoteResource DeepCopy(RemoteResource original)
{
//Se l'oggetto originale è nullo, ritorno null
if (original == null)
return null;
//Creo una nuova instanza e copio i valori
return new RemoteResource
{
Url = original.Url,
RegistrationDate = original.RegistrationDate
};
}
}
}
]]></script>
L'unico metodo che è stato necessario implementare esplicitamente, come è possibile vedere dal codice sopra riportato, è "DeepCopy". Questo metodo, "wrapper" tipizzato dell'interfaccia base di "IUserType", richiede allo sviluppatore di fornire la logica di duplicazione di uno degli oggetti presenti nella lista; nel nostro caso è sufficiente creare una nuova instanza della classe "RemoteResource" e copiarci dentro le informazioni dell'oggetto sorgente.
<br/>
<br/>
L'interfaccia "IUserType" di NHibernate è incredibilmente versatile; permette di immettersi nella "pipeline" dell'OR/M in maniera efficace, intervenendo nei processi di archiviazione e recupero delle informazioni dalla base dati sottostante. L'utilizzo che ne ho fatto in questo articolo è tutto sommato banale: nonostante questo mi ha permesso di risparmiarmi parecchie ore dedicate alla definizione delle mappature ".hbm.xml", e diverse tabelle sul database (della utilità quantomeno dubbia). Alla prossima...
<br/>
<br/>
M.
Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-85145745212597559672013-10-27T11:21:00.000+01:002013-11-07T19:16:41.181+01:00NHibernate e Json: semplice e funzionaleNon nego di aver trascurato un po' il blog in questi ultimi tre mesi; il tanto lavoro e niente vacanze, hanno influito negativamente sulla mia vena creativa.
<br />
<br />
Nonostante tutto, ancora una volta, un "meraviglioso" lavoro con cui mi sto cimentando in questo periodo é fonte dell'ispirazione di questo post.
<br />
<br />
Definiamo lo scenario di lavoro: ammettiamo di avere un elenco di elementi, tuttosommato semplici, che posseggono una serie di oggetti collegati. Per fare un esempio calzante, definiamo un certo numero di locazioni geografiche ("GeoLocation"), ciascuna delle quali contiene una lista di risorse ("RemoteResource"). La locazione possiede una serie di attributi (che sono di nessuna rilevanza per la questione), mentre le risorse sono composte solamente da un url assoluto, e una data di registrazione della risorse stessa.
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System.Collections.Generic;
namespace ZenProgramming.JsonUserType
{
public class GeoLocation
{
public virtual int? Id { get; set; }
public virtual string Name { get; set; }
public virtual double Latitude { get; set; }
public virtual double Longitude { get; set; }
public virtual IList<RemoteResource> RemoteResources { get; set; }
}
}
]]></script>
Normalmente, per la persistenza dei dati su un database, si procederebbe alla creazione di una tabella per le locazioni ("GeoLocations"), ed una separata per le risorse ("RemoteResources"); ciascuna di queste ultime, oltre ai campi "Url" e "RegistrationDate", avrà poi una chiave esterna alla specifica locazione di riferimento.
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
namespace ZenProgramming.JsonUserType
{
public class RemoteResource
{
public string Url { get; set; }
public DateTime RegistrationDate { get; set; }
}
}
]]></script>
Ma ammettiamo di essere un situazione in cui <b>non è necessario</b> che siano eseguite delle particolare ricerche sulla base degli attributi di "RemoteResource": tutti gli ordinamenti e le proiezioni sono alla base della sola "GeoLocation". La lista di risorse, a tutti gli effetti, è un attributo "fisso" della locazione; e allora, perchè non trattarlo come tale e permettere che una "GeoLocation" sia persistita sulla base dati come entità atomica? Perchè, quando eseguo un'operazione di salvataggio o di caricamento, non posso ottenere con un singolo "round trip" sulla base dati tutte le informazioni "auto-consistenti" di una entità geografica?
<br />
<br />
Certo, la prima possibilità per fare questo, con la struttura di archiviazione definita, è quella di procedere con un semplice "join" sulle due tabelle sorgenti. Ma diciamo che vogliamo trattare il modello "GeoLocation" come un grafo, un documento, che contiene tutte le informazioni per definire se stesso nel dominio dati <b>come entità unica ed inscindibile</b>.
<br />
<br />
Per fare ciò, l'unica possibile soluzione è predisporre un campo della nostra tabella "GeoLocations" ad ospitare i dati della lista di "RemoteResource" collegate alla locazione stessa: un banale "NVARCHAR(MAX)" (in SQL Server) o un type di analoghe caratteristiche.
<br />
<br />
Il dato deve essere poi, in qualche modo, trasformato in un formato dal quale sia possibile eseguire la smaterializzazione/rimaterializzazione dei modelli delle risorse remote, in maniera semplice, rapida e senza alcuna perdita di informazione. Il formato più consono allo scopo (e il più compatto) è sicuramente JSON (Javascript Object Notation), e lo strumento per realizzare questa operazione di conversione è la libreria open source Json.Net.
<br />
<br />
Ma procediamo con ordine. La mappatura di NHibernate dell'entità "GeoLocation" non è niente di più che il solito file XML dichiarato come Embedded Resource, all'interno del quale il campo che sarà adibito al contenimento dei nostri dati serializzati, è opportunamente caratterizzato da un "type" custom.
<script class="brush: xml" type="syntaxhighlighter"><![CDATA[
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="JsonUserType"
namespace="ZenProgramming.JsonUserType">
<class name="GeoLocation" table="GeoLocations">
<id name="Id" column="Id">
<generator class="hilo"/>
</id>
<property name="Name" length="255" not-null="true" />
<property name="Latitude" not-null="true" />
<property name="Longitude" not-null="true" />
<property name="RemoteResources" not-null="true" type="ZenProgramming.JsonUserType.Types.RemoteResourceListType, JsonUserType" />
</class>
</hibernate-mapping>
]]></script>
Tutta la "magia" si nasconde nell'implementazione dell'interfaccia "IUserType", che opportunamente combinata con le funzioni di serializzazione di Json.Net, permette di "insegnare" ad NHibernate come trattare un oggetto particolare, che altrimenti non sarebbe in grado di gestire.
<br />
<br />
L'implementazione del custom type è tutt'altro che semplice da illustrare. In rete si trovano decine di tutorial ed articoli che trattano la tematica nel dettaglio, ed illustrano con rara precisione ogni singola sua peculiarità; ma a me interessa condividere questa particolare applicazione...nel prossimo post.
<br />
<br />
Alla prossima...
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-77998702662402703222013-07-13T16:06:00.000+02:002013-07-14T19:03:28.428+02:00Sfruttare le Action ASP.NET MVC da Silverlight: dalla teoria alla praticaCome detto, in alcuni scenari legati a Silverlight, è possibile sostituire le funzionalità di WCF Ria Services
con delle più “snelle” action ASP.NET MVC. L’obiettivo dichiarato è rimuovere la complessità di una piattaforma
di servizi che, a volte, può risultare eccessivamente pesante da utilizzarsi in situazioni tutto sommato semplici.
<br />
<br />
Se non si fa un massiccio uso del “tracking” (cioè del tracciamento dello stato di ogni modello emesso dal server),
l'uso dei WCF Ria Services è rimpiazzabile con una soluzione, a mio parere, semplice e performante;
una soluzione che rappresenta il giusto compromesso tra produttività e innovazione, orientata a tecnologie più moderne.
<br />
<br />
Se siamo in una situazione in cui l'applicazione Silverlight (come detto, basata su WCF Ria Services) è già completa
e funzionante - certo, se non volete farvi del male - vi esorterei a domandarvi se esiste una reale necessità di
cambiare il meccanismo per migliorare le performance. Se invece l'applicazione è in sviluppo oppure la sua dimensione
è ridotta, ritengo sia un "enhancement" che vale la pena di provare.
<br />
<br />
Mi sembra inutile dirlo, ma bisogna innanzitutto partire dal fatto che l'applicazione web che terrà in hosting
il file ".xap" di Silverlight dovrà essere basata su ASP.NET MVC (almeno la versione 3.0, anche se consiglio di partire
subito con la 4.0). Una volta pronta la "solution" e fatto il setup della pagina che ispiterà il plugin di Silverlight,
è necessario inserire un controller apposito per permettere di ospitare le action che emetteranno le informazioni.
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Collections.Generic;
using System.Threading;
using System.Web.Mvc;
using WcfRiaAlternative.Mvc.Models.Home;
namespace WcfRiaAlternative.Mvc.Controllers
{
public class ProductsController : Controller
{
public ActionResult FetchProducts(int categoryId)
{
//*** In questo punto dovrebbe essere eseguita una validazione della categoria
//*** richiesta, recuperato il suo riferimento dal database e recuperata la lista
//*** di elementi prodotto appartenenti alla categoria specificata
//Compongo una lista di elementi "fake" per scopo di test
Random rnd = new Random();
IList<ProductModel> models = new List<ProductModel>();
for (int i = 0; i < 1000; i++)
{
models.Add(new ProductModel
{
Id = i,
Name = string.Format("Product {0}", i),
Description = "some description of current product",
Quantity = rnd.Next(0, 1000)
});
}
//*** Facciamoci del male inserendo anche uno sleep per aumentare
//*** il tempo di attesa tra la richiesta lato "smart-client" e
//*** la risposta del server basato su action MVC
Thread.Sleep(5000);
//Ritorno i modelli in formato JSON
return Json(models);
}
}
}
]]></script>
Nel mio esempio ho creato un semplicissimo controller denominato "ProductsController", che contiene
la action "FetchProducts", e prende in input l'identificativo della categoria di cui vogliamo
recuperare l'elenco di prodotti (sto usando il solito, noioso esempio Prodotti/Categorie...
scusate la mancanza di fantasia).
<br />
<br />
Il contenuto è auto-esplicativo; talvolta tendo ad essere molto pigro, quindi non ho nemmeno provato
ad accennare una persistenza del dato sul un eventuale database applicativo: la cosa richiederebbe
tempo e non è lo scopo di questo articolo. Quindi, da sfaticato, genero un migliaio di elementi
con valori randomizzati.
<br />
<br />
A scopo di test ho inserito anche uno "sleep" di 5 secondi per simulare un caricamento da una base
dati estremamente lenta. Magari, nelle vostre soluzioni, se usate lo stesso "trick" durante lo sviluppo,
ricordatevi di toglierlo quando passate a Produzione. Non come ho fatto io qualche anno fa, quando i
clienti hanno utilizzato per tre settimane un sistema che sospendeva l'esecuzione ad ogni accesso alla base dati. :)
<br />
<br />
La funzione emette dei modelli: una lista dei famosi "Model" di MVC ("Model", "View", "Controller"). Essi
rappresentano il dato che, presente sul server, deve essere inviato al client. E a questo punto, va subito
detto che non sta scritto da nessuna parte che il modello di persistenza (il nostro Prodotto presente sulla
base dati), debba essere trasferito esattamente identico sul client.
<br />
<br />
Il "modello" che deve arrivare sull'interfaccia utente "può" avere una rappresentazione identica all'entità
database; ma è molto più comune lo scenario in cui solo alcune delle informazioni contenute siano necessarie
per l'interazione sulla UI; oppure sia necessario integrare il modello con informazioni aggiuntive che lo completano.
<br />
<br />
Nel nostro esempio, a sostegno di questa tesi, ho introdotto un campo "Quantity", che contiene la giacenza a
magazzino delle unità del prodotto. Normalmente, questa informazione sarebbe archiviata in una entità diversa
dal "Product", e solo in rari casi si procederebbe ad "inquinare" l'anagrafica dei prodotti con un
dato che poco centra con le sue caratteristiche.
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
namespace WcfRiaAlternative.Mvc.Models.Home
{
public class ProductModel
{
public int? Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int Quantity { get; set; }
}
}
]]></script>
Una cosa che ho sempre apprezzato dei WCF Ria Services (dei tools di Visual Studio, per la verità) è la
capacità di auto-generare il codice client per persistere i modelli provenienti dal server. Rimuovendo
dall'equazione questa piattaforma, ci perdiamo questa funzione...ma poco male a confronto dei vantaggi
che otterremo. Basta un semplice "copia&incolla" (tecnica molto in voga negli ultimi tempi ;) ) e il nostro
"ProductModel" sarà presente anche nella folder "Models" presente nel progetto Silverlight.
<br />
<br />
Ora Il nostro lavoro si trasferisce sul client: quello che serve è un sistema che permetta di agganciare
il risultato JSON (Javascript Object Notation) emesso dalla action MVC "FetchProducts" definita sopra. E cosa ci
può essere di meglio di un semplicissimo "WebClient"? Niente, dico io...
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Net;
using System.Windows;
using Newtonsoft.Json;
namespace WcfRiaAlternative.Ria.Services
{
/// <summary>
/// Contains utilities for server management
/// </summary>
public static class ServerUtils
{
/// <summary>
/// Execute an async AJAX invoke on remote server sending data in JSON format
/// </summary>
/// <typeparam name="TModel">Type of result model</typeparam>
/// <param name="uri">Method uri</param>
/// <param name="verb">Http verb</param>
/// <param name="data">Data to sent to the server</param>
/// <param name="callback">Callback action</param>
public static void Ajax<TModel>(Uri uri, string verb, string data, Action<TModel> callback)
{
//Eseguo la validazione degli argomenti
if (uri == null) throw new ArgumentNullException("uri");
if (string.IsNullOrEmpty(verb)) throw new ArgumentNullException("verb");
if (data == null) throw new ArgumentNullException("data");
if (callback == null) throw new ArgumentNullException("callback");
//Utilizzo il web client per eseguire l'operazione
WebClient client = new WebClient();
//Imposto gli header della richiesta
client.Headers["Content-Type"] = "application/json; charset=utf-8";
client.Headers["Data-Type"] = "json";
//Aggancio l'evento di completamento lettura
client.UploadStringCompleted += (s, e) =>
{
//Se ci sono stati errori, emetto eccezione
if (e.Error != null) throw e.Error;
//Eseguo la deserializzazione delle informazioni
var result = JsonConvert.DeserializeObject<TModel>(e.Result);
//Invoco l'azione passando i risultati
callback(result);
};
//Mando in esecuzione la chiamata asincrona
client.UploadStringAsync(uri, verb, data);
}
//...
}
}
]]></script>
Il tutto è wrappato in un helper, con un metodo che vuole assomigliare molto alla chiamata asincrona di JQuery
per il recupero delle informazioni server. Se vogliamo essere pignoli, il componente XHR (XMLHttpRequest) è
lo stesso utilizzato da JQuery, quindi stiamo solo "simulando" un metodo JQuery in ambiente .NET. Figo, no?
<br />
<br />
Il codice è tutto sommato molto semplice: istanziamo il webclient impostando gli header di base in modo che la
richiesta inviata sia in formato JSON, agganciamo il metodo di completamento della richesta, e la avviamo in maniera
asincrona specificando l'indirizzo, il verb HTTP da usare ("GET", "POST", etc.) e gli eventuali dati da inviare
al server.
<br />
<br />
Nella funzione di callback va posto l'accento sull'utilizzo della fantastica libreria Json.NET (a mio parere la
libreria per parsing di contenuti JSON migliore in assoluto). I dati emessi dal server arrivano sul client in formato
testuale; è tuttavia necessario eseguire il parsing di tale testo in un modello strutturato: Json.NET fa questo lavoro
per noi in maniera assolutamente trasparente (e velocissima), renderizzando il modello (o la lista di modelli) specificati come parametro generico.
<br />
<br />
Resta ancora il problema di recuperare il corretto indirizzo della action lato server: il metodo presente nella classe
di utilità ci aiuta in questo. Ammetto che non è proprio una soluzione elegante, ma fa il suo sporco lavoro...
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Net;
using System.Windows;
using Newtonsoft.Json;
namespace WcfRiaAlternative.Ria.Services
{
/// <summary>
/// Contains utilities for server management
/// </summary>
public static class ServerUtils
{
//...
/// <summary>
/// Get absolute method uri using application host and partial address
/// </summary>
/// <param name="methodAddress">Method address</param>
/// <returns>Returns absolute method uri</returns>
public static Uri GetMethodUri(string methodAddress)
{
//Eseguo la validazione degli argomenti
if (string.IsNullOrEmpty(methodAddress)) throw new ArgumentNullException("methodAddress");
//Recupero l'indirizzo completo del dell'indirizzo locale
string fullAddress = Application.Current.Host.Source.AbsoluteUri;
//Recupero l'indice della cartella "ClientBin"
int clientBinPosition = fullAddress.IndexOf("/ClientBin/");
//Rimuovo la sezione con clientbin per ottenere l'indirizzo base
string baseAddress = fullAddress.Substring(0, clientBinPosition);
//Se il metodo non inizia con uno "/", lo aggiungo
if (methodAddress.Substring(0, 1) != "/")
methodAddress = "/" + methodAddress;
//Eseguo la composizione dell'Uri assoluto
return new Uri(baseAddress + methodAddress, UriKind.Absolute);
}
}
}
]]></script>
L'utilizzo del client è altrettanto semplice. Sono solito segregare il più possibile le funzionalità applicative,
quindi ho previsto l'inserimento di una classe denominata "ServerDomain" che conterrà l'elenco delle possibili
operazioni richiamabili sul server, in un unico punto. L'obiettivo è "nascondere" i dettagli implementativi di ciascun
metodo, permettendo di avere nei vari viewmodels solo invocazioni minimali e pulite.
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using WcfRiaAlternative.Ria.Models;
namespace WcfRiaAlternative.Ria.Services
{
/// <summary>
/// Server domain base on ASP.NET MVC actions
/// </summary>
public class ServerDomain
{
/// <summary>
/// Fetch all elements from server
/// </summary>
/// <param name="categoryId">Categry identifier</param>
/// <param name="callback">Async execution callback</param>
public void FetchProductsAsync(int categoryId, Action<IList<ProductModel>> callback)
{
//Eseguo la validazione degli argomenti
if (callback == null) throw new ArgumentNullException("callback");
//Eseguo la composizione dell'uri con la chiamata remota
Uri uri = ServerUtils.GetMethodUri(@"/Products/FetchProducts");
//Compongo i parametri da inviare al server
IDictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("categoryId", categoryId);
//Serializzo i dati da inviare al server
string postData = JsonConvert.SerializeObject(parameters);
//Eseguo la chiamata asincrona sul server
ServerUtils.Ajax<IList<ProductModel>>(uri, "POST", postData, callback);
}
}
}
]]></script>
Il metodo "FetchProductsAsync" fa il paio con "FetchProducts" presente sul server. Il parametro valore
specificato rappresenta l'identificativo della categoria, mentre la "Action" definita come secondo argomento
è la funzione di callback gestita sul viewmodel.
<br />
<br />
Il dettaglio implementativo della funzione contiene la "colla" che unisce le varie parti: composizione del url
della action MVC sul server, composizione dei "data" (in formato JSON) da inviare al server con l'identificativo
della categoria, ed invocazione della funzione asincrona di richiesta.
<br />
<br />
Un'altra piccola nota va dedicata ai "data" da "postare" sul server. Come detto la request deve essere in formato
JSON, quindi sfruttiamo nuovamente Json.NET per serializzare un oggetto Javascript che contiene una singola proprietà
denominata "categoryId". Vi esorto a fare attenzione al funzionamento del "model binding" di ASP.NET MVC: per eseguire
correttamente la chiamata ad una specifica action con parametri, è necessario che il nome dei parametri definiti sul
server sia una stringa identica al nome della proprietà dell'oggetto JSON serializzato lato client.
<br />
<br />
Adesso che tutto è (spero) chiaro, vediamo come invocare il tutto partendo dal nostro "MainViewModel".
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
using System.Windows.Input;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using WcfRiaAlternative.Ria.Models;
using WcfRiaAlternative.Ria.Services;
namespace WcfRiaAlternative.Ria.ViewModels
{
public class MainViewModel : ViewModelBase
{
#region Private fields
private ServerDomain _Domain;
private bool _IsBusy;
private TimeSpan _Duration;
private ObservableCollection<ProductViewModel> _ProductsSource;
private ICollectionView _Products;
#endregion
#region Public properties
public bool IsBusy
{
get { return _IsBusy; }
set { _IsBusy = value; RaisePropertyChanged(() => IsBusy); }
}
public TimeSpan Duration
{
get { return _Duration; }
set { _Duration = value; RaisePropertyChanged(() => Duration); }
}
public ICollectionView Products
{
get { return _Products; }
set { _Products = value; RaisePropertyChanged(() => Products); }
}
#endregion
#region Commands
public ICommand FetchProducts { get; set; }
#endregion
public MainViewModel()
{
//Inizializzo il comando
FetchProducts = new RelayCommand(() =>
{
//Imposto il busy indicator
IsBusy = true;
//Recupero l'orario corrente per campionamento
DateTime now = DateTime.Now;
//Invoco la funzione presente sul domain
_Domain.FetchProductsAsync(47, p =>
{
//Calcolo la durata dell'operazione
Duration = DateTime.Now.Subtract(now);
//Scorro tutti gli elementi recuperati e compongo i viewmodels
foreach (ProductModel model in p)
{
//Aggiungo alla lista il viewmodel creato
_ProductsSource.Add(new ProductViewModel
{
Name = model.Name,
Description = model.Description,
Quantity = model.Quantity
});
}
//Rimuovo il busy indicator
IsBusy = false;
});
});
//Inizializzo il domain per l'accesso al server
_Domain = new ServerDomain();
//Inizializzo la lista di elementi
_ProductsSource = new ObservableCollection<ProductViewModel>();
_Products = new PagedCollectionView(_ProductsSource);
}
}
}
]]></script>
Nel pieno rispetto del pattern MVVM ("Model-View-ViewModel") in Silverlight - non iniziate nemmeno ad usare Silverlight o WPF se non lo
conoscete - definisco le proprietà "osservabili" che mi servono per il binding con l'interfaccia XAML; definisco
le collezioni osservabili che ospiteranno i dati provenienti dal server, e definisco il "command" che esegue il
caricamento dei dati da remoto.
<br />
<br />
La parte succosa è "_Domain.FetchProductsAsync(..." che permette di invocare il metodo definito sul "ServerDomain",
passando una categoria con identificativo numerico "47" (la mia pigrizia continua), e gestendo il callback tramite una elegante
lamda expression, ricevente la lista di "ProductModel" deserializzati a partire dalla risposta JSON del server.
<br />
<br />
Il resto è solo paglia: composizione dei viewmodels a partire dai modelli ricevuti, e aggiunta alla collezione
in binding con una listview sulla UI. Per fare un pò di scena ho anche aggiunto una variabile per "misurare"
la durata dell'operazione di interazione con il server, visualizzando i risultati a video.
<br />
<br />
Magari, per esercizio, provare ad implementare il medesimo flusso logico usando i WCF Ria Services, confrontando
l'esecuzione delle due versioni e toccando con mano quanto questa soluzione sia più performante.
<br />
<br />
Detto questo, non è necessario che lo scenario sia applicato a tappeto sull'applicativo; ci possono essere situazioni
ibride, con "WCF Ria Services" e action MVC che lavorano contemporaneamente. L'unico accorgimento da considerare
è che le entità "gestite" da una piattaforma di servizi, avrà qualche difficoltà ad essere gestita con l'altra (per
esempio non potete "scaricare" un oggetto usando Mvc e rimandarlo al server, in "update", usando Ria). Ciononostante tutte queste
situazioni possono essere gestite con un po' di fantasia e usando nella maniera opportuna la "materia grigia"...
<br />
<br />
Stay tuned...
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-23963791927605278732013-06-28T15:40:00.001+02:002013-06-28T16:34:07.513+02:00Sfruttare le Action ASP.NET MVC da Silverlight<p dir=ltr>Sto scrivendo questo post da San Leonardo in Passiria (in Südtirol...uno dei posti piú belli al mondo, a mio parere), usando il mio smartphone; ogni giorno mi sorprendo sempre di più come l'informatica fa passi da gigante, come nascono nuove tecnologie o nuove tecniche per sfruttare meglio quelle esistenti. E in questo post, nel mio piccolo, voglio condividere la mia recente esperienza in questo senso.</p>
<p dir=ltr>Poche settimane fa stavamo lavorando ad una nuova versione di un applicativo rilasciato inizialmente nel 2008, basato su Microsoft Silverlight e su WCF Ria Services; naturalmente la primissima versione era basata su SL2 e .NET DataServices, poi ripetutamente aggiornato a SL3/4 e WCF Ria Services. Le revisioni successive non hanno minimamente modificato l'approccio architetturale iniziale, pur introducendo quanto di meglio era disponibile per semplificare la vita dello sviluppatore.</p>
<p dir=ltr>Ma con il crescere dei dati gestititi, nel corso degli anni, il sistema ha sempre piú dato segni di soffrire sul lato performance. Benché il tutto fosse stato fatto nella maniera piú corretta, il colloquio tra lo "smart client" Silverlight e la piattaforma di servizi lato server, sembrava essere il vero collo di bottiglia.</p>
<p dir=ltr>In tutta onestà c'é da dire che Silverlight, ed in particolare i Ria Services funzionano molto bene se la "line of business" comprende l'accoppiamento lato server con Entity Framework: tutto é perfettamente integrato e funzionale.</p>
<p dir=ltr>In questa particolare situazione si era scelto di gestire il layer di accesso ai dati usando un altro OR/M, quindi non si sono rispettati i dettami Microsoft, per una serie di motivi che non vi sto a raccontare. </p>
<p dir=ltr>Detto questo, per appurare che il reale problema di lentezza fosse nel processing dei Ria Services, si é fatto un piccolo esperimento: lato server si é creato un metodo IQueryable che emetteva una lista di oggetti "Poco" hard-coded; quindi si é misurato il tempo in cui gli stessi raggiungevano il punto di debug posto sullo smartclient Silverlight. I risultati non sono stati incoraggianti; tanto piú che sembravano deteriorare man mano che i Ria Services mantenevano il tracking di una quantitá di dati sempre maggiore.</p>
<p dir=ltr>E qui finalmente torniamo al tema del post: perché devo essere "obbligato" ad usare una tecnologia (i Ria Services) se non la sfrutto appieno e, sopratutto, peggiora le performance generali del mio sistema?</p>
<p dir=ltr>Con "non sfruttare appieno" le possibilitá della tecnologia, mi riferisco proprio al tracking dello stato delle entitá inviate dal server al client; la capacità dei Ria Services di determinare, in ogni momento, se un particolare oggetto é stato creato modificato o eliminato da operazioni client non ancora "commitate" sul server.</p>
<p dir=ltr>Personalmente ho sempre pensato che strumenti automatici o semi-automatici abbiano sempre avuto una valenza enorme nel lavoro di uno sviluppatore; specialmente se si voleva essere un "utilizzatore" di una tecnologia piuttosto che un esperto. Recentemente ho cambiato totalmente idea; ritengo assolutamente giusta l'asserzione "Never protect the developer", lasciando quindi l'onere di capire veramente una tecnica, prima di utilizzarla. </p>
<p dir=ltr>Perché devo affidarmi ad un sistema automatico che fa un sacco di cose in background quando io ho solo bisogno di un "attrezzo" che trasporti dal server al client (e viceversa) un set di oggetti?</p>
<p dir=ltr>Togliendo dall'equazione i Ria Services ed affidandomi ad un piú "snello" trio Mvc, Json e WebClient, ho raggiunto il mio scopo.</p>
<p dir=ltr>Come? Brevemente...<br>
Creo un' applicazione server basata su ASP .NET MVC (consiglio almeno la versione 3.0), che espone una serie di Controllers; questi ultimi contengono Actions che emettono i nostri oggetti "Poco", in formato Json. Le action devono essere organizzate in maniera adeguata per permettere la comunicazione bidirezionale: alcune rispondenti a verb GET (per l'emissione dei dati), altre a POST (per accettare oggetti Poco in ingresso e ricevere i dati).</p>
<p dir=ltr>Sul client useremo WebClient per invocare queste action presenti sul server, naturalmente usando l'opportuno verb.</p>
<p dir=ltr>Per la serializzazione e deserializzazione dei dati ricevuti ed inviati, faremo uso della libreria opensource Json.net (la migliore in assoluto per questo scopo, a mio parere).</p>
<p dir=ltr>Il resto della struttura é solo un po' di organizzazione per permettere di interagire con il server (dal client) in maniera coerente e omogenea. In questo senso, saró piú prolisso nel prossimo post...</p>
<p dir=ltr>Se riuscite anche solo ad immaginare il risultato, vi posso garantire che il problema delle performance é stato totalmente risolto. Pensate solo che una operazione, che Con vecchio sistema impiegava circa 10-12 secondi per essere completata, scende a 0,3 secondi...</p>
<p dir=ltr>Nessuna magia; solo la consapevolezza di aver risolto un problema (il trasporto dei dati) in maniera piú semplice e lineare,  scomponendolo nelle sue problematiche di dettaglio, ed applicando il giusto strumento per ciascuna di esse.</p>
<p dir=ltr>Detto cinese:<br>
"Regalate un pesce ad un affamato e lo avrete salvato per un giorno; insegnategli ad usare una canna da pesca, e non avrá mai piú fame"</p>
<p dir=ltr>M.</p>
Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com2tag:blogger.com,1999:blog-6432843868391268534.post-15341854795411176002013-05-18T15:22:00.000+02:002013-05-18T15:22:00.546+02:00Sfruttare IDisposable per realizzare un misuratore di tempistiche di esecuzione (Parte 2)Dicevamo (nel <a href="http://zenprogramming-it.blogspot.it/2013/04/sfruttare-idisposable-per-realizzare-un.html">post precedente</a>)...
<br />
L'interfaccia IDisposable ci permette di controllare il ciclo di vita di una classe, dal suo istanziamento, fino alla completa distruzione; l'uso congiunto di questa interfaccia (o meglio, di classi che implementano questa interfaccia) e la keywork "using", ci permette di controllare questo ciclo vitale in maniera molto più rigida, ottenendo quindi un controllo completo sulla classenquando essa viene effettivamente dealocata dalla memoria del processo.
<br />
<br />
Partendo dall'helper "Tracer", che funge da "entry-point" della funzionalità che stiamo realizzando, possiamo accedere al metodo che ci permetterà di generare una nuova instanza della classe materialmente responsabile dell'attività.
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
namespace ZenProgramming.Diagnostic.Tracer
{
/// <summary>
/// Represents helper for tracing
/// </summary>
public static class Tracer
{
/// <summary>
/// Measures duration of operation contained in "using" statement
/// </summary>
/// <param name="message">Message</param>
/// <param name="parameters">Format parameters</param>
/// <returns>Returns tracer duration instance</returns>
public static TraceDuration Duration(string message, params object[] parameters)
{
//Creo un nuovo oggetto che implementa IDisposable
return new TraceDuration(message, parameters);
}
}
}
]]></script>
La banalità dell'implementazione del metodo lascia veramente pochissimo spazio ai commenti: un semplice passaggio dei parametri previsti dal metodo (uno dei quali opzionale), iniettati immediamente nel costrutture della nuova istanza di "TraceDuration", poi emessa al chiamante.
<br />
<br />
L'implementazione reale di "TraceDuration" è certamente più interessante. Faccio subito notare che il codice riportato è puramente esemplificativo, senza fronzoli. Ci sono tantissimi modi per migliorare la qualità e la flessibilità della classe, magari facendo largo uso di tecniche quali IoC e D/I (di cui abbiamo già parlato in post precedenti).
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
namespace ZenProgramming.Diagnostic.Tracer
{
/// <summary>
/// Traces duration of operations between costructor and dispose of current class
/// </summary>
public class TraceDuration : IDisposable
{
#region Private fields
private string _Message;
private object[] _Parameters;
#endregion
#region Public properties
/// <summary>
/// Start time
/// </summary>
public DateTime Start { get; private set; }
/// <summary>
/// End time
/// </summary>
public DateTime End { get; private set; }
#endregion
/// <summary>
/// Constructor
/// </summary>
/// <param name="message">Message to trace</param>
/// <param name="parameters">Optional parameters</param>
internal TraceDuration(string message, params object[] parameters)
{
//Valorizzo l'orario di inizio
Start = DateTime.Now;
//Imposto le proprietà locali
_Message = message;
_Parameters = parameters;
//Modifico il messaggio aggiungendo la durata
string outMessage = string.Format("{0} - {1} -> Duration start...",
DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"), message);
//Scrivo il messaggio di inizio tracciamento
Console.WriteLine(outMessage, parameters);
}
/// <summary>
/// Release local resources
/// </summary>
public void Dispose()
{
//Valorizzo l'orario di fine
End = DateTime.Now;
//Modifico il messaggio aggiungendo la durata
string outMessage = string.Format("{0} - {1} -> Duration end. {2}",
DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"), _Message, End.Subtract(Start));
//Scrivo il messaggio di fine tracciamento
Console.WriteLine(outMessage, _Parameters);
}
}
}
]]></script>
Prima di tutto definiamo l'implementazione di "IDisposable" (come più volte già anticipato) per assicurarci i benefici derivanti dal suo utilizzo in combinazione a "using".
<br />
<br />
Poi avremo cura che tale classe sia istanziata solo all'interno dell'assembly in cui è definito l'helper "Tracer", marcando il costruttore come "internal"; "finezza", questa mirata unicamente a guidare l'utilizzatore affinchè sfrutti sempre il punto di ingresso che abbiamo studiato per la funzionalità.
<br />
<br />
Il costruttore rappresenta il momento temporale in cui iniziare la nostra misurazione della durata; quindi, dopo aver opportunamente salvato i parametri di ingresso della classe e l'orario corrente, emettiamo su standard output (standard console) il messaggio utente con i dettagli testuali di inizio del tracciamento.
<br />
<br />
L'unico altro metodo contenuto in "TraceDuration" è quello che l'interfaccia implementata ci obbliga a costruire: "Dispose". Esso viene invocato quando il costrutto "using" cessa il suo "scope", cioè quando il codice che vogliamo misurare è stato completamente eseguito: un paio di calcoli elementari per ottere la durata dell'operazione, seguiti dalla composizione del messaggio di terminazione, ed ancora lo standard output che aiuta ad interagire con l'utente finale...
<br />
<br />
Poche righe di codice ed il gioco è fatto: semplice, elegante e sintetico...
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-19683776693791933522013-04-29T21:16:00.000+02:002013-05-17T13:42:36.392+02:00Sfruttare IDisposable per realizzare un misuratore di tempistiche di esecuzione (Parte 1)Sicuramente almeno una volta nella vostra carriera di sviluppatori vi sarà capitato di dover misurare la durata di esecuzione di un'istruzione oppure di un gruppo di istruzioni. La cosa più semplice da fare è dichiarare due variabili di tipo "DateTime", dove saranno registrate la data di inizio dell'operazione, e la data di fine; quindi eseguire una banale "end.Subtract(start)" per ottenere il "TimeSpan" che rappresenta l'effettiva misurazione della durata.
<br />
<br />
Non vi ho detto niente di nuovo, quindi. Ma ammettiamo che dobbiate misurare diverse porzioni e funzioni del vostro sistema. Se poi si tratta di un sistema che processa "batch" un quantitativo di dati considerevole, la cosa è un tantino dispersiva, e si rischia di sporcare tutto il codice con variabili come quelle sopra citate.
<br />
<br />
La domanda che mi sono posto è la seguente: ho modo di ottenere il medesimo risultato evitando di inquinare il mio codice con vagonate di istruzioni che hanno il solo scopo di verificare le performance applicative, ma nessun significato a livello di flusso funzionale?
<br />
<br />
La prima risposta che mi sono dato è : "No, non è possibile eliminare del tutto il codice di misurazione.". E, a pensarci bene, è comprensibile che alcuni "marker" debbano necessariamente essere incorporati nel codice per permettere di identificare il punto di inizio e quello di fine della misurazione. Esistono alcuni strumenti come "PostSharp" che eseguono una ricompilazione del nostro sorgente inserendo altro codice prima di compilare il tutto in "IL", ma non è questo lo scopo di questo post (anche se ritengo PostSharp uno strumento estremamente valido).
<br />
<br />
Prima di buttarmi a capofitto nella questione, ho "scomposto" il problema: avevo bisogno di una rilevazione temporale prima dell'istruzione da misurare, e una dopo l'esecuzione di quest'ultima. Al termine della rilevazione, per semplicità di spiegazione, diciamo che vogliamo stampare sulla console il tempo di esecuzione corredato di un messaggio che ci permetta di identificare facilmente "cosa abbiamo misurato".
<br />
<br />
Ammettiamo quindi di voler ottenere un'uscita del tipo:
<br />
"13/03/2013 12:32:23 - 'Estrazione prodotti' (START)",
<br />
"13/03/2013 12:36:25 - 'Estrazione prodotti' (END -> Duration 00:04:02)"
<br />
rispettivamente per l'inizio della misurazione e al completamento di questa ipotetica estrazione di prodotti da una base dati.
<br />
<br />
Quindi ho un inizio e ho una fine...Perchè non usare l'interfaccia "IDisposable" per realizzare qualche cosa cosa di sintetico ed efficace? L'utilizzo di quello che andremo a realizzare è presentato qui sotto: una semplice dichiarazione di "using" sull'helper statico "Tracer" (specificando il messaggio) e il gioco è fatto.
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System.Collections.Generic;
namespace ZenProgramming.Diagnostic.Tracer.Maintenance
{
public static class TracerTest
{
public static void ExecuteTraceTest()
{
//Traccio la durata dell'estrazione dei prodotti
using (Tracer.Duration("Products extraction..."))
{
//Lancio l'estrazione dei prodotti dalla base dati
IList<Product> products = ProductRepository.FetchAll();
}
}
}
}
]]></script>
Per fare tutto ciò, come detto, è necessario che la chiamata al metodo "Duration" emetta un oggetto che implementa l'interfaccia "IDisposable", che come sappiamo è facilmente utilizzabile con la clausola di "using": l'oggetto viene istanziato nella definizione, quindi rilasciato alla chiusura del "bracket" di competenza dello "using" stesso.
<br />
<br />
Volere vedere il contenuto di "Duration" e l'implementazione reale di "IDisposable"?
<br />
La prossima volta... ;)
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0tag:blogger.com,1999:blog-6432843868391268534.post-76574108946923078402013-04-20T15:32:00.000+02:002013-04-20T15:41:39.415+02:00Maggiore espressività in un'applicazione consoleNell'ultimo periodo mi sono concentrato non su architetture tradizionali (web o desktop), ma su processori ed elaboratori che macinano grossissime quantità di informazioni in background in modo asincrono.
<br />
<br />
In questo genere di progetti, l'interazione utente non è la cosa più importante; lo sono invece la gestione degli errori, il tracciamento dei vari step applicativi in tutte le fasi dell'elaborazione e, più di tutto, le prestazioni. Lo scopo finale è ottenere un sistema che sia in grado di completare il workflow attraverso una sequenza ben definita di semplici passi logici, tutti tracciabili e ben evidenti anche in fase di analisi dei log.
<br />
<br />
Un'applicazione console è la scelta più indicata per iniziare a sviluppare (e testare) un elaboratore massivo, perchè rimuove i "fronzoli" di un sistema tradizionale, permettendo di concentrarsi sul lavoro che deve essere portato a termine.
<br />
<br />
Ben presto ci si rende però conto che la semplice "WriteLine" della console di .NET non è così tanto "espressiva", e spesso si fa molta fatica a riconoscere nell'enorme sequenza di stringhe di testo che scorre sullo schermo, i messaggi rilevanti. In particolare è molto complesso evidenziare eventuali errori rilevati a "runtime", oppure step intermedi di elaborazione completati correttamente.
<br />
<br />
Non volendo passare ad un'applicazione desktop per risolvere la questione, mi sono chiesto se fosse possibile utilizzare in maniera più costruttiva il "minimalismo" della standard console. Dopo qualche ricerca in rete, ho individuato un'opzione dello "standard output" che non conoscevo; in ogni istante è possibile modificare il colore del "foreground" semplicemente intervenendo sulla proprietà statica di "Console".
<br />
<br />
Il meccanismo è in realtà molto semplice, ma richiede un paio di righe di codice che, sparse per un'applicazione tradizionale, risultano ripetitive. Quindi ho realizzato un semplice helper, che vi riporto qui sotto.
<script class="brush: csharp" type="syntaxhighlighter"><![CDATA[
using System;
using System.Linq;
namespace ZenProgramming.Chakra.Utilities.Server
{
/// <summary>
/// Utilities class used to write to standard output
/// </summary>
public static class ConsoleUtils
{
/// <summary>
/// Execute write of a colored line on the standard console
/// </summary>
/// <param name="color">Color of message</param>
/// <param name="message">Message to write</param>
/// <param name="formatParams">Format parameters</param>
public static void WriteColorLine(ConsoleColor color, string message, params object[] formatParams)
{
//Eseguo le memorizzazione del vecchio colore e imposto il nuovo
var oldColor = Console.ForegroundColor;
Console.ForegroundColor = color;
//Eseguo la scrittura delle informazioni sulla console
Console.WriteLine(message, formatParams);
//Reimposto il vecchio colore
Console.ForegroundColor = oldColor;
}
}
}
]]></script>
Queste poche e semplici righe di codice, ve lo posso assicurare, hanno contribuito ad aumentare esponenzialmente l'espressività della mia soluzione; niente di trascendentale, devo ammetterlo, ma mi ha aiutato ad identificare facilmente eventuali errori scaturiti dal processo, oppure monitorare facilmente l'avanzamento dell'intero flusso. Spero possa essere utile anche a voi...
<br />
<br />
M.Mauro Bussinihttp://www.blogger.com/profile/09456594490324352110noreply@blogger.com0