Category Archives: Development

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.

Jak přepsat PageStatePersister

 

Jak již bylo napsáno v jiném našem článku, vlastnost PageStatePersister slouží k získání objektu, který zajišťuje uložení ViewState. Článek detailně popisuje použití třídy SessionPageStatePersister, která zajišťuje uložení ViewState do Session.

Chybná implementace

Pokud chceme použít SessionPageStatePersister bez konfigurace pomocí adaptéru, musíme přepsat zmíněnou vlastnost, ideálně ve společném předkovi všech stránek v aplikaci. Nejjednodušší (avšak nesprávná implementace) může při každém dotazu na hodnotu vlastnosti vyrobit instanci persisteru a tu vrátit.

protected override PageStatePersister PageStatePersister
{
    get
    {
        // takto ne!
        new SessionPageStatePersister(this.Page);
    }
}
Symptomy

Dosáhneme sice požadovaného efektu – ViewState se ukládá do Session, ovšem v dříve funkčních částech aplikace začneme pozorovat nečekané efekty:

  1. Mezi postbacky se nezachovávají některé hodnoty ukládané do control state (například DataKeys u GridView).
  2. V některých velmi specifických případech se neprovede nastavení hodnoty do vlastnosti, pokud tato vlastnost již měla hodnotu nastavenou v souboru ASPX a hodnota vlastnosti se vnitřně ukládala do ViewState (po provedení příkazMujControl.Vlastnost = novaHodnota nebude hodnota vlastnosti změněna).
Správná implementace

Ačkoliv se to nedočteme v dokumentaci, ale zjistíme po bližším zkoumání .NET reflektorem, musíme v jedné instanci třídy Page vracet stále stejnou instanci, protože na hodnotu vlastnosti se opakovaně dotazují metody třídy Page (jednou z nich je metoda RegisterRequiresControlState, což by vysvětlovalo symptomy v bodu 1). Po zapracování tohoto poznatku jsme se zmíněných problémů zbavili. Správně je tedy třeba vlastnost PageStatePersister přepsat takto:

protected override PageStatePersister PageStatePersister
{
    get
    {
        if (_pageStatePersister == null)
        {
            _pageStatePersister = new SessionPageStatePersister(this.Page);
        }
        return _pageStatePersister;
    }
}
private PageStatePersister _pageStatePersister;

Zjištění volné paměti počítače na webhostingu

Pokud se chceme z naší hostované ASP.NET aplikace dozvědět něco o stroji, na kterém běží, nemůžeme tak zpravidla udělat přes WMI dotazy, či performance-countery, protože nemáme k dispozici účet s dostatečnými právy a dostáváme výjimku Access Denied.

I nám C#-istům je však k dispozici to, co mají ve VisualBasicu ukryto pod featurou My, konkrétně nás zde zajímá My.Computer.Info, které není nic jiného než instancí třídyMicrosoft.VisualBasic.Devices.ComputerInfo s propertama AvailablePhysicalMemory,AvailableVirtualMemory, TotalPhysicalMemory, TotalVirtualMemory a několika dalšími s informacemi o verzi OS atp.

Stačí si tedy do našeho C# projektu nareferencovat assembly Microsoft.VisualBasic, vytvořit si instanci třídy ComputerInfo a zeptat se jí na přísušné hodnoty:

using Microsoft.VisualBasic.Devices;

...

ComputerInfo computerInfo = new ComputerInfo();
InfoLb.Text = computerInfo.AvailablePhysicalMemory.ToString("n0");

Třída ComputerInfo používá volání podpůrných nativních metod .NET a informace o paměti stroje dostaneme i pod běžným hostingovým účtem.

Změna výchozí verze ASP.NET pro nové weby IIS

Pokud Vám IIS pro nově zakládané web-site přednastavuje jinou verzi ASP.NET, než si představujete, můžete toto výchozí nastavení ovlivnit spuštěním příkazu aspnet_regiis.exe od nově požadované verze ASP.NET s níže uvedenými parametry:

aspnet_regiis -sn W3SVC/

Utilitu aspnet_regiis najdete v C:\WINDOWS\Microsoft.NET\Framework\<číslo verze>\.

ViewState vs. fáze Init, aneb jak jsem se chytil

Na svém posledním ASP.NET kurzu jsem se báječně chytil na jednom primitivním demu na fungování ViewState. Krásně jsem předváděl, jak „ve fázi Init není ještě ViewState trackován, po fázi Init nastává LoadViewState a před tím se zapne jeho tracking – TrackViewState()“, nu což, vyrobil jsem si krásné demo a nestíhal jsem pak chvíli zírat.

(Základy fungování ViewState viz článek Co by měl každý vědět o ViewState.)

„Nefunkční“ demo

Vyrobil jsem primitivní stránku, s jedním Labelem a jedním tlačítkem.

<asp:Label ID="MyLabel" Text="Výchozí text" runat="server" />
<asp:Button ID="MyButton" Text="Postback" runat="server" />

a jak jsem se demonstrovat, jak přiřazení vlastnosti Text ve fázi Init nepřežije postback:

void Page_Init(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        MyLabel.Text = "Nová hodnota z fáze Init!";
    }
}

Uff, jaké bylo mé překvapení, když Label i po kliknutí na tlačítko Postback přežíval s hodnotou „Nová hodnota z fáze Init!“. Mnohým je hned jasné, kde jsem udělal chybu, ostatním přiblížím:

ViewState v životním cyklu stránek/controlů

Mé překvapení naštěstí netrvalo dlouho, rychle jsem si ověřil, kde přesně se volá v životním cyklu controlů TrackViewState() a rozuzlení bylo na světě:

  1. TrackViewState() není první operace po fázi Init, nýbrž poslední operace fáze Init samotné,
  2. fáze Init (jako jediná ze základních fází životního cyklu) probíhá zdola nahoru, procházením stromu control-tree do hloubky – fáze Init nadřazeného controlu je završena až po dokončení fází Init všech jeho child-controls.

Výsledek je tedy nasnadě, v obsluze události Page_Init jsme úplně na vrcholu control-tree a fáze Init jednotlivých controlů již proběhla. Jejich TrackViewState() již proběhl a ViewState controlů tak již sleduje změny jednotlivých hodnot. Nastavení MyLabel.Text ve fází Page_Init tedy již je operací, kdy změny ViewState labelu jsou sledovány, narozdíl od ViewState stránky (Page) samotné, kde ještě TrackViewState() neproběhl.

„Funkční“ demo

Demo jsem rychle opravil a vše krásně fungovalo, jak má:

void MyLabel_Init(object sender, EventArgs e)
{
    if (!Page.IsPostback)
    {
        this.Text = "Text nastavený ve fázi Init labelu, nepřežije Postback.";
    }
}

void Page_Init(object sender, EventArgs e)
{
    if (!IsPostback)
    {
        this.Title = "Title nastavený ve fázi Init page, nepřežije Postback.";
    }
}

Ve fázi Init samotného Labelu ještě jeho TrackViewState() neproběhl, tedy se Text nezachová.

Ve fázi Init stránky už proběhl TrackViewState() controlů, ale stránky samotné ještě ne, tedy třeba Title se nezachová.

Makra pro ExpandAllRegions, CollapseAllRegions (#region)

Již delší dobu používám ve Visual Studiu dvě makra – ExpandAllRegions a CollapseAllRegions, ke kterým mám přidružené klávesové zkratky Ctrl+Alt+NumPlus a Ctrl+Alt+NumMinus. Makra jsou prostá, rozbalí všechny regiony v kódu, či zabalí všechny #regiony v kódu (což se od doby, kdy si VS2005+ pamatuje poslední sbalení/rozbalení u každého souboru, narozdíl od předchozích verzí, kde se vždy vše dalo otevírat sbalené, ukázalo jako neobyčejně potřebnná funkce).

Makra jsou prostá:

Sub ExpandAllRegions()
    DTE.ExecuteCommand(&quot;Edit.StopOutlining&quot;)
    DTE.ExecuteCommand(&quot;Edit.StartAutomaticOutlining&quot;)
End Sub

Sub CollapseAllRegions()
    ExpandAllRegions()
    Dim objSelection As TextSelection
    objSelection = DTE.ActiveDocument.Selection
    objSelection.StartOfDocument()
    While (objSelection.FindText(&quot;#region&quot;))
        objSelection.StartOfLine(vsStartOfLineOptions.vsStartOfLineOptionsFirstColumn)
        DTE.ExecuteCommand(&quot;Edit.ToggleOutliningExpansion&quot;)
        objSelection.StartOfDocument()
    End While
    DTE.ActiveDocument.Selection.StartOfDocument()
End Sub

…a přiřazení klávesových zkratek je v Tools ~ Options ~ Environment ~ Keyboard ~ …a dále dohledat název makra dle procedury, takže např. Macros.MyMacros.Havit.CollapseAllRegions, atd.

Klientské (ne)cachování skriptů z WebResource.axd

Skripty emitované do stránky pomocí odkazů do assembly, pomocí WebResource.axd, se v browseru cachují pouze pokud máme debug-kompilaci vypnutou:

<compilation debug="false" />

NullReferenceException v DefaultWsdlHelpGenerator.aspx při přístupu k webovým službám

Vytváříme klasickou webovou službu publikovanou pomocí ASMX. Při pokusu o přístup k této webové službě přes internetový prohlížet obdržíme výjimku NullReferenceException – a to ještě žádnou metodu nevoláme, jen prohlížíme dostupné služby.
Debugger se nám zastavuje v souboru DefaultWsdlHelpGenerator.aspx na řádku 1335:

OperationBinding FindHttpBinding(string verb) {
        foreach (ServiceDescription description in serviceDescriptions) {
            foreach (Binding binding in description.Bindings) {
                HttpBinding httpBinding = (HttpBinding)binding.Extensions.Find(typeof(HttpBinding));

Vskutku pozoruhodné je pak řešení problému: Ve web.configu máme v sekci <pages> nastavení autoEventWireUp=“false“. Po odstranění tohoto nastavení přístup k dokumentaci webových služeb funguje.
DefaultWsdlHelpGenerator.aspx je generátor dokumentace pro zobrazení v prohlížeči. Ten se evidentně kompiluje s použitím nastavení aplikace a v kódu spoléhá na zavolání metody Page_Load pomocí automatického navázání vybraných událostí (AutoEventWireUp). K dispozici máme i zdrojový kód tohoto souboru, standardně se nachází v C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG.
(Runtime .NET Framework 2.0)

Workaround

Na vývojářské mašině se dá taky rovnou zeditovat soubor C:\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG\DefaultWsdlHelpGenerator.aspx a na první řádek přidat:

<%@ Page AutoEventWireup="true" %>

Money – kouzlíme s generiky II.

Generika jsou dobrá nejenom pro kolekce. Že se dají velmi dobře využít i v jiných situacích, dnes předvedu na třídě Money  (diskuze, kdy má být Money třída a kdy je lepší struct není předmětem tohoto článku). Uvidíte, že jdou dělat i takové věci jako generické operátory.

Zadání problému

Chceme řešit imlementaci třídy Money, třídy reprezentující peněžní datový typ skládající se z částky (Amount) a její měny (Currency). Naším cílem je dosáhnout následujího:

  • chceme zavést opakovaně využitelnou třídu Money (typicky do vlastní class-library) – ta má obsahovat vlastnost Amount (částku typu decimal) a vlastnost Currency (měnu), jejíž typ obecně naznáme a chceme ho ponechat na libovůli klientské aplikace (známe však pravidla pro obecné Money, které nám říkají, že bez kurzových převodů můžeme běžné operace provádět jen mezi operandy stejné měny – porovnávání, sčítání/odečítání, násobení/dělení číslem, dělení mezi sebou, atp.). Do class-library tedy chceme implementovat třídu Money<TCurrency>.
  • implementaci veškeré logiky chceme mít součástí této třídy Money<TCurrency> – v této třídě chceme definovat potřebné operace a centrálně je udržovat v class-library, chceme mít pokryté všechny základní operátory, atp. atp.
  • v jednoduchých svých projektech zadefinujeme třídu Currency (obvykle business-object odpovídající číselníku měn v DB s vlastnostmi Nazev, Zkratka, atp.) a budeme v nich třídu Money<TCurrency> používat přímo v kódu
Money<Currency> cena = new Money(150.20M, Currency.Czk);
cena = cena + new Money(20M, Currency.Czk);
cena = cena * 10;
MyLabel.Text = cena.Amount.ToString("n2") + " " + cena.Currency.Zkratka;
...
  • v rozsáhlejších našich projektech máme také třídu Currency, chceme však vytvořit odvozenou negenerickou třídu Money, která bude základní funkčnost obecnějšího Money<TCurrency> rozšiřovat o nějaké nové project-specific možnosti, např. ony kurzové přepočty, standardní výstup, atp. Použití zamýšlíme nějak takto:
Money cena = new Money(10M, Currency.Czk);
cena = cena.ToCurrency(Currency.Usd) + new Money(159.3M, Currency.Usd);
MyLabel.Text = cena.ToString("n2");
...
  • stále však trváme na tom, že implementace běžných operací (zejména operátorů) musí být použita z obecného Money<TCurrency>. Uvědomme si však základní problém – jak se vyhnout nové definici operátorů v negenerickém potomkovi Money : Money<Currency>, když operátory jsou statické, nejsou předmětem dědičnosti a v Money<TCurrency> budou vypadat nejspíš nějak takto:
public static Money<TCurrency> operator +(Money<TCurrency> money1, Money<TCurrency> money2)
{
// implementace
}

…v potomkovi Money chceme přeci součet dvou Money a hlavně návratový typ Money, nikoliv Money<Currency>

public static Money operator +(Money money1, Money money2)
{
// znovu implementace??? Tomu jsme se chtěli vyhnout! Implementaci chceme mít v class-library.
}
Kouzlo první – generická implementace funkčnosti operátorů

Čeho chceme dosáhnout – mít centralizovanou implementaci funkčnosti operátorů tak, abychom ji mohli snadno udržovat pro všechny naše projekty. Kouzlo první spočívá v extrahování implementace operací do generických metod třídy Money<TCurrency>, kteréžto metody sama třída Money<TCurrency> ve svých operátorech využije, stejnětak jako je může využít libovolný potomek při definici svých typovaných operátorů.

Money<TCurrency>:

public static Money<TCurrency> operator +(Money<TCurrency> money1, Money<TCurrency> money2)
{
    return SectiMoney<Money<TCurrency>>(money1, money2);
}

public static TResult SectiMoney<TResult>(Money<TCurrency> money1, Money<TCurrency> money2)
    where TResult : Money<TCurrency>, new()
{
    if (money1.Currency != money2.Currency)
    {
        throw ...
    }

    TResult result = new TResult();
    result.Amount = money1.Amount + money2.Amount;
    result.Currency = money1.Currency;
    return result;
}

Obdobně jako u BusinessObjectCollection využíváme kouzla generika s „cílovým typem“. Daní za tento způsob je nám nutnost vyžadovat constructor bez parametrů, abychom v generiku byly schopni vytvořit instanci cílového typu (možnost generika s constructorem předepsaných parametrů se asi v dohledné době nedočkáme, v .NET 3.5 pokud vím, nic takového stále není a po diskuzi s částí CLR teamu na letošním MVP Summitu v Redmondu už i chápu proč).

Project-specific potomek Money už pak může využívat této centrální implementace snadno:

public static Money operator +(Money money1, Money money2)
{
    return SectiMoney<Money>(money1, money2);
}
Kouzlo druhé – projektový potomek Money bez nové implementace operátorů

Řekněme, že se chceme v projektech, v project-specific třídách Money, úplně zbavit nutnosti implementovat operátory, byť už máme cestu jak v těchto operátorech využít centrální implementace funkčnosti. Projektový kód chceme mít co nejčistší, prostý takovýchto infrastrukturních redefinic, tak, abychom se v něm mohli přehledně věnovat jen nové funkčnosti Money oproti předkovi Money<TCurrency> – např. doplnit ony omílané kurzové přepočty, atp.

Jak na to?

Do řetězce dědičnosti mezi Money a Money<TCurrency> vložíme kouzelnou třídu MoneyImplementationBase, která nás dalším fíglem s generiky a operátory posune ke kýženému výsledku…

public abstract class MoneyImplementationBase<TCurrency, TResult>: Money<TCurrency>
    where TCurrency: class
    where TResult : MoneyImplementationBase<TCurrency, TResult>, new()
{
    public static TResult operator +(MoneyImplementationBase<TCurrency, TResult> money1, MoneyImplementationBase<TCurrency, TResult> money2)
    {
        return SectiMoney<TResult>(money1, money2);
    }
}

Tato třída samozřejmě patří do naší centrální class-library odkud ji budou využívat jednotlivé projekty.

A výsledek? Plně funkční projektové Money pak dostaneme takto:

public class Money : MoneyImplementationBase<Currency, Money>
{
    // toť vše!
    // sem už můžeme doplnit jen project-specific rozšíření Money, veškerou základní funkčnost nám poskytují předci
}

Hezké, že? Stejně jako u kouzlení s BusinessObjectCollection předhazujeme generickému předkovi sami sebe (pokud to bude potřeba, můžeme předhodit i svého potomka).