Industry Insights, Information, and Developer News
Lately, I've been working on a bunch of tools to analyze GitHub Issues and GitHub Project data to calculate and report on project management metrics. I don't know if you've ever tried to get this data out of GitHub before via their public APIs but -- well -- you end up needing to write a LOT of GraphQL queries. Which then begs the question: where do you actually put these queries so that you can use them from C#?
We've all been there -- you need to include SQL scripts, configuration templates, GraphQL queries, or other text-based resources in your application, and you end up embedding them as string constants. It starts innocently enough with one small query, but soon you're managing hundreds of lines of embedded strings that make your code impossible to read and maintain.
Here's my solution.
The Journey from .resx to External Files In the .NET Framework days, we had a standard solution for this: .resx files. They were built into Visual Studio, had designer support, and generated strongly-typed accessors. Life was good ... if you were on Windows.
.resx
Then came .NET Core, and .resx files disappeared. They eventually returned in .NET Core 3.0 (yah ... I had to look that up) so theoretically, I could solve this external files problem by going back to .resx files. The problem is that they're pretty Visual Studio-centric ... which makes them Windows-centric. And I tend to mostly work on a Mac developing using VSCode and deploying on to Linux-based resources in Azure. That makes the whole .resx workflow a little awkward for me because the resource editor tooling isn't so hot in VSCode.
But here's the thing: I don't actually need the complexity and feature-rich power of resource files. A simpler pattern has emerged that's more flexible, more portable, and easier to work with across different platforms and IDEs.
The Hidden Cost of Embedded Resources When you embed non-C# content directly in your code, you're mixing concerns and creating maintenance headaches. Let's take a look one of my GitHub GraphQL queries. This query brings back a bunch of data about Issues that are linked off of a GitHub Project (aka. "Project v2").
What do you notice about this query? Here's a hint: how would you encode this into a string in C# and what would be ultra-annoying about that? Yah. It's the curly braces. I could put this into a C# string but then I'd be fighting against the interpolated string syntax and how much do you wanna bet that I mess up somewhere and accidentally make the compiler think that this is supposed to be C# code? My guess is that the chance of messing up is about 99%.
# File: Resources/queries/project-issues.graphql query($org: String!, $projectNumber: Int!, $after: String) { organization(login: $org) { projectV2(number: $projectNumber) { title items(first: 100, after: $after) { pageInfo { hasNextPage endCursor } nodes { id content { ... on Issue { title number state createdAt closedAt } } } } } } }
But the curly brace problem is just the beginning. Whether it's SQL queries, JSON schemas, XML templates, GraphQL queries, or configuration files, these resources have their own syntax, their own formatting rules, and their own reasons to change. They don't belong inside C# strings.
Consider what happens when you embed these resources:
The Simple Answer: External Resource Files The solution is straightforward: keep these resources as separate files in your project and load them at runtime. This approach works beautifully whether you're in Visual Studio on Windows, VS Code on Mac, or even vim on Linux. Let me walk through this pattern using a real-world example from my GitHub metrics application, where I manage dozens of GraphQL queries.
Step 1: Organize Your Resources
Create a logical folder structure in your project:
MyProject/ ├── Services/ ├── Models/ ├── Resources/ # or "queries", "scripts", "templates", etc. │ ├── queries/ │ │ ├── get-user-data.graphql │ │ └── get-repository-stats.graphql │ ├── sql/ │ │ ├── initialize-database.sql │ │ └── cleanup-old-records.sql │ └── templates/ │ └── email-notification.html
Step 2: Configure Build Actions
The crucial step is ensuring these files are copied to your output directory. In your .csproj file:
<ItemGroup> <!-- Copy all GraphQL files --> <None Update="Resources\queries\*.graphql"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> <!-- Copy all SQL files --> <None Update="Resources\sql\*.sql"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> <!-- Copy specific template files --> <None Update="Resources\templates\email-notification.html"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>
The PreserveNewest option only copies files when they're newer than the destination, keeping builds fast. This works identically whether you're building on Windows, Mac, or Linux -- no platform-specific tooling required.
PreserveNewest
Step 3: Create Resource Loaders
Build simple utility classes to load these resources:
public static class ResourceLoader { private static string GetResourcePath(string folder, string filename) { // Path.Combine handles path separators correctly on all platforms return Path.Combine("Resources", folder, filename); } public static string LoadQuery(string queryName) { var path = GetResourcePath("queries", $"{queryName}.graphql"); return File.ReadAllText(path); } public static string LoadSqlScript(string scriptName) { var path = GetResourcePath("sql", $"{scriptName}.sql"); return File.ReadAllText(path); } public static string LoadTemplate(string templateName) { var path = GetResourcePath("templates", templateName); return File.ReadAllText(path); } }
Step 4: Use Your Resources
Now your code becomes clean and purposeful:
public class GitHubService { public async Task<ProjectData> GetProjectMetrics(string projectId) { // Load the GraphQL query from file var query = ResourceLoader.LoadQuery("get-project-metrics"); // Use it with your API client var response = await _client.ExecuteQuery(query, new { projectId }); // Process response... } }
Real-World Example: GraphQL Queries Back to the query sample from my GitHub metrics app, here's what one of my GraphQL query files looks like:
This query is properly formatted, syntax-highlighted in my IDE (whether that's VS Code, Rider, or Visual Studio), and easy to test in GraphQL playground tools -- none of which would be possible if it were embedded in a C# string.
Benefits Beyond Code Cleanliness
Development Experience
Maintenance
Flexibility
Advanced Patterns Once you adopt this approach, several enhancements become possible:
Caching for Performance
private static readonly Dictionary<string, string> _cache = new(); public static string LoadQuery(string queryName) { if (_cache.TryGetValue(queryName, out var cached)) return cached; var content = File.ReadAllText(GetPath(queryName)); _cache[queryName] = content; return content; }
Embedded Resources for NuGet Packages
If you're building a library, you can still embed these as resources without using .resx:
<ItemGroup> <EmbeddedResource Include="Resources\**\*" /> </ItemGroup>
Then load them from the assembly:
using var stream = Assembly.GetExecutingAssembly() .GetManifestResourceStream($"MyLibrary.Resources.queries.{queryName}.graphql"); using var reader = new StreamReader(stream); return reader.ReadToEnd();
When to Use This Pattern This approach works well for:
It might be overkill for:
Conclusion The shift from .NET Framework to .NET Core/.NET 5+ forced us to reconsider many patterns we took for granted. In the case of resource management, losing easy .resx support initially felt like a step backward. But it pushed us toward simpler, more portable solutions that actually work better in our modern, cross-platform development world.
Keeping non-C# resources in separate files is a simple pattern that works everywhere -- from Visual Studio on Windows to VS Code on Mac to containers running in Azure. Your code becomes cleaner, your resources become more maintainable, and your entire team becomes more productive regardless of their preferred development environment.
Sometimes losing a feature leads you to a better solution. This is one of those times.
What types of resources do you manage in your applications? Have you found other patterns that work well in cross-platform development? I'd love to hear about your approaches -- reach out at [email protected] or on Twitter @benday.
About the Author
Posted by Benjamin Day on 10/22/2025