Can SOLID Principles Save Code Generation from Itself?

Hey there, fellow .NET devs, architects, and tech leads! Recently, my good friend Erik Dietrich shared some thought-provoking insights on his blog about code generation—specifically, why he sees it as a “failure of vision” (check out his post here). Erik argues that code generation often masks deeper design flaws; it’s a Rube-Goldbergian approach—writing code to write code—when we might be better off solving the root problem with better abstractions (like generics over a “ListGenerator”). He points to Entity Framework (EF) as a prime example: elegant locally for generating data transfer objects, but globally inelegant due to the boilerplate it creates. Let’s dive into whether SOLID principles—and “DRY” (Don’t Repeat Yourself)—can rescue code generation from its pitfalls, using EF as our case study.
We’re going to explore if SOLID can address Erik’s concerns by reducing the boilerplate and maintenance headaches he highlights. The “D” in SOLID—Dependency Inversion Principle—tells us to depend on abstractions, not concretions; this could be our ticket to cleaner, more maintainable generated code. But before we get there, let’s also consider how the “Don’t Repeat Yourself” (DRY) principle might help us tame the beast of repetition that Erik rightfully calls out in generated code.
DRY to the Rescue: Tackling Repetition in Entity Framework
Erik’s EF example is a great starting point—EF generates C# classes for each database table, often resulting in hundreds of files with repetitive code. Think about it: if your database has 100 tables, and many share common columns like Id, CreatedDate, or ModifiedDate, EF will dutifully generate those properties in every single class. That’s a DRY violation screaming for attention! If we make clean (as Erik references from The Pragmatic Programmer), those generated files might get blown away—but the repetition remains a maintenance nightmare. What if we could identify these repeated column sets upfront, define abstractions for them, and generate smarter, leaner entities? Let’s see how we can apply DRY to streamline this process.
Scanning Tables and Building Abstractions
Imagine we’re building a custom entity generator—something I specialize in at Funcular Labs, where we design custom ORMs and entity generators for .NET projects. First, we scan a set of SQL Server tables to identify repeated column patterns. Let’s say we’re working with a database schema like this:
CREATE TABLE customer (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(100),
Label NVARCHAR(100),
Description NVARCHAR(500),
CreatedDate DATETIME,
ModifiedDate DATETIME,
CreatedById INT,
ModifiedById INT
);
CREATE TABLE order (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(100),
Label NVARCHAR(100),
Description NVARCHAR(500),
CustomerId INT,
CreatedDate DATETIME,
ModifiedDate DATETIME,
CreatedById INT,
ModifiedById INT
);
Our generator scans these tables and identifies two repeated column sets:
Id, CreatedDate, ModifiedDate, CreatedById, ModifiedById
→ Let’s call this IAuditFields.
Name, Label, Description
→ Let’s break this into INamed (Name)
, ILabeled (Label)
, and IDescribed (Description)
, with IDescribed
inheriting INamed
and ILabeled
.
Here’s how we might define these interfaces in C#:
public interface INamed
{
string Name { get; set; }
}
public interface ILabeled
{
string Label { get; set; }
}
public interface IDescribed : INamed, ILabeled
{
string Description { get; set; }
}
public interface IAuditFields
{
int Id { get; set; }
DateTime CreatedDate { get; set; }
DateTime ModifiedDate { get; set; }
int CreatedById { get; set; }
int ModifiedById { get; set; }
}
Now, instead of generating these properties repeatedly in each entity, we implement them in abstract base classes—applying Dependency Inversion by depending on these abstractions:
public abstract class AuditableEntity : IAuditFields
{
public int Id { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime ModifiedDate { get; set; }
public int CreatedById { get; set; }
public int ModifiedById { get; set; }
}
public abstract class DescribedEntity : AuditableEntity, IDescribed
{
public string Name { get; set; }
public string Label { get; set; }
public string Description { get; set; }
}
Our generator then produces leaner entities like this:
public class Customer : DescribedEntity
{
// Only unique properties are generated
// Id, CreatedDate, etc., are inherited from AuditableEntity
// Name, Label, Description are inherited from DescribedEntity
}
public class Order : DescribedEntity
{
public int CustomerId { get; set; }
}
Benefits and Tradeoffs of This Approach
By leaning on Inversion, we’ve centralized repetitive logic in abstract base classes—reducing duplication and making our generated code far more maintainable. Let’s break down the benefits:
Centralized Business Logic: We can now standardize behaviors in our base classes. For example, a single method to set audit fields on creation or update:
public abstract class AuditableEntity : IAuditFields
{
// Properties as before...
public void SetAuditFieldsOnCreate(int userId)
{
CreatedDate = DateTime.UtcNow;
ModifiedDate = DateTime.UtcNow;
CreatedById = userId;
ModifiedById = userId;
}
public void SetAuditFieldsOnUpdate(int userId)
{
ModifiedDate = DateTime.UtcNow;
ModifiedById = userId;
}
}
Performance Optimization: By defining these abstractions at design time, we avoid runtime reflection (which EF often relies on). Reflection at runtime can be slow—especially with large datasets—but our approach bakes the structure into the code upfront, giving us better performance.
The tradeoff? We’re adding a layer of complexity with inheritance; if your database schema changes frequently, you might need to adjust these base classes manually—potentially introducing maintenance overhead. But for stable schemas, this approach keeps things DRY and SOLID.
Other Benefits to Ponder
What else can we gain here? First, this method makes our code more testable—since we’re depending on interfaces like IAuditFields, we can easily mock these dependencies in unit tests, ensuring our business logic is robust. Second, it opens the door to extensibility; if a new table needs a different audit behavior, we can create a new base class implementing IAuditFields with custom logic—without touching existing entities. It’s a win for the Open/Closed Principle (another SOLID gem) as well!
Wrapping Up: A Nod to Erik and a Hopeful Future
Erik’s post really got us thinking—and he’s spot-on that code generation can feel like a Band-Aid for deeper design issues. But by applying SOLID principles like Dependency Inversion, and honoring DRY, we can mitigate some of the impedance mismatch between our database and object-oriented worlds. At Funcular Labs, we’re passionate about crafting custom ORMs that solve these problems thoughtfully—ensuring your projects are both efficient and maintainable. So, hats off to Erik for the nudge; let’s keep pushing the boundaries of what code generation can achieve with a SOLID foundation! What do you think—can we bridge this gap for good?