diff --git a/csharp.xUnit/GildedRose/AgedBrieUpdater.cs b/csharp.xUnit/GildedRose/AgedBrieUpdater.cs
new file mode 100644
index 00000000..955f2d96
--- /dev/null
+++ b/csharp.xUnit/GildedRose/AgedBrieUpdater.cs
@@ -0,0 +1,16 @@
+namespace GildedRoseKata;
+
+public class AgedBrieUpdater : ItemUpdaterBase
+{
+ public override string Name => "Aged Brie";
+
+ public override void Update(Item item)
+ {
+ item.Quality = ClampQuality(item.Quality + 1);
+ item.SellIn -= 1;
+ if (item.SellIn < 0)
+ {
+ item.Quality = ClampQuality(item.Quality + 1);
+ }
+ }
+}
diff --git a/csharp.xUnit/GildedRose/BackstagePassUpdater.cs b/csharp.xUnit/GildedRose/BackstagePassUpdater.cs
new file mode 100644
index 00000000..c1c1f4af
--- /dev/null
+++ b/csharp.xUnit/GildedRose/BackstagePassUpdater.cs
@@ -0,0 +1,17 @@
+namespace GildedRoseKata;
+
+public class BackstagePassUpdater : ItemUpdaterBase
+{
+ public override string Name => "Backstage passes to a TAFKAL80ETC concert";
+
+ public override void Update(Item item)
+ {
+ var increase = item.SellIn <= 5 ? 3 : item.SellIn <= 10 ? 2 : 1;
+ item.Quality = ClampQuality(item.Quality + increase);
+ item.SellIn -= 1;
+ if (item.SellIn < 0)
+ {
+ item.Quality = 0;
+ }
+ }
+}
diff --git a/csharp.xUnit/GildedRose/IItemUpdater.cs b/csharp.xUnit/GildedRose/IItemUpdater.cs
new file mode 100644
index 00000000..c7a0a823
--- /dev/null
+++ b/csharp.xUnit/GildedRose/IItemUpdater.cs
@@ -0,0 +1,10 @@
+namespace GildedRoseKata;
+
+public interface IItemUpdater
+{
+ /// The exact item name this strategy handles ("default" for the fallback).
+ string Name { get; }
+
+ /// Applies one full day: quality change + SellIn change + clamping.
+ void Update(Item item);
+}
diff --git a/csharp.xUnit/GildedRose/IItemUpdaterFactory.cs b/csharp.xUnit/GildedRose/IItemUpdaterFactory.cs
new file mode 100644
index 00000000..66855fe6
--- /dev/null
+++ b/csharp.xUnit/GildedRose/IItemUpdaterFactory.cs
@@ -0,0 +1,6 @@
+namespace GildedRoseKata;
+
+public interface IItemUpdaterFactory
+{
+ IItemUpdater Get(Item item);
+}
diff --git a/csharp.xUnit/GildedRose/ItemUpdaterBase.cs b/csharp.xUnit/GildedRose/ItemUpdaterBase.cs
new file mode 100644
index 00000000..a973b8d4
--- /dev/null
+++ b/csharp.xUnit/GildedRose/ItemUpdaterBase.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace GildedRoseKata;
+
+public abstract class ItemUpdaterBase : IItemUpdater
+{
+ public const int MinQuality = 0;
+ public const int MaxQuality = 50;
+
+ public abstract string Name { get; }
+
+ public abstract void Update(Item item);
+
+ protected static int ClampQuality(int quality) => Math.Clamp(quality, MinQuality, MaxQuality);
+}
diff --git a/csharp.xUnit/GildedRose/ItemUpdaterFactory.cs b/csharp.xUnit/GildedRose/ItemUpdaterFactory.cs
new file mode 100644
index 00000000..a6bc365d
--- /dev/null
+++ b/csharp.xUnit/GildedRose/ItemUpdaterFactory.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+
+namespace GildedRoseKata;
+
+public class ItemUpdaterFactory : IItemUpdaterFactory
+{
+ public const string ConjuredPrefix = "Conjured";
+ private const string DefaultName = "default";
+
+ private readonly Dictionary _updatersByName;
+ private readonly IItemUpdater _fallback;
+
+ public ItemUpdaterFactory(IEnumerable updaters)
+ {
+ _updatersByName = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var updater in updaters)
+ {
+ _updatersByName[updater.Name] = updater;
+ }
+
+ if (!_updatersByName.TryGetValue(DefaultName, out _fallback))
+ {
+ throw new ArgumentException($"A \"{DefaultName}\" fallback updater is required.", nameof(updaters));
+ }
+ }
+
+ public static ItemUpdaterFactory Default() => new(new IItemUpdater[]
+ {
+ new NormalItemUpdater(),
+ new AgedBrieUpdater(),
+ new SulfurasUpdater(),
+ new BackstagePassUpdater(),
+ });
+
+ public IItemUpdater Get(Item item)
+ {
+ if (_updatersByName.TryGetValue(item.Name, out var updater))
+ {
+ return updater;
+ }
+
+ if (item.Name.StartsWith(ConjuredPrefix, StringComparison.OrdinalIgnoreCase) &&
+ _updatersByName.TryGetValue(ConjuredPrefix, out var conjured))
+ {
+ return conjured;
+ }
+
+ return _fallback;
+ }
+}
diff --git a/csharp.xUnit/GildedRose/NormalItemUpdater.cs b/csharp.xUnit/GildedRose/NormalItemUpdater.cs
new file mode 100644
index 00000000..5f35bf3a
--- /dev/null
+++ b/csharp.xUnit/GildedRose/NormalItemUpdater.cs
@@ -0,0 +1,16 @@
+namespace GildedRoseKata;
+
+public class NormalItemUpdater : ItemUpdaterBase
+{
+ public override string Name => "default";
+
+ public override void Update(Item item)
+ {
+ item.Quality = ClampQuality(item.Quality - 1);
+ item.SellIn -= 1;
+ if (item.SellIn < 0)
+ {
+ item.Quality = ClampQuality(item.Quality - 1);
+ }
+ }
+}
diff --git a/csharp.xUnit/GildedRose/SulfurasUpdater.cs b/csharp.xUnit/GildedRose/SulfurasUpdater.cs
new file mode 100644
index 00000000..66ee371e
--- /dev/null
+++ b/csharp.xUnit/GildedRose/SulfurasUpdater.cs
@@ -0,0 +1,11 @@
+namespace GildedRoseKata;
+
+public class SulfurasUpdater : ItemUpdaterBase
+{
+ public override string Name => "Sulfuras, Hand of Ragnaros";
+
+ public override void Update(Item item)
+ {
+ // Legendary item: Quality stays at 80 and SellIn never changes.
+ }
+}