Warum Ihr C#-Code ins Leere läuft (und wie Sie das beheben)
\n\nSie haben Wochen mit der Refaktorisierung Ihres Empfehlungsalgorithmus verbracht. Die neue Version des RecommendationService sieht makellos aus, der Code ist sauberer, die LINQ-Abfragen sind schneller. Sie rollen das Update in die Produktion aus, und… die Conversion fällt um 15%. Kommt Ihnen das bekannt vor? Wenn nicht – haben Sie Glück gehabt. Wenn ja – haben Sie gerade Geld und Zeit verloren, weil Sie sich auf Ihre Intuition verlassen haben, nicht auf Daten.
In der Welt der C#-Entwicklung gibt es eine goldene Regel: „Vertraue niemals deiner Meinung, vertraue der Statistik“. Genau dafür gibt es A/B-Testing (oder Split-Testing). Das ist kein Marketing-Gimmick – es ist ein technischer Ansatz zur Entscheidungsfindung. In diesem Artikel werden wir durchgehen, wie Sie vollwertiges A/B-Testing in Ihre C#-Anwendung integrieren: von der Architektur bis zur Berechnung der statistischen Signifikanz. Und ja, es wird viel Code geben.
\n\nWas ist A/B-Testing im Kontext von C#?
\n\nStellen Sie sich vor, Sie haben zwei Varianten derselben Logik – Kontrollgruppe A (alte Version) und Experimentalgruppe B (neue Version). Sie verteilen die Benutzer zufällig auf diese Gruppen, sammeln Metriken und bestimmen mit Mathematik, welche Variante tatsächlich besser ist.
\n\nIn C# kann dies alles Mögliche betreffen:
\n- \n
- Änderung des Suchalgorithmus \n
- Neues Design des Zahlungsformulars \n
- Verschiedene Varianten der Artikelsortierung \n
- Experimente mit der Cache-Zeit \n
- Testen von ML-Modellen \n
Der entscheidende Punkt: A/B-Testing in C# geht nicht darum, „ein Häkchen in der Konfiguration zu setzen“. Es geht um saubere Architektur, Randomisierung und fehlerfreie Metrikerfassung.
\n\nArchitektur des A/B-Testings: Layer Cake in C#
\n\nKommen wir direkt zur Praxis. Ich zeige Ihnen eine minimale, aber funktionale Architektur. Sie besteht aus drei Schichten:
\n\n- \n
- Assigner – entscheidet, in welche Gruppe der Benutzer fällt. \n
- Feature Decision – welche Codevariante ausgeführt wird. \n
- Tracker – zeichnet das Ereignis und das Ergebnis auf. \n
Schritt 1: Assigner – Wer bist du, Benutzer?
\n\nDer häufigste Fehler ist eine falsche Randomisierung. Random zu verwenden ist eine schlechte Idee, da die Gruppen beim Neustart der Anwendung „verrutschen“. Die richtige Lösung ist eine deterministische Verteilung basierend auf der UserId.
public interface IExperimentAssigner\n{\n string GetVariant(string userId, string experimentName);\n}\n\npublic class ConsistentExperimentAssigner : IExperimentAssigner\n{\n public string GetVariant(string userId, string experimentName)\n {\n // Erzeuge einen stabilen Hash aus userId + experimentName\n var hash = HashCode.Combine(userId, experimentName);\n // Nimm Modulo 100 – erhalte einen Prozentsatz\n var bucket = Math.Abs(hash) % 100;\n \n // Angenommen, 50% in A, 50% in B\n return bucket < 50 ? "A" : "B";\n }\n}\n\n\nJetzt ist derselbe Benutzer für ein bestimmtes Experiment immer in derselben Gruppe. Das ist entscheidend für die Korrektheit der Daten.
\n\nSchritt 2: Feature Decision – Kampf der Algorithmen
\n\nJetzt müssen wir den Code dazu bringen, selbst zu wählen, welche Logik ausgeführt wird. Wir verwenden das Strategie-Muster mit einer Factory.
\n\npublic interface IPricingStrategy\n{\n decimal CalculatePrice(decimal basePrice);\n}\n\npublic class OldPricingStrategy : IPricingStrategy\n{\n public decimal CalculatePrice(decimal basePrice) \n => basePrice; // Ohne Rabatt\n}\n\npublic class NewPricingStrategy : IPricingStrategy\n{\n public decimal CalculatePrice(decimal basePrice) \n => basePrice * 0.9m; // 10% Rabatt\n}\n\npublic class PricingStrategyFactory\n{\n private readonly IExperimentAssigner _assigner;\n\n public PricingStrategyFactory(IExperimentAssigner assi