Category Archives: .NET Framework

Code Contracts – Slides a dema [TechEd Praha 2010]

Slides a dema z přednášky na konferenci TechEd Praha 2010:

Záznam z přednášky nebyl pořizován.

ClickOnce – spouštění při startu Windows, dvojí konfigurace a podobné záludnosti

Trochu nedobrovolně jsem byl dnes donucen šťourat se podrobněji v ClickOnce útrobách, přestože to není moje oblast zájmu.

Symptomy problému byly zajímavé – aplikace instalovaná pomocí ClickOnce, která v sobě měla volbu „Automaticky spouštět při startu Windows“, se chovala divně. Zprvu se zdálo, že nastavení provedená v aplikaci (.NET Settings mechanizmus), se restartem počítače ztratí. Nakonec jsem diagnostikoval, že ve skutečnosti aplikace používá dvoje nastavení – jedna nastavení používaná po instalaci a při spuštění aplikace automaticky vytvořenou položkou z menu Start, druhá nastavení používaná při automatickém spuštění po startu Windows. Zároveň jsem v C:\Users\… našel v profilu uživatele dva různé user.config soubory, které se používaly pro tyto dva scénáře spuštění aplikace.

Chyba byla ve skutečnosti v nekoretním spouštění aplikace po startu Windows. ClickOnce totiž sám automatické spouštění po startu Windows nepodporuje a při vlastní implementaci nebyl na rozdíl od zástupce v menu Start totiž použit link typu .asppref-ms (link soubor pro spouštění ClickOnce apliace, který zajišťuje i aktualizace a další ClickOnce věci), ale nekorektně aplikace pro spuštění po startu Windows používala přímo odkaz na svůj .exe soubor. To způsobilo nejen použití jiného user.config souboru, ale i nefunkčnost automatických aktualizací a dalších vlastností ClickOnce.

Poučení tedy zní: Pokud chcete korektně spouštět ClickOnce aplikaci, je potřeba volat vytvořený soubor .appref-ms (vytvořený ClickOnce instalátorem do menu Start) a nikoliv se odkazovat na .exe soubor aplikace.

Chybný kód tak může vypadat:

Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true)
.SetValue("InformTrayReader", Application.ExecutablePath);

Zatímco „správně“ (při dalších omezeních) by to mohlo vypadat nějak takto:

Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true)
.SetValue("InformTrayReader", GetApplicationStartupLink());

private object GetApplicationStartupLink()
{
    const string productName = "název dle ClickOnce Publish nastavení";
    const string publisherName = "nastavení dle ClickOnce Publish nastavení";

    string allProgramsPath = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
    string shortcutPath = Path.Combine(allProgramsPath, publisherName);

    return Path.Combine(shortcutPath, productName) + ".appref-ms";
}

Viz též související info:

UPDATE pro Windows Vista

Pokud nedejbože potřebujete, aby to fungovalo pro nějakého nebožáka používajícího alfa-verzi od Windows7 zvanou Windows Vista (dodnes nechápu, jak mohl Microsoft něco takového vydat), potom je nutný ještě jeden workaround.

Visty totiž nepustí .appref-ms přímo, a to ani z registrů, ani kdybyste odkaz dopravili do Start/Startup. Funguje v nich ale spouštění .exe a kupodivu lze .appref-ms zavolat z našeho exe a spustí se. Stačí tedy našemu programu přidat přepínač příkazové řádky -LaunchClickOnce a při jeho zjištění v Main() místo aplikace samotné odstartovat jen .appref-ms a sám se ukončit.

Podrobnější popis s ukázkou konkrétního kódu najdete na stránce http://www.brokenwire.net/bw/Programming/116/run-clickonce-app-on-startup.

web.config transformace a Code Contracts – Dema [MS DevDays 2009]

Dema k mým ukázkám na konferenci MS DevDays 2009:

Záznam z vystoupení nebyl pořizován, na téma web.config transformací však doporučuji svůj screencast.

Generátor klíčů GP WebPay (PayMuzo) hlásí chybu „Illegal key size“

Při poslední z implementací GP WebPay (PayMuzo) se nám stalo, že nám generátor klíčů obchodníka zahlásil chybu

exception encrypting data - java.security.InvalidKeyException: Illegal key size

Po chvilce testování (na dvou různých PC) jsme zjistili, že to dělá při Heslu pro keystore delším 8 a více znaků. Pokud jsme heslo zkrátili na 7 a méně znaků, vygenerování klíčů proběhlo úspěšně.

Verzi utility lze popsat jako „(c) 2004/2005 Muzo a.s.“ v UI a velikost souboru GenCert.jar 33567 bytů, víc mě nad tím bádat nebaví.

Pokud byste někdo bojoval s implementací GP WebPay pro .NET, tak se mi klidně ozvěte, máme to hotový a určitě se nějak dohodneme… ;-)

X509Certificate2: CryptographicException: The system cannot find the file specified.

Pokud vytváříte ve webové aplikaci X509Certificate2

myCertificate = new X509Certificate2(rawData, password);
//nebo
myCertificate = new X509Certificate2(fileName, password);

pak můžete být po nasazení své funkční aplikace na produkční server (např. hosting) obdařeni výjimkou

System.Security.Cryptography.CryptographicException: The system cannot find the file specified.
at System.Security.Cryptography.CryptographicException.ThrowCryptogaphicException(Int32 hr)
at System.Security.Cryptography.X509Certificates.X509Utils._LoadCertFromBlob(Byte[] rawData, IntPtr password, UInt32 dwFlags, Boolean persistKeySet, SafeCertContextHandle& pCertCtx)
at System.Security.Cryptography.X509Certificates.X509Certificate.LoadCertificateFromBlob(Byte[] rawData, Object password, X509KeyStorageFlags keyStorageFlags)
at System.Security.Cryptography.X509Certificates.X509Certificate2..ctor(Byte[] rawData, String password)

…přestože soubor s certifikátem buď zaručeně existuje (fileName), nebo se ani nepoužívá (rawData).

Problém souvisí s tím, že v produkčním prostředí Vaše aplikace beží pod uživatelským účtem, který nemá založen profil, který se má certifikát vytvořit (volně přeloženo, omlouvám se za případnou nepřesnost).

Řešením je použití přetížení constructoru, kterým zvolíte cílové uložiště certifikátu:

 

myCertificate = new X509Certificate2(rawData, password, X509KeyStorageFlags.MachineKeySet);
//nebo
myCertificate = new X509Certificate2(fileName, password, X509KeyStorageFlags.MachineKeySet);

Nastavení Image.Source z kódu

myImage.Source = new BitmapImage(new Uri("images/myImage.jpg", UriKind.Relative));

Na lokále mi .NET aplikace chodí, ale při spouštění ze síťové cesty padá na SecurityException

Problém, který poprvé zaskočí snad každého .NET programátora. Ani já nebyl kdysi výjimkou a tuto otázku dostávám stále dokola.

Symptom je jednoduchý – při spouštění .NET aplikace z lokálního počítače vše krásně funguje, při spouštění přes síť (např. po deploymentu do sdílené složky) aplikace padá na SecurityException.

Jádrem celého problému je .NETí mechanizmus Code Access Security Policy, který má mj. chránit hostující počítač před neoprávněným spouštěním kódu, popř. před závadným kódem. Stručně řečeno tento mechanizmus používá pro řízení práv k provádění jednotlivých operací sadu faktů zvanou Evidence (důkazy), přičemž jeden z podstatných faktů je zóna, z které je kód spouštěn (jde o klasické zóny Můj počítač, Lokální intranet, Internet, Důveryhodné servery, …). No a výchozí nastavení na hostujícím počítači po instalací .NET frameworku říká, že kód spouštěný ze zón Intranet/Internet má určitá omezení, např. nesmí přistupovat k souborovému systému, či nesmí pracovat se sítí (SQL serverem), atp.

V podstatě jsou dvě cesty k řešení:

  1. Rozšířit sadu Evidence aplikace tak, aby ve výchozím nastavení .NET frameworku měla aplikace práva lepší. Základem je např. podepsat aplikaci (strong-name), atp.
  2. Změnit výchozí nastavení zabezpečení .NET frameworku na hostujícím počítači tak, aby i aplikace z méně bezpečných zón směly provádět zabezpečené operace. Což provedeme:
    1. Spustíme MMC konzoli pro konfiguraci .NET Frameworku (z Nástrojů pro správu, popř. z Run mmc.exe a pak Přidat modul snap-in…)
    2. Donavigujeme se ve stromu na My Computer ~ Runtime Security Policty
    3. dáme Adjust Zone Security
    4. Make changes to this computer / to the current user only (dle potřeby)
    5. Vybereme zónu a šoupátkem zvýšíme její „trust“. Full Trust znamená „bez omezení“.

Pozor, neměňte bezhlavě tato nastavení. Týkají se totiž i kódu spouštěného z internetu, např. i přímo z webových stránek. Můžete tak nechtěně otevřít bránu do svého počítače i pro nežádoucí kód.

Správná cesta k řešení vede přes bod 1), nicméně připouštím, že pro začínajícího programátora a jednoduché síťové utilitky je to cesta složitější.

VasekB doplnil

lze to povolit i takto:

%SystemRoot%\Microsoft.NET\Framework\v1.1.4322\caspol.exe -q -f -cg 1.2 FullTrust

nastaveni se ulozi tady:

%SystemRoot%\Microsoft.NET\Framework\v1.1.4322\CONFIG\security.config

Jak podepsat tu aplikaci?

Musíte si nejprve vytvořit podpisový klíč, obvykle pomocí utility SN, spusťte „sn -k my.key“. Potom tento klíč připojíte ke své assembly pomocí atributu [assembly: AssemblyKeyFile(„C:\Path\my.key“)].  Obvykle se tyto atributy zapisují do souboru AssemblyInfo.cs, který lze navíc editovat pomocí UI VisualStudia (vlastnosti projektu).

Jednoduchý exception logging pomocí Trace/TraceSource mechanizmů .NET

Pokud se sháníte po jednoduchém mechanizmu, jak ve Vašich aplikacích logovat výjimky, pak zřejmě v .NET pro consolové/WinForm aplikace marně hledáte něco, jako je healthMonitoring pro ASP.NET, kde je připraven jednoduchý, ale mocný nástroj a lze pomocí pouhých několika řádků ve web.config souboru získat přehled o výjimkách ve Vaší aplikaci, např.:

<configuration>
    <system.web>
        <healthMonitoring enabled="true">
            <rules>
                <add name="Mail Notifications on All Errors" eventName="All Errors" provider="SimpleMailWebEventProvider" profile="Default"/>
            </rules>
            <providers>
                <add name="SimpleMailWebEventProvider" type="System.Web.Management.SimpleMailWebEventProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A" from="errors@havit.cz" to="errors@example.cz" subjectPrefix="MyApplication: " buffer="true" bufferMode="Notification" maxEventLength="4096" maxMessagesPerNotification="2"/>
            </providers>
        </healthMonitoring>
    </system.web>
</configuration>

…výše uvedená konfigurace Vám zajistí zasílání mailových notifikacích o neobsloužených výjimkách a chyách Vaší aplikace, jednoduché, prosté.

Jak na to u consolových a WinForm aplikací?

Pro consolové či WinForm aplikace nic jako healthMonitoring připraveno není, nicméně s pár řádky kódu lze dosáhnout obdobného efektu a dokonce u toho využít široké možnosti pro tracing v .NET 2.0. Základní myšlenka je prostá:

  • .NET 2.0+ v sobě obsahuje široké možnosti pro tracing, kdy můžeme definovat různé zdroje zpráv (TraceSource), zprávy různé rozdělovat (Switch), filtrovat (TraceFilter) a směrovat na různé listenery (výstup do konzole, XML, textového souboru, event-logu, SQL, SMTP, atd. další si můžeme napsat) – to vše bez nutnosti jakýchkoliv doplňků – ať už Microsoftích Enterprise Library / Application Blocků, či produktů třetích stran typu Log4Net. Základní přehled o možnostech si můžeme udělat například z článku A Tracing Primer – Part I [Mike Rousos].
  • .NET sám nemá mechanizmus, jak výjimky zapisovat do trace
  • napsat pár řádků kódu, které zajistí zápis výjimek do trace je snadné!!!
  • do trace můžeme buď výjimky posílat explicitně (níže volání ExceptionTracer.TraceException(myException)),
  • nebo se můžeme přihlásit k odběru událostí, které neošetřené výjimky způsobují,
    • obecně jde o událost AppDomain.CurrentDomain.UnhandledException
    • ve WinForm aplikacích pak událost Application.ThreadException

Interface pro nasazení?  …prostý

V nejjednodušší variantě si můžeme udělat například třídu ExceptionTracer, kterou přihlásíme k odběru příslušných událostí a v obsluze událostí už budeme jen výjimku posílat do trace.

Pro konzolové aplikace:

class Program
{
    static void Main(string[] args)
    {
        ExceptionTracer.SubscribeToUnhandledExceptions();

        throw new InvalidOperationException("Chybka!");

        // ExceptionTracer.Default.TraceException(new ArgumentNullException("param", "Sakrapísek!"));
    }
}

Pro WinFormAplikace:

static class Program
{
    [STAThread]
    static void Main()
    {
        ExceptionTracer.SubscribeToWindowsFormsThreadExceptions();

        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}

App.config pak může vypadat nějak takto:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.diagnostics>
        <sources>
            <source name="Exceptions" switchValue="Error">
                <listeners>
                    <add name="LogFileListener"
                        type="System.Diagnostics.TextWriterTraceListener"
                         initializeData="Exceptions.log"
                    />
                    <add name="XmlListener"
                         initializeData="Exceptions.xml"
                         type="System.Diagnostics.XmlWriterTraceListener"
                    />
                </listeners>
            </source>
        </sources>
    </system.diagnostics>
</configuration>

ExceptionTracer [Simplified Version]

Jednoduchá, ale plně funkční verze ExceptionTracekru může vypadat třeba takto:

 

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Windows.Forms;

namespace Havit.Diagnostics
{
    public class ExceptionTracer
    {
        /// <summary>
        /// Přihlásí ExceptionTracer k odběru všech neobsloužených výjimek (event AppDomain.CurrentDomain.UnhandledException).
        /// </summary>
        public static void SubscribeToUnhandledExceptions()
        {
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
        }
        private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            if (e.ExceptionObject is Exception)
            {
                TraceException((Exception)e.ExceptionObject);
            }
        }

        /// <summary>
        /// Přihlásí ExceptionTracer k odběru všech neobsloužených výjimek WinForm (event Application.ThreadException).
        /// </summary>
        public static void SubscribeToWindowsFormsThreadExceptions()
        {
            Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
        }
        private static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
        {
            TraceException(e.Exception);

            // původní implementace obsluhy výjimky, WinForm dialog s chybou
            using (ThreadExceptionDialog excptDlg = new ThreadExceptionDialog(e.Exception))
            {
                DialogResult result = excptDlg.ShowDialog();
                if (result == DialogResult.Abort)
                {
                    Application.Exit();
                }
            }
        }

        /// <summary>
        /// Pošle do trace zadanou výjimku.
        /// </summary>
        /// <param name="exception">výjimka k zaznamenání</param>
        public static void TraceException(Exception exception)
        {
            TraceSource ts = new TraceSource("Exceptions");

            ts.TraceEvent(TraceEventType.Critical, 0, exception.ToString());

            ts.Flush();
            ts.Close();
        }
    }
}

Příklad výstupu [XML]

<E2ETraceEvent xmlns="http://schemas.microsoft.com/2004/06/E2ETraceEvent">
<System xmlns="http://schemas.microsoft.com/2004/06/windows/eventlog/system">
  <EventID>0</EventID> 
  <Type>3</Type> 
  <SubType Name="Critical">0</SubType> 
  <Level>1</Level> 
  <TimeCreated SystemTime="2008-01-31T21:08:35.0625000Z" /> 
  <Source Name="Exceptions" /> 
  <Correlation ActivityID="{00000000-0000-0000-0000-000000000000}" /> 
  <Execution ProcessName="WindowsFormsApplication1" ProcessID="1232" ThreadID="1" /> 
  <Channel /> 
  <Computer>OSKAR</Computer> 
</System>
<ApplicationData>System.ApplicationException: Test !!! at WindowsFormsApplication1.Form1.button1_Click(Object sender, EventArgs e) in D:\Development\ExceptionLogging\WindowsFormsApplication1\Form1.cs:line 20 at System.Windows.Forms.Control.OnClick(EventArgs e) at System.Windows.Forms.Button.OnClick(EventArgs e) at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent) at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks) at System.Windows.Forms.Control.WndProc(Message& m) at System.Windows.Forms.ButtonBase.WndProc(Message& m) at System.Windows.Forms.Button.WndProc(Message& m) at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m) at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m) at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)</ApplicationData> 
</E2ETraceEvent>

Použití C# 3.0 pro runtime .NET Framework 2.0

Pokud vyvíjíte aplikaci ve Visual Studiu 2008 a aplikace je určena pro runtime .NET Framework 2.0, nic vám nebrání použít nové charakteristiky jazyka C# 3.0. Úžasné je, že pro to nemusíte udělat vůbec nic.
Není možné pochopitelně použít ty rysy jazyka, které vyžadují knihovny instalované .NET Frameworkem 3.5. Můžete využít například eleganci lambda výrazů nebo zkráceného zápisu vlastností, zapomeňte ale na LINQ.

Programátorská hádanka – catch & throw

Jaký je rozdíl v prapagaci výjimky z bloku catch:

try
{
...
}
catch (Exception e)
{
   throw;
}

versus

try
{
...
}
catch (Exception e)
{
   throw e;
}

Odpověď je tentokrát jednoduchá a jako obvykle ji najdete o řádku níže napsanou bílým písmem (pro odtajnění třeba označte):

>>> Při vyhození výjimkt přes „throw;“ se nezmění stack trace – výjimka se stále tváří, že vzešla z původní metody. Při vyhození přes „throw e;“ je změněn stack trace, výjimka se tváří, že vzešla z naší metody obsluhující výjimku. <<<