VSLive! Blog

Industry Insights, Information, and Developer News

Blog archive

Skip the String Constants: Managing GraphQL, SQL, and Other Text Files in C#

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.

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:

  • You lose IDE support for that file type's syntax
  • Version control diffs become noisy and hard to review
  • Testing and debugging require extracting strings from code
  • Formatting becomes a nightmare with escape sequences
  • Your C# files grow unwieldy

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.

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:

# 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
            }
          }
        }
      }
    }
  }
}

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

  • Full IDE support for each file type (syntax highlighting, validation, formatting)
  • Easy to test resources in isolation
  • Resources can be edited by non-developers

Maintenance

  • Clear separation of concerns
  • Clean version control history
  • Easy to find and update specific resources

Flexibility

  • Resources can be overridden at deployment time
  • Same resources can be shared across projects
  • Easy to switch between different resource sets (dev/test/prod)

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:

  • SQL queries and migration scripts
  • GraphQL queries and mutations
  • Email or document templates
  • Configuration file templates
  • Test data files
  • Any multi-line text resource

It might be overkill for:

  • Simple, one-line strings
  • Resources that are truly constants
  • Performance-critical code where file I/O is a concern

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

Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 10/22/2025


Keep Up-to-Date with Visual Studio Live!

Email Address*Country*
Please type the letters/numbers you see above.