Industry Insights, Information, and Developer News
Building HonestCheetah.com (my GitHub project metrics tool) has taught me a lot of lessons, but one of the most annoying problems I've run into is what I call "message type sprawl." When you're making dozens of different REST and GraphQL calls to various APIs, you end up with an explosion of similar-but-not-quite-the-same classes for deserializing JSON responses.
Let me show you what I mean and how I've learned to manage it.
The Problem: Death by a Thousand DTOs Here's what typically happens. You start with one API call:
// GitHub API: Get a repository public class Repository { public string Name { get; set; } public string Owner { get; set; } public DateTime CreatedAt { get; set; } // ... 20 more properties }
Then you need to call another endpoint that also returns repository data, but with slightly different fields:
// GitHub GraphQL: Repository in search results public class SearchRepository // Can't call it Repository - name collision! { public string Name { get; set; } public int StarCount { get; set; } public string Language { get; set; } // ... different set of properties }
And another...with slightly different fields:
// GitHub API: Repository from user endpoint public class UserRepository // Running out of names... { public string Name { get; set; } public bool IsPrivate { get; set; } public string DefaultBranch { get; set; } // ... yet another variation }
Before you know it, you've got Repository, RepositoryInfo, RepositoryDetails, SearchRepository, UserRepository, ProjectRepository, and on and on. Your Models folder becomes a graveyard of slightly different DTO classes, and you're constantly playing the "which Repository class did I mean?" game.
Repository
RepositoryInfo
RepositoryDetails
SearchRepository
UserRepository
ProjectRepository
The Solution: Response Classes with Inner Types Here's the pattern I've settled on: create a Response class for each API call and use inner classes to define the message types specific to that response.
public class GetRepositoryResponse { public Repository Data { get; set; } public class Repository { public string Name { get; set; } public string Owner { get; set; } public DateTime CreatedAt { get; set; } // Properties specific to this endpoint } } public class SearchRepositoriesResponse { public List<Repository> Items { get; set; } public int TotalCount { get; set; } public class Repository { public string Name { get; set; } public int StarCount { get; set; } public string Language { get; set; } // Different properties for search results } }
Now I can have multiple Repository classes without naming conflicts because they're scoped to their parent Response class:
// Usage is clear and unambiguous var getRepoResponse = await GetRepository("benday-inc", "slnutil"); GetRepositoryResponse.Repository repo = getRepoResponse.Data; var searchResponse = await SearchRepositories("slnutil"); SearchRepositoriesResponse.Repository firstResult = searchResponse.Items[0];
Benefits of This Approach
The Catch: Types Aren't Interchangeable Here's the thing that might feel weird at first: GetRepositoryResponse.Repository and SearchRepositoriesResponse.Repository are completely different types as far as C# is concerned. You can't pass one where the other is expected, even if they have similar properties.
GetRepositoryResponse.Repository
SearchRepositoriesResponse.Repository
// This won't compile GetRepositoryResponse.Repository repo1 = getRepoResponse.Data; SearchRepositoriesResponse.Repository repo2 = repo1; // Error!
This is actually a feature, not a bug. These really are different types from different API calls with different guarantees about what data is present.
If you need to work with common properties across different response types, consider:
// Option 1: Create an interface for shared properties public interface IRepositoryBase { string Name { get; } string Owner { get; } } // Have your inner classes implement it public class GetRepositoryResponse { public class Repository : IRepositoryBase { public string Name { get; set; } public string Owner { get; set; } // ... other properties } } // Option 2: Map to a common domain model public class DomainRepository // Your actual business object { public string Name { get; set; } public string Owner { get; set; } public static DomainRepository FromGetResponse(GetRepositoryResponse.Repository repo) { return new DomainRepository { Name = repo.Name, Owner = repo.Owner }; } }
Automating the Boring Parts Creating these Response classes by hand is tedious, especially when you're looking at a complex JSON response with nested objects. That's why I added a feature to my (free) slnutil tool to generate these classes automatically.
Install it:
dotnet tool install -g slnutil
Copy a sample JSON message result to the clipboard, then call slnutil from the command line:
slnutil
slnutil classesfromjson /clipboard /innerclasses
It'll generate the Response class with all the inner classes properly structured based on your actual JSON. The /innerclasses flag is the key -- it creates the nested structure instead of flat classes. Of course, if you want to skip this inner class approach and use regular public types, you can do that, too by omitting the /innerclasses option.
/innerclasses
When to Use This Pattern This pattern works great when:
It might be overkill if:
Real-World Example from HonestCheetahHere's an actual Response class from my GitHub metrics tool:
public class GetProjectItemsResponse { public Organization Organization { get; set; } public class Organization { public ProjectV2 ProjectV2 { get; set; } public class ProjectV2 { public string Title { get; set; } public ItemConnection Items { get; set; } public class ItemConnection { public PageInfo PageInfo { get; set; } public List<ProjectItem> Nodes { get; set; } public class PageInfo { public bool HasNextPage { get; set; } public string EndCursor { get; set; } } public class ProjectItem { public string Id { get; set; } public Content Content { get; set; } public class Content { public string Title { get; set; } public int Number { get; set; } public string State { get; set; } // ... more fields } } } } } }
Yes, it's deeply nested. Yes, it's verbose. But it exactly matches the structure of the GraphQL response, there are no naming conflicts with my other 20+ Response classes, and when GitHub changes this endpoint, I know exactly where to make updates.
Conclusion Message type sprawl is one of those problems that sneaks up on you. You start with a few DTOs, and before you know it, you're drowning in similarly-named classes that are almost impossible to keep straight. The Response class pattern with inner types isn't the only solution, but it's one that's served me well in building real applications that consume multiple APIs.
It keeps your types organized, avoids naming conflicts, and makes it crystal clear which data shapes belong to which API calls. And with tools like slnutil to generate the boilerplate, it's not even that much extra work.
Give it a try next time you find yourself creating RepositoryDto2 or UserResponseModelInfo. Your future self will thank you.
RepositoryDto2
UserResponseModelInfo
About the Author
Posted by Benjamin Day on 12/01/2025