Clean code principles including DRY, KISS, and YAGNI for .NET. Use when writing or reviewing code to ensure maintainability and simplicity.
Applies DRY, KISS, and YAGNI principles when writing or reviewing .NET code to eliminate duplication, reduce complexity, and avoid unnecessary features. Triggers automatically during code creation or review to suggest cleaner, more maintainable alternatives.
/plugin marketplace add DoubleslashSE/claude-marketplace/plugin install dotnet-tdd@doubleslash-pluginsThis skill is limited to using the following tools:
reference.mdEvery piece of knowledge must have a single, unambiguous, authoritative representation within a system.
// BAD: Duplicated validation logic
public class UserService
{
public void CreateUser(string email, string name)
{
if (string.IsNullOrWhiteSpace(email) || !email.Contains("@"))
throw new ArgumentException("Invalid email");
// ...
}
public void UpdateEmail(int userId, string newEmail)
{
if (string.IsNullOrWhiteSpace(newEmail) || !newEmail.Contains("@"))
throw new ArgumentException("Invalid email");
// ...
}
}
// GOOD: Single source of truth
public class UserService
{
private readonly IEmailValidator _emailValidator;
public void CreateUser(string email, string name)
{
_emailValidator.ValidateOrThrow(email);
// ...
}
public void UpdateEmail(int userId, string newEmail)
{
_emailValidator.ValidateOrThrow(newEmail);
// ...
}
}
public class EmailValidator : IEmailValidator
{
public bool IsValid(string email) =>
!string.IsNullOrWhiteSpace(email) && email.Contains("@");
public void ValidateOrThrow(string email)
{
if (!IsValid(email))
throw new ArgumentException("Invalid email", nameof(email));
}
}
// BAD: Magic values scattered throughout code
public decimal CalculateDiscount(decimal total)
{
if (total > 100)
return total * 0.1m; // What is 100? What is 0.1?
return 0;
}
public bool IsEligibleForFreeShipping(decimal total)
{
return total > 100; // Duplicated magic number!
}
// GOOD: Named constants
public static class OrderThresholds
{
public const decimal FreeShippingMinimum = 100m;
public const decimal StandardDiscountRate = 0.10m;
}
public decimal CalculateDiscount(decimal total)
{
if (total > OrderThresholds.FreeShippingMinimum)
return total * OrderThresholds.StandardDiscountRate;
return 0;
}
public bool IsEligibleForFreeShipping(decimal total)
{
return total > OrderThresholds.FreeShippingMinimum;
}
// BAD: Connection strings in multiple places
public class OrderRepository
{
private readonly string _conn = "Server=prod;Database=Orders;";
}
public class CustomerRepository
{
private readonly string _conn = "Server=prod;Database=Orders;";
}
// GOOD: Centralized configuration
public class DatabaseOptions
{
public string ConnectionString { get; set; } = string.Empty;
}
// In startup
services.Configure<DatabaseOptions>(configuration.GetSection("Database"));
// In repositories
public class OrderRepository
{
private readonly string _connectionString;
public OrderRepository(IOptions<DatabaseOptions> options)
{
_connectionString = options.Value.ConnectionString;
}
}
// Over-DRY: Forced abstraction hurts readability
public T ProcessEntity<T>(T entity, Func<T, bool> validator, Action<T> processor)
where T : class
{
if (!validator(entity))
throw new ValidationException();
processor(entity);
return entity;
}
// Better: Some duplication is acceptable for clarity
public Order ProcessOrder(Order order)
{
ValidateOrder(order);
SaveOrder(order);
return order;
}
public Customer ProcessCustomer(Customer customer)
{
ValidateCustomer(customer);
SaveCustomer(customer);
return customer;
}
Only extract duplication after you've seen it THREE times:
The simplest solution is usually the best solution.
// BAD: Over-engineered for simple use case
public interface IUserNameFormatter
{
string Format(User user);
}
public abstract class UserNameFormatterBase : IUserNameFormatter
{
protected abstract string GetFirstNamePart(User user);
protected abstract string GetLastNamePart(User user);
public string Format(User user) =>
$"{GetFirstNamePart(user)} {GetLastNamePart(user)}";
}
public class StandardUserNameFormatter : UserNameFormatterBase
{
protected override string GetFirstNamePart(User user) => user.FirstName;
protected override string GetLastNamePart(User user) => user.LastName;
}
public class UserNameFormatterFactory
{
public IUserNameFormatter Create(string type) => type switch
{
"standard" => new StandardUserNameFormatter(),
_ => throw new NotSupportedException()
};
}
// GOOD: Simple and direct
public static class UserExtensions
{
public static string GetFullName(this User user) =>
$"{user.FirstName} {user.LastName}";
}
// BAD: Abstraction for one implementation
public interface IOrderIdGenerator
{
string Generate();
}
public class GuidOrderIdGenerator : IOrderIdGenerator
{
public string Generate() => Guid.NewGuid().ToString();
}
// Registration
services.AddSingleton<IOrderIdGenerator, GuidOrderIdGenerator>();
// GOOD: Direct until you need flexibility
public class Order
{
public string Id { get; } = Guid.NewGuid().ToString();
}
// Add abstraction ONLY when you need a second implementation
// BAD: Hard to understand nested LINQ
var result = orders
.Where(o => o.Status == OrderStatus.Completed)
.GroupBy(o => o.CustomerId)
.Select(g => new
{
CustomerId = g.Key,
TotalSpent = g.Sum(o => o.Total),
OrderCount = g.Count(),
AverageOrder = g.Average(o => o.Total)
})
.Where(x => x.TotalSpent > 1000)
.OrderByDescending(x => x.TotalSpent)
.Take(10)
.SelectMany(x => customers.Where(c => c.Id == x.CustomerId)
.Select(c => new CustomerReport
{
Name = c.Name,
Email = c.Email,
TotalSpent = x.TotalSpent,
OrderCount = x.OrderCount
}))
.ToList();
// GOOD: Break into readable steps
var completedOrders = orders.Where(o => o.Status == OrderStatus.Completed);
var customerOrderSummaries = completedOrders
.GroupBy(o => o.CustomerId)
.Select(g => new CustomerOrderSummary(
CustomerId: g.Key,
TotalSpent: g.Sum(o => o.Total),
OrderCount: g.Count()))
.Where(s => s.TotalSpent > 1000)
.OrderByDescending(s => s.TotalSpent)
.Take(10)
.ToList();
var customerLookup = customers.ToDictionary(c => c.Id);
var reports = customerOrderSummaries
.Select(s => CreateReport(s, customerLookup[s.CustomerId]))
.ToList();
// BAD: What does 'true' mean?
SendEmail(user, "Welcome!", true, false, true);
// GOOD: Named parameters or dedicated methods
SendEmail(user, "Welcome!",
isHtml: true,
includeAttachments: false,
trackOpens: true);
// Even better: Specific methods
SendWelcomeEmail(user);
SendPasswordResetEmail(user);
// BAD: Deep inheritance hierarchy
public abstract class Entity { }
public abstract class AuditableEntity : Entity { }
public abstract class SoftDeletableAuditableEntity : AuditableEntity { }
public class Order : SoftDeletableAuditableEntity { }
// GOOD: Composition and interfaces
public interface IAuditable
{
DateTime CreatedAt { get; }
DateTime? ModifiedAt { get; }
}
public interface ISoftDeletable
{
bool IsDeleted { get; }
DateTime? DeletedAt { get; }
}
public class Order : IAuditable, ISoftDeletable
{
public DateTime CreatedAt { get; init; }
public DateTime? ModifiedAt { get; private set; }
public bool IsDeleted { get; private set; }
public DateTime? DeletedAt { get; private set; }
}
Don't implement something until it is necessary.
// BAD: Building for hypothetical future requirements
public class UserService
{
public User CreateUser(
string email,
string name,
string? middleName = null, // No requirement for this
string? suffix = null, // No requirement for this
string? preferredName = null, // No requirement for this
bool enableTwoFactor = false, // No requirement for this
string? backupEmail = null, // No requirement for this
Dictionary<string, string>? metadata = null) // "Might need it later"
{
// ...
}
}
// GOOD: Only what's needed now
public class UserService
{
public User CreateUser(string email, string name)
{
return new User
{
Id = Guid.NewGuid(),
Email = email,
Name = name,
CreatedAt = DateTime.UtcNow
};
}
}
// BAD: Configurable everything (but we only use JSON)
public interface ISerializer
{
string Serialize<T>(T obj);
T Deserialize<T>(string data);
}
public class JsonSerializer : ISerializer { }
public class XmlSerializer : ISerializer { }
public class YamlSerializer : ISerializer { }
public class BinarySerializer : ISerializer { }
public class MessagePackSerializer : ISerializer { }
public class SerializerFactory
{
public ISerializer Create(string format) => // ...
}
// GOOD: Use what you need
public static class JsonHelper
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public static string Serialize<T>(T obj) =>
System.Text.Json.JsonSerializer.Serialize(obj, Options);
public static T? Deserialize<T>(string json) =>
System.Text.Json.JsonSerializer.Deserialize<T>(json, Options);
}
// BAD: Interface with single implementation, no plans for others
public interface IEmailSender
{
Task SendAsync(string to, string subject, string body);
}
public class SmtpEmailSender : IEmailSender
{
public Task SendAsync(string to, string subject, string body)
{
// Only implementation we'll ever have
}
}
// GOOD: Just use the class directly
public class EmailSender
{
public async Task SendAsync(string to, string subject, string body)
{
// ...
}
}
// Add interface WHEN you actually need a second implementation
// BAD: Caching before measuring
public class ProductService
{
private readonly IMemoryCache _cache;
private readonly IDistributedCache _distributedCache;
private readonly IProductRepository _repository;
public async Task<Product?> GetByIdAsync(int id)
{
var cacheKey = $"product_{id}";
// Check L1 cache
if (_cache.TryGetValue(cacheKey, out Product? product))
return product;
// Check L2 cache
var cached = await _distributedCache.GetStringAsync(cacheKey);
if (cached != null)
{
product = JsonSerializer.Deserialize<Product>(cached);
_cache.Set(cacheKey, product, TimeSpan.FromMinutes(5));
return product;
}
// Database fallback
product = await _repository.GetByIdAsync(id);
if (product != null)
{
var serialized = JsonSerializer.Serialize(product);
await _distributedCache.SetStringAsync(cacheKey, serialized);
_cache.Set(cacheKey, product, TimeSpan.FromMinutes(5));
}
return product;
}
}
// GOOD: Start simple, optimize when needed
public class ProductService
{
private readonly IProductRepository _repository;
public Task<Product?> GetByIdAsync(int id) =>
_repository.GetByIdAsync(id);
}
// Add caching AFTER you've identified it as a bottleneck
See reference.md for more examples.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.