Showing posts with label post. Show all posts
Showing posts with label post. Show all posts

Using AI with .NET 10 Scripts: What Worked, What Didn’t, and Lessons Learned

I wanted to kick the tires on the upcoming .NET 10 C# script experience and see how far I could get calling Reka’s Research LLM from a single file, no project scaffolding, no .csproj. This isn’t a benchmark; it’s a practical tour to compare ergonomics, setup, and the little gotchas you hit along the way. I’ll share what worked, what didn’t, and a few notes you might find useful if you try the same.

All the sample code (and a bit more) is here: reka-ai/api-examples-dotnet · csharp10-script. The scripts run a small “top 3 restaurants” prompt so you can validate everything quickly.

We’ll make the same request in three ways:

  1. OpenAI SDK
  2. Microsoft.Extensions.AI for OpenAI
  3. Raw HttpClient

What you need

The C# "script" feature used below ships with the upcoming .NET 10 and is currently available in preview. If you prefer not to install a preview SDK, you can run everything inside the provided Dev Container or on GitHub Codespaces. I include a .devcontainer folder with everything set up in the repo.

Set up your API key

We are talking about APIs here, so of course, you need an API key. The good news is that it's free to sign up with Reka and get one! It's a 2-click process, more details in the repo. The API key is then stored in a .env file, and each script loads environment variables using DotNetEnv.Env.Load(), so your key is picked up automatically. I went this way instead of using dotnet user-secrets because I thought it would be the way it would be done in a CI/CD pipeline or a quick script.

Run the demos

From the csharp10-script folder, run any of these scripts. Each line is an alternative

dotnet run 1-try-reka-openai.cs
dotnet run 2-try-reka-ms-ext.cs
dotnet run 3-try-reka-http.cs

You should see a short list of restaurant suggestions.

Propmt Result: 3 restaurants


OpenAI SDK with a custom endpoint

Reka's API is using the OpenAI format; therefore, I thought of using the NuGet package OpenAI. To reference a package in a script, you use the #:package [package name]@[package version] directive at the top of the file. Here is an example:

#:package OpenAI@2.3.0

// ...

var baseUrl = "http://api.reka.ai/v1";

var openAiClient = new OpenAIClient(new ApiKeyCredential(REKA_API_KEY), new OpenAIClientOptions
{
    Endpoint = new Uri(baseUrl)
});

var client = openAiClient.GetChatClient("reka-flash-research");

string prompt = "Give me 3 nice, not crazy expensive, restaurants for a romantic dinner in Montreal";

var completion = await client.CompleteChatAsync(
    new List<ChatMessage>
    {
        new UserChatMessage(prompt)
    }
);

var generatedText = completion.Value.Content[0].Text;

Console.WriteLine($" Result: \n{generatedText}");

The rest of the code is more straightforward. You create a chat client, specify the Reka API URL, select the model, and then you send a prompt. And it works just as expected. However, not everything was perfect, but before I share more about that part, let's talk about Microsoft.Extensions.AI.

Microsoft Extensions AI for OpenAI

Another common way to use LLM in .NET is to use one ot the Microsoft.Extensions.AI NuGet package. In our case Microsoft.Extensions.AI.OpenAI was used.

#:package Microsoft.Extensions.AI.OpenAI@9.8.0-preview.1.25412.6

// ....

var baseUrl = "http://api.reka.ai/v1";

IChatClient client = new ChatClient("reka-flash-research", new ApiKeyCredential(REKA_API_KEY), new OpenAIClientOptions
{
    Endpoint = new Uri(baseUrl)
}).AsIChatClient();

string prompt = "Give me 3 nice, not crazy expensive, restaurants for a romantic dinner in Montreal";

Console.WriteLine(await client.GetResponseAsync(prompt));

As you can see, the code is very similar. Create a chat client, set the URL, the model, and add your prompt, and it works just as well.

That's two ways to use Reka API with different SDKs, but maybe you would prefer to go "SDKless", let's see how to do that.

Raw HttpClient calling the REST API

Without any SDK to help, there is a bit more line of code to write, but it's still pretty straightforward. Let's see the code:

using var httpClient = new HttpClient();

var baseUrl = "http://api.reka.ai/v1/chat/completions";

var requestPayload = new
{
    model = "reka-flash-research",
    messages = new[]
            {
                new
                {
                    role = "user",
                    content = "Give me 3 nice, not crazy expensive, restaurants for a romantic dinner in New York city"
                }
            }
};

using var request = new HttpRequestMessage(HttpMethod.Post, baseUrl);
request.Headers.Add("Authorization", $"Bearer {REKA_API_KEY}");
request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

var response = await httpClient.SendAsync(request);

var responseContent = await response.Content.ReadAsStringAsync();

var jsonDocument = JsonDocument.Parse(responseContent);

var contentString = jsonDocument.RootElement
    .GetProperty("choices")[0]
    .GetProperty("message")
    .GetProperty("content")
    .GetString();

Console.WriteLine(contentString);

So you create an HttpClient, prepare a request with the right headers and payload, send it, get the response, and parse the JSON to extract the text. In this case, you have to know the JSON structure of the response, but it follows the OpenAI format.

What did I learn from this experiment?

I used VS Code while trying the script functionality. One thing that surprised me was that I didn't get any IntelliSense or autocompletion. I try to disable the DevKit extension and change the setting for OmniSharp, but no luck. My guess is that because it's in preview, and it will work just fine in November 2025 when .NET 10 will be released.

In this light environment, I encountered some issues where, for some reason, I couldn't use an https endpoint, so I had to use http. In the raw httpClient script, I had some errors with the Reflection that wasn't available. It could be related to the preview or something else, I didn't investigate further.

For the most part, everything worked as expected. You can use C# code to quickly execute some tasks without any project scaffolding. It's a great way to try out the Reka API and see how it works.

What's Next?

While writing those scripts, I encountered multiple issues that aren't related to .NET but more about the SDKs when trying to do more advanced functionalities like optimization of the query and formatting the response output. Since it goes beyond the scope of this post, I will share my findings in a follow-up post. Stay tuned!

Video version

Here is a video version of this post


Learn more

Get an AI assistant to do your research

Introducing Reka Research: Your AI Research Assistant

An AI Assistant searching in documents
Meet Reka Research a powerful AI agent that can search the web and analyze your files to answer complex questions in minutes. Whether you're staying up to date with AI news, screening resumes, or researching technical topics, Reka Research does the heavy lifting for you.


What Makes Reka Research Special?

Reka Research stands out in four key areas:

  • Top Performance: Best in class results on research benchmarks
  • Fast Results: Get thorough answers in 1-3 minutes
  • Full Transparency: See exactly how the AI reached its conclusions. All steps are visible.

Smart Web Search That Actually Works

Ever wished you could ask someone to research the latest AI developments while you focus on other work? That's exactly what Reka Research does.

Watch how it works:

In this demo, Jess and Sharath shows how Reka Research can automatically gather the most important AI news from the past week. The AI visits multiple websites, takes notes, and presents a clean summary with sources. You can even restrict searches to specific domains or set limits on how many sites to check.

File Search for Your Private Documents

Sometimes the information you need isn't on the web - it's in your company's documents, meeting notes, or file archives. Reka Research can search through thousands of private files to find exactly what you're looking for.

See it in action:  

In this example, ess and Sharath shows how HR teams can use Reka Research to quickly screen resumes. Instead of manually reviewing hundreds of applications, the AI finds candidates who meet specific requirements (like having a computer science degree and 3+ years of backend experience) in seconds!

Writing Better Prompts Gets Better Results

Like any AI tool, Reka Research works best when you know how to ask the right questions. The key is being specific about what you want and providing context.

Learn the techniques:

Jess and Yi shares practical tips for getting the most out of Reka Research. Instead of asking "summarize meeting minutes," try "summarize April meeting minutes about public participation." The more specific you are, the better your results will be.

Ready to Try Reka Research?

Reka Research is currently available for everyone! Try it via the playground, or using directly the API. Whether you're researching competitors, analyzing documents, or staying current with industry trends, it can save you hours of work.

Want to learn more and connect with other users? Join our Discord community where you can:

  • Get tips from other Reka Research users
  • Share your use cases and success stories
  • Stay updated on new features and releases
  • Ask questions directly to our team

Join our Discord Community

Ready to transform how you research? Visit reka.ai to learn more about Reka Research and our other AI tools.

Why Your .NET Code Coverage Badge is 'Unknown' in GitLab (And How to Fix It)


In a recent post, I shared how to set up a CI/CD pipeline for a .NET Aspire project on GitLab. The pipeline includes unit tests, security scanning, and secret detection, and if any of those fail, the pipeline would fail. Great, but what about code coverage for the unit tests? The pipeline included code coverage commands, but the coverage was not visible in the GitLab interface. Let's fix that.

(blog post en français ici)

Badge on Gitlab showing coverage unknown

The Problem

One thing I initially thought was that the regex used to extract the coverage was incorrect. The regex used in the pipeline was:

coverage: '/Total\s*\|\s*(\d+(?:\.\d+)?)%/'

That regex came directly from the GitLab documentation, so I thought it should work correctly. However, coverage still wasn't visible in the GitLab interface.

So with the help of GitHub Copilot, we wrote a few commands to validate:

  • That the coverage.cobertura.xml was in a consistent location (instead of being in a folder with a GUID name)
  • That the coverage.cobertura.xml file was in a valid format
  • What exactly the regex was looking for

Everything checked out fine, so why was the coverage not visible?

The Solution

It turns out that the coverage command with the regex expression is scanning the console output and not the coverage.cobertura.xml file. Aha! One solution was to install dotnet-tools to changing where the the test results was persisted; to the console instead of the XML file, but I preferred keeping the .NET environment unchanged.

The solution I ended up implementing was executing a grep command to extract the coverage from the coverage.cobertura.xml file and then echoing it to the console. Here's what it looks like:

- COVERAGE=$(grep -o 'line-rate="[0-9.]*"' TestResults/coverage.cobertura.xml | head -1 | grep -o '[0-9.]*' | awk '{printf "%.1f", $1*100}')
- echo "Total | ${COVERAGE}%"

Results

And now when the pipeline runs, the coverage is visible in the GitLab pipeline!



And the badge is updated to show the coverage percentage.

Coverage badge showing percentage


Complete Configuration

Here's the complete test job configuration. Of course, the full .gitlab-ci.yml file is available in the GitLab repository.

test:
  stage: test
  image: mcr.microsoft.com/dotnet/sdk:9.0
  <<: *dotnet_cache
  dependencies:
    - build
  script:
    - dotnet test $SOLUTION_FILE --configuration Release --logger "junit;LogFilePath=$CI_PROJECT_DIR/TestResults/test-results.xml" --logger "console;verbosity=detailed" --collect:"XPlat Code Coverage" --results-directory $CI_PROJECT_DIR/TestResults
    - find TestResults -name "coverage.cobertura.xml" -exec cp {} TestResults/coverage.cobertura.xml \;
    - COVERAGE=$(grep -o 'line-rate="[0-9.]*"' TestResults/coverage.cobertura.xml | head -1 | grep -o '[0-9.]*' | awk '{printf "%.1f", $1*100}')
    - echo "Total | ${COVERAGE}%"
  artifacts:
    when: always
    reports:
      junit: "TestResults/test-results.xml"
      coverage_report:
        coverage_format: cobertura
        path: "TestResults/coverage.cobertura.xml"
    paths:
      - TestResults/
    expire_in: 1 week
  coverage: '/Total\s*\|\s*(\d+(?:\.\d+)?)%/'

Conclusion

I hope this helps others save time when setting up code coverage for their .NET projects on GitLab. The key insight is that GitLab's coverage regex works on console output, not on the files (XML or other formats).

If you have any questions or suggestions, feel free to reach out!


~frank



How to Have GitLab CI/CD for a .NET Aspire Project

Getting a complete CI/CD pipeline for your .NET Aspire solution doesn't have to be complicated. I've created a template that gives you everything you need to get started in minutes.

(blog post en français ici)

Watch the Video


Part 1: The Ready-to-Use Template

I've built a .NET Aspire template that comes with everything configured and ready to go. Here's what you get:

What's Included

  • A classic .NET Aspire Starter project (API and frontend)
  • Unit tests using xUnit (easily adaptable to other testing frameworks)
  • Complete .gitlab-ci.yml pipeline configuration
  • Security scanning and secret detection
  • All documentation you need

What the Pipeline Does

The pipeline runs two main jobs automatically:

  1. Build: Compiles your code
  2. Test: Runs all unit tests, scans for vulnerabilities, and checks for accidentally committed secrets (API keys, passwords, etc.)

You can see all test results directly in GitLab's interface, making it easy to track your project's health.

How to Get Started

It's simple:

  1. Clone the template repository: cloud5mins/aspire-template
  2. Replace the sample project with your own .NET Aspire code
  3. Push to your GitLab repository
  4. Watch your CI/CD pipeline run automatically

That's it! You immediately get automated builds, testing, and security scanning.

Pro Tip: The best time to set up CI/CD is when you're just starting your project because everything is still simple.


Part 2: Building the Template with GitLab Duo

Now let me share my experience creating this template using GitLab's AI assistant, GitLab Duo.

Starting Simple, Growing Smart

I didn't build this complex pipeline all at once. I started with something very basic and used GitLab Duo to gradually add features. The AI helped me:

  • Add secret detection when I asked: "How can I scan for accidentally committed secrets?"
  • Fix test execution issues when my unit tests weren't running properly
  • Optimize the pipeline structure for better performance
screen capture in VSCode using GitLab Duo to change the default location for the job SAST

Working with GitLab in VS Code

While you can edit .gitlab-ci.yml files directly in GitLab's web interface, I prefer VS Code. Here's my setup:

  1. Install the official GitLab extension from the VS Code marketplace

Once you've signed in, this extension gives you:

  • Direct access to GitLab issues and work items
  • AI-powered chat with GitLab Duo

GitLab Duo in Action

GitLab Duo became my pair programming partner. Here's how I used it:

Understanding Code: I could type /explain and ask Duo to explain what any part of my pipeline configuration does by highlighting that section.

screen capture in VSCode using GitLab Duo to explain part of the code

Solving Problems: When my solution didn't compile, I described the issue to Duo and got specific suggestions. For example, it helped me realize some projects weren't in .NET 9 because dotnet build required the Aspire workload. I could either keep my project in .NET 8 and add a before_script instruction to install the workload or upgrade to .NET 9; I picked the latest.

Adding Features: I started with just build and test, then incrementally asked Duo to help me add security scanning, secret detection, and better error handling.

Adding Context: Using /include to add the project file or the .gitlab-ci.yml file while asking questions helped Duo understand the context better.

Learn More with the Docs: During my journey, I knew Duo wasn't just making things up as it was referencing the documentation. I could continue my learning there and read more examples of how before_script is used in different contexts.

The AI-Assisted Development Experience

What impressed me most was how GitLab Duo helped me learn while building. Instead of just copying configurations from documentation, each conversation taught me something new about GitLab CI/CD best practices.

Conclusion

I think this template can be useful for anyone starting a .NET Aspire project. Ready to try it? Clone the template at cloud5mins/aspire-template and start building with confidence.

Whether you're new to .NET Aspire or CI/CD, this template gives you a good foundation. And if you want to customize it further, GitLab Duo is there to help you understand and modify the configuration.

If you think we should add more features or improve the template, feel free to open an issue in the repository. Your feedback is always welcome!

[Screen capture of the Aspire template project on GitLab




Thanks to ‪David Fowler‬ for his feedback!

How to convert code with GitHub Copilot, can AI really help?

Recently, someone asked me an interesting question: "Can GitHub Copilot or AI help me convert an application from one language to another?" My answer was a definitive yes! AI can not only help you write code in a new language, but it can also improve team collaboration and bridge the knowledge gap between developers who know different programming languages.

(Version française ici)

Setting Up the Environment

To demonstrate this capability, I decided to convert a COBOL application to Java—a perfect test case since I don't know either language well, which means I really needed Copilot to do the heavy lifting. All the code is available on GitHub.

The first step was setting up a proper development environment. I used a dev container and asked Copilot to help me build it. I also asked for recommendations on the best VS Code extensions for Java development. Within just a few minutes, I had a fully configured environment ready for Java development.

Choosing the Right Copilot Agent

When working with GitHub Copilot for code conversion, you have different mode to choose from:

  • Ask: Great for general questions (like asking about Java extensions)
  • Edit: Perfect for simple document editing (like modifying the generated code)
  • Agent: The powerhouse for complex tasks involving multiple files, imports, and structural changes

For code conversion projects, the Agent is your best friend. It can look at different source files, understand project structure, edit code, and even create new files on your behalf.

The Conversion Process

I used Claude 3.5 Sonnet for this conversion. Here's the simple prompt I used:

"Convert this hello business COBOL application into Java"

Copilot didn't just convert the code, it also provided detailed information about how to execute the Java application, which was invaluable since I had no Java experience.

The results varied depending on the AI model used (Claude, GPT, Gemini, etc.), but the core functionality remained consistent across different attempts. Since the original application was simple, I converted it multiple times using different prompts and models to test the consistency. Sometimes it generated a single file, other times it created multiple files: a main application and an Employee class (which wasn't in my original COBOL version). Sometimes it updated the Makefile to allow compilation and execution using make, while other times it provided instructions to use javac and java commands directly.

This variability is expected with generative AI results will differ between runs, but the core functionality remains reliable.

Real-World Challenges

Of course, the conversion wasn't perfect on the first try. For example, I encountered runtime errors when executing the application. The issue was with the data format—the original file used a flat file format with fixed length records (19 characters per record) and no line breaks.

I went back to Copilot, highlighted the error message from the terminal, and provided additional context about the 19 character record format. This iterative approach is key to successful AI assisted conversion.

"It's not working as expected, check the error in #terminalSelection. The records have fixed length of 19 characters without line breaks. Adjust the code to handle this format"

The Results

After the iterative improvements, my Java application successfully:

  • Compiled without errors
  • Processed all employee records
  • Generated a report with employee data
  • Calculated total salary (a nice addition that wasn't in the original)

While the output format wasn't identical to the original COBOL version (missing leading zeros, different line spacing), the core functionality was preserved.

Video Demonstration

Watch the complete conversion process in action:

Best Practices for AI-Assisted Code Conversion

Based on this experience, here are my recommendations:

1. Start with Small Pieces

Don't try to convert thousands of lines at once. Break your conversion into manageable modules or functions.

2. Set Up Project Standards

Consider creating a .github folder at your project root with an instructions.md file containing:

  • Best practices for your target language
  • Patterns and tools to use
  • Specific versions and frameworks
  • Enterprise standards to follow

3. Stay Involved in the Process

You're not just a spectator - you're an active participant. Review the changes, test the output, and provide feedback when things don't work as expected.

4. Iterate and Improve

Don't expect perfection on the first try. In my case, the converted application worked but produced slightly different output formatting. This is normal and expected, after all you are converting between two different languages with different conventions and styles.

Can AI Really Help with Code Conversion?

Absolutely, yes! GitHub Copilot can significantly:

  • Speed up the conversion process
  • Help with syntax and language specific patterns
  • Provide guidance on running and compiling the target language
  • Bridge knowledge gaps between team members
  • Generate supporting files and documentation

However, remember that it's generative AI, results will vary between runs, and you shouldn't expect identical output every time.

Final Thoughts

GitHub Copilot is definitely a tool you need in your toolkit for conversion projects. It won't replace the need for human oversight and testing, but it will dramatically accelerate the process and help teams collaborate more effectively across different programming languages.

The key is to approach it as a collaborative process where AI does the heavy lifting while you provide guidance, context, and quality assurance. Start small, iterate often, and don't be afraid to ask for clarification or corrections when the output isn't quite right.

Have you tried using AI for code conversion? I'd love to hear about your experiences in the comments below! Visit c5m.ca/copilot to get started with GitHub Copilot.

References


Stop Writing Git Commits: How AI-Powered GitKraken CLI Accelerates Your Development

As developers, we're constantly looking for tools that can help us stay in the flow and be more productive. Today, I want to share a powerful tool that's been gaining traction in the developer community: GitKraken CLI. This command-line interface brings together several key features that modern developers love - it's AI-powered, terminal-based, and incredibly efficient for managing Git workflows.

(Version française ici)

What Makes GitKraken CLI Special?

GitKraken CLI (accessible via the gk command) stands out because it simplifies complex Git workflows while adding intelligent automation. Unlike traditional Git commands, it provides a more intuitive workflow management system that can handle multiple repositories simultaneously.

Getting Started

Installation is straightforward. On Windows, you can install it using:

winget install gitkraken.cli

Once installed, you'll have access to the gk command, which becomes your gateway to streamlined Git operations.

The Workflow in Action

Let's walk through a typical development session using GitKraken CLI:

1. Starting a Work Session

Instead of manually creating branches and switching contexts, you can start a focused work session:

gk w start "Add Behind my Cloud feed" -i "Add Behind my Cloud feed #1"

This single command:

  • Creates a new branch based on your issue/feature name
  • Switches to that branch automatically
  • Links the work session to a specific issue
  • Sets up your development environment for focused work

2. Managing Multiple Work Sessions

You can easily see all your active work sessions:

gk w list

This is particularly powerful when working across multiple repositories or juggling several features simultaneously.

3. Committing with Intelligence

After making your changes, adding files works as expected:

gk add .

But here's where the AI magic happens. Instead of writing commit messages manually:

gk w commit --ai

The AI analyzes your changes and generates meaningful, descriptive commit messages automatically. No more "quick fix" or "update stuff" commits!

4. Pushing and Creating Pull Requests

Publishing your work is equally streamlined:

gk w push

And when you're ready to create a pull request:

gk w pr create --ai

Again, AI assistance helps generate appropriate PR titles and descriptions based on your work.

5. Wrapping Up

Once your work is complete and merged, clean up is simple:

gk w end

This command:

  • Switches you back to the main branch
  • Deletes the feature branch, locally and on GitHub
  • Closes the work session
  • Leaves your repository clean and ready for the next task
all the commands


Why This Matters

The beauty of GitKraken CLI lies in its ability to keep you in the zone. You don't need to:

  • Switch between multiple tools
  • Remember complex Git commands
  • Write commit messages from scratch
  • Manually manage branch lifecycle

Everything flows naturally from one command to the next, maintaining your focus on what matters most: writing code.

Multi-Repository Power

One of the standout features is GitKraken CLI's ability to manage multiple repositories simultaneously. This is invaluable for:

  • Microservices architectures
  • Full-stack applications with separate frontend/backend repos
  • Organizations with multiple related projects

Try It Yourself

GitKraken CLI is part of a broader suite of developer tools that GitKraken offers. The CLI itself is free to use, which makes it easy to experiment with and integrate into your workflow without any upfront commitment. If you find value in the CLI and want to explore their other tools, GitKraken has various products that might complement your development setup.

The learning curve is genuinely minimal since it builds on Git concepts you already know while adding helpful automation. I've found that even small workflow improvements can compound over time, especially when you're working on multiple projects or dealing with frequent context switching.

If you're curious about what else GitKraken offers beyond the CLI, you can explore their full product lineup here. For those who decide the Pro features would benefit their workflow, as an ambassador of GitKraken I can share my code to provide a 50% discount for your GitKraken Pro subscription.

The combination of AI assistance and intuitive commands addresses real pain points that many developers face daily. Whether GitKraken CLI becomes a core part of your toolkit will depend on your specific workflow, but it's worth trying given that it's free and takes just a few minutes to set up.



The best tools are the ones that get out of your way and let you focus on building. GitKraken CLI aims to do exactly that.

I Co-Wrote 88 Unit Tests Using AI: A Developer's Journey

Testing has always been one of those tasks that developers know is essential but often find tedious. When I decided to add comprehensive unit tests to my NoteBookmark project, I thought: why not make this an experiment in AI-assisted development? What followed was a fascinating 4-hour journey that resulted in 88 unit tests, a complete CI/CD pipeline, and some valuable insights about working with AI coding assistants.

(Version française ici)

The Project: NoteBookmark

NoteBookmark is a .NET application built with C# that helps users manage and organize their reading notes and bookmarks. The project includes an API, a Blazor frontend, and uses Azure services for storage. You can check out the complete project on GitHub.

The Challenge: Starting from Zero

I'll be honest - it had been a while since I'd written comprehensive unit tests. Rather than diving in myself, I decided to see how different AI models would approach this task. My initial request was deliberately vague: "add a test project" without any other specifications.

Looking back, I realize I should have been more specific about which parts of the code I wanted covered. This would have made the review process easier and given me better control over the scope. But sometimes, the best learning comes from letting the AI surprise you.

The Great AI Model Comparison



GPT-4.1: Competent but Quiet

GPT-4.1 delivered decent results, but the experience felt somewhat mechanical. The code it generated was functional, but I found myself wanting more context. The explanations were minimal, and I often had to ask follow-up questions to understand the reasoning behind certain test approaches.

Gemini: The False Start

My experience with Gemini was... strange. Perhaps it was a glitch or an off day, but most of what was generated simply didn't work. I didn't persist with this model for long, as debugging AI-generated code that fundamentally doesn't function defeats the purpose of the exercise. Note that at the time of this writing, Gemini was still in preview, so I expect it to improve over time.

Claude Sonnet: The Clear Winner

This is where the magic happened. Claude Sonnet became my co-pilot of choice for this project. What set it apart wasn't just the quality of the code (though that was excellent), but the quality of the conversation. It felt like having a thoughtful colleague thinking out loud with me.

The explanations were clear and educational. When Claude suggested a particular testing approach, it would explain why. When it encountered a complex scenario, it would walk through its reasoning. I tried different versions of Claude Sonnet but didn't notice significant differences in results - they were all consistently good.

The Development Process: A 4-Hour Journey


Hour 1-2: Getting to Compilation

The first iteration couldn't compile. This wasn't surprising given the complexity of the codebase and the vague initial request. But here's where the AI collaboration really shined. Instead of manually debugging everything myself, I worked with Copilot to identify and fix issues iteratively.

We went through several rounds of:

  1. Identify compilation errors
  2. Discuss the best approach to fix them
  3. Let the AI implement the fixes
  4. Review and refine

After about 2 hours, we had a test project with 88 unit tests that compiled successfully. The AI had chosen xUnit as the testing framework, which I was happy with - it's a solid choice that I might not have picked myself if I was rusty on the current .NET testing landscape.

Hour 2.5-3.5: Making Tests Pass

Getting the tests to compile was one thing; getting them to pass was another challenge entirely. This phase taught me a lot about both my codebase and xUnit features I wasn't familiar with.

I relied heavily on the /explain feature during this phase. When tests failed, I'd ask Claude to explain what was happening and why. This was invaluable for understanding not just the immediate fix, but the underlying testing concepts.

One of those moment was learning about [InlineData(true)] and other xUnit data attributes. These weren't features I was familiar with, and having them explained in context made them immediately useful.


InlineData in the code


Hour 3.5-4: Structure and Style

Once all tests were passing, I spent time ensuring I understood each test and requesting structural changes to match my preferences. This phase was crucial for taking ownership of the code. Just because AI wrote it doesn't mean it should remain a black box. Let's repeat this: Understanding the code is essential; just because AI wrote it doesn't mean it's good.

Beyond Testing: CI/CD Integration

With the tests complete, I asked Copilot to create a GitHub Actions workflow to run tests on every push to main and v-next branches, plus PR reviews. Initially it started modifiying my existing workflow that takess care of the Azure deployment. I wanted a separate workflow for testing, so I interrupted (that's nice I wasn't "forced" to wait), and asked it to create a new one instead. The result was the running-unit-tests.yml workflow that worked perfectly on the first try.

This was genuinely surprising. CI/CD configurations often require tweaking, but the generated workflow handled:

  • Multi-version .NET setup
  • Dependency restoration
  • Building and testing
  • Test result reporting
  • Code coverage analysis
  • Artifact uploading

Code coverage


The PR Enhancement Adventure

Here's where things got interesting. When I asked Copilot to enhance the workflow to show test results in PRs, it started adding components, then paused and asked if it could delete the current version and start from scratch.

I said yes, and I'm glad I did. The rebuilt version created beautiful PR comments showing:

  • Test results summary
  • Code coverage reports (which I didn't ask for but appreciated)
  • Detailed breakdowns.

PR display


The Finishing Touches

No project is complete without proper status indicators. I added a test status badge to the README, giving anyone visiting the repository immediate visibility into the project's health.

test status badge


Key Takeaways


What Worked Well

  1. AI as a Learning Partner: Having Copilot explain testing concepts and xUnit features was like having a patient teacher
  2. Iterative Refinement: The back-and-forth process felt natural and productive
  3. Comprehensive Solutions: The AI didn't just write tests; it created a complete testing infrastructure
  4. Quality Over Speed: While it took 4 hours, the result was thorough and well-structured

What I'd Do Differently

  1. Be More Specific Initially: Starting with clearer scope would have streamlined the process
  2. Set Testing Priorities: Identifying critical paths first would have been valuable
  3. Plan for Visual Test Reports: Thinking about test result visualization from the start

Lessons About AI Collaboration

  • Model Choice Matters: The difference between AI models was significant
  • Conversation Quality Matters: Clear explanations make the collaboration more valuable
  • Trust but Verify: Understanding every piece of generated code is crucial
  • Embrace Iteration: The best results come from multiple refinement cycles

The Bigger Picture

This experiment reinforced my belief that AI coding assistants are most powerful when they're true collaborators rather than code generators. The value wasn't just in the 88 tests that were written, but in the learning that happened along the way.

For developers hesitant about AI assistance in testing: this isn't about replacing your testing skills, it's about augmenting them. The AI handles the boilerplate and suggests patterns, but you bring the domain knowledge and quality judgment.

Conclusion

Would I do this again? Absolutely. The combination of comprehensive test coverage, learning opportunities, and time efficiency made this a clear win. The 4 hours invested created not just tests, but a complete testing infrastructure that will pay dividends throughout the project's lifecycle.

If you're considering AI-assisted testing for your own projects, my advice is simple: start the conversation, be prepared to iterate, and don't be afraid to ask "why" at every step. The goal isn't just working code - it's understanding and owning that code.

The complete test suite and CI/CD pipeline are available in the NoteBookmark repository if you want to see the results of this AI collaboration in action.


Full-Stack Azure Deployment Made Easy: Containers, Databases, and More

Automating deployments is something I always enjoy. However, it's true that it often takes more time than a simple "right-click deploy." Plus, you may need to know different technologies and scripting languages.

(Version française ici)

But what if there was a tool that could help you write everything you need—Infrastructure as Code (IaC) files, scripts to copy files, and scripts to populate a database? In this post, we'll explore how the Azure Developer CLI (azd) can make deployments much easier.

What do we want to do?

Our goal: Deploy the 2D6 Dungeon App to Azure Container Apps.

This .NET Aspire solution includes:

  • A frontend
  • A data API
  • A database

Aspire resources schema


The Problem

In a previous post, we showed how azd up can easily deploy web apps to Azure.

If we try the same command for this solution, the deployment will be successful—but incomplete:

  • The .NET Blazor frontend is deployed perfectly.
  • However, the app fails when trying to access data.
  • Looking at the logs, we see the database wasn't created or populated, and the API container fails to start.

Let's look more closely at these issues.

The Database

When running the solution locally, Aspire creates a MySQL container and executes SQL scripts to create and populate the tables. This is specified in the AppHost project:

var mysql = builder.AddMySql("sqlsvr2d6")
                   .WithLifetime(ContainerLifetime.Persistent);
                
var db2d6 = mysql.AddDatabase("db2d6");

mysql.WithInitBindMount(source: "../../database/scripts", isReadOnly: false);

When MySQL starts, it looks for SQL files in a specific folder and executes them. Locally, this works because the bind mount is mapped to a local folder with the files.

However, when deployed to Azure:

  • The mounts are created in Azure Storage Files
  • The files are missing!

The Data API

This project uses Data API Builder (dab). Based on a single config file, a full data API is built and hosted in a container.

Locally, Aspire creates a DAB container and reads the JSON config file to create the API. This is specified in the AppHost project:

var dab = builder.AddDataAPIBuilder("dab", ["../../database/dab-config.json"])
                .WithReference(db2d6)
                .WaitFor(db2d6);

But once again, when deployed to Azure, the file is missing. The DAB container starts but fails to find the config file.

Logs of DAB failing to start


The Solution

The solution is simple: the SQL scripts and DAB config file need to be uploaded into Azure Storage Files during deployment.

You can do this by adding a post-provision hook in the azure.yaml file to execute a script that uploads the files. See an example of a post-provision hook in this post.

Alternatively, you can leverage azd alpha features: azd.operations and infraSynth.

  • azd.operations extends the provisioning providers and will upload the files for us.
  • infraSynth generates the IaC files for the entire solution.

💡Note: These features are in alpha and subject to change.

Each azd alpha feature can be turned on individually. To see all features:

azd config list-alpha

To activate the features we need:

azd config set alpha.azd.operations on
azd config set alpha.infraSynth on

Let's Try It

Once the azd.operation feature is activated, any azd up will now upload the files into Azure. If you check the database, you'll see that the db2d6 database was created and populated. Yay!

However, the DAB API will still fail to start. Why? Because, currently, DAB looks for a file, not a folder, when it starts. This can be fixed by modifying the IaC files.

One Last Step: Synthesize the IaC Files

First, let's synthesize the IaC files. These Bicep files describe the required infrastructure for our solution.

With the infraSynth feature activated, run:

azd infra synth

You'll now see a new infra folder under the AppHost project, with YAML files matching the container names. Each file contains the details for creating a container.

Open the dab.tmpl.yaml file to see the DAB API configuration. Look for the volumeMounts section. To help DAB find its config file, add subPath: dab-config.json to make the binding more specific:

containers:
    - image: {{ .Image }}
      name: dab
      env:
        - name: AZURE_CLIENT_ID
          value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }}
        - name: ConnectionStrings__db2d6
          secretRef: connectionstrings--db2d6
      volumeMounts:
        - volumeName: dab-bm0
          mountPath: /App/dab-config.json
          subPath: dab-config.json
scale:
    minReplicas: 1
    maxReplicas: 1

You can also specify the scaling minimum and maximum number of replicas if you wish.

Now that the IaC files are created, azd will use them. If you run azd up again, the execution time will be much faster—azd deployment is incremental and only does "what changed."

The Final Result

The solution is now fully deployed:

  • The database is there with the data
  • The API works as expected
  • You can use your application!
2D6 Dungeon App deployed


Bonus: Deploying with CI/CD

Want to deploy with CI/CD? First, generate the GitHub Action (or Azure DevOps) workflow with:

azd pipeline config

Then, add a step to activate the alpha feature before the provisioning step in the azure-dev.yml file generated by the previous command.

- name: Extends provisioning providers with azd operations
  run: azd config set alpha.azd.operations on     

With these changes, and assuming the infra files are included in the repo, the deployment will work on the first try.

Conclusion

It's exciting to see how tools like azd are shaping the future of development and deployment. Not only do they make the developer's life easier today by automating complex tasks, but they also ensure you're ready for production with all the necessary Infrastructure as Code (IaC) files in place. The journey from code to cloud has never been smoother!

If you have any questions or feedback, I'm always happy to help—just reach out on your favorite social media platform.

In Video

Here the video version of this blog post.


References


Converting a Blazor WASM to FluentUI Blazor server

TL;DR: This post walks through migrating a Blazor WebAssembly project to FluentUI Blazor server, highlighting key improvements in UI, authentication, and containerization using Azure Container Apps and .NET Aspire.

(👓Version en français ici

Why Migrate?

The migration to FluentUI Blazor server brought three major benefits:

  • 🎨 Modern UI with FluentUI components
  • 🔒 Simplified authentication using Azure Container Apps
  • 🚀 Better development experience with .NET Aspire

In this post, I'm sharing my journey while migrating a Blazor WebAssembly (WASM) project to a FluentUI Blazor server project. The goal was to use the new FluentUI Blazor components library, take advantage of .NET Aspire and be able to execute the project in a container.

Recently, I've been working on the migration of AzUrlShortener. Upgrading SDKs and packages, improving the security, and changing the architecture of the solution. This post is part of a series of posts where I share a few interesting details, tips, and tricks I learned while working on this project.

AzUrlShortener is an Open source project that consist of simple URL shortener that I built a few years ago. The goal was simple: having a budget friendly solution to share short URL that would be secure, easy to use and where the data would stay mine. Each instance is hosted in Azure and consist of an API (Azure Function), an Blazor WebAssembly website (Azure Static Web App), and Data Storage (Azure Storage table).

This post is part of a series about modernizing the AzUrlShortener project:

Migration Strategy: Fresh Start vs. Refactor

When migrating an existing project, you have two options: Editing the existing project to reshaping it into the new type or creating a new project and copy-pasting pieces of code from the old project to the new one. In this case, I chose to create a new project and copy-paste the code. This way, I could keep the old project as a backup in case something went wrong.

Creating a New Project

Like mentioned earlier I wanted to use the new FluentUI Blazor components library. It's an open-source project that provides a set of components for building web applications using the Fluent Design System. It makes it easy to create beautiful and responsive user interfaces that are consistent. To create a new project we can use the available template.

dotnet new fluentblazor -n Cloud5mins.ShortenerTools.TinyBlazorAdmin

Dark Mode & Theming Support 🌙

The one thing I do to all my FluentUI Blazor projects is to add a settings page. This page allows the user to change the theme and color of the application. I should do a template to save time, but until then here the required code to add the settings page.

Settings.razor

Let's start by creating that new page called Settings.razor. With two selects, one for the theme (dark or light) and one for the accent color.

@page "/settings"

@using Microsoft.FluentUI.AspNetCore.Components.Extensions

@rendermode InteractiveServer

<FluentDesignTheme @bind-Mode="@Mode"
				   @bind-OfficeColor="@OfficeColor"
				   StorageName="theme" />

<h3>Settings</h3>

<div>
	<FluentStack Orientation="Orientation.Vertical">
		<FluentSelect   Label="Theme" Width="150px"
						Items="@(Enum.GetValues<DesignThemeModes>())"
						@bind-SelectedOption="@Mode" />
		<FluentSelect   Label="Color"
						Items="@(Enum.GetValues<OfficeColor>().Select(i => (OfficeColor?)i))"
			Height="200px" Width="250px" @bind-SelectedOption="@OfficeColor">
			<OptionTemplate>
				<FluentStack>
					<FluentIcon Value="@(new Icons.Filled.Size20.RectangleLandscape())" Color="Color.Custom"
						CustomColor="@(@context.ToAttributeValue() != "default" ? context.ToAttributeValue() : "#036ac4" )" />
					<FluentLabel>@context</FluentLabel>
				</FluentStack>
			</OptionTemplate>
		</FluentSelect>
	</FluentStack>
</div>

@code {
    public DesignThemeModes Mode { get; set; }
    public OfficeColor? OfficeColor { get; set; }
}

App.razor

In the App it self, we need to some JavaScript and the loading theme component. Just after the </body> tag, we need to add the following code:

<!-- Set the default theme -->

<script src="_content/Microsoft.FluentUI.AspNetCore.Components/js/loading-theme.js" type="text/javascript"></script>

<loading-theme storage-name="theme"></loading-theme>

Imports.razor

I noticed some warning in the code about missing using directives. To fix that, find the line that reference to Components.Icons in the _Imports.razor and change it by the following. The Icons alias should resolve the problem.

@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons

MainLayout.razor

The main layout is our base page by default. We need to add Mode and OfficeColor to make the accessible to the entire application.

@code {
    public DesignThemeModes Mode { get; set; }
    public OfficeColor? OfficeColor { get; set; }
}

NavMenu.razor

The only thing left is to be able to easily access this new page. This can be done simply by adding an option in the navigation menu.

<FluentNavLink Href="/settings" Match="NavLinkMatch.All" Icon="@(new Icons.Regular.Size20.TextBulletListSquareSettings())">Settings</FluentNavLink>

Test it

And voilà! You should now have a settings page that allows you to change the theme and color of the application. This is all great and it's not really related to the migration, but it's a nice addition to have. Dark mode for the win!

The migration

The migration itself had many little pieces, but wasn't that complex. The project is part of a .NET Aspire solution, so I added the project to the solution dotnet sln add ./Cloud5mins.ShortenerTools.TinyBlazorAdmin. Then added the references to Cloud5mins.ShortenerTools.Core (the class library with all the model, and services) and the ServiceDefault project that was generated when we added Aspire to the solution.

The next logical step was to add our Blazor project the the orchestrator with those lines in the Program.cs of the AppHost project.

builder.AddProject<Projects.Cloud5mins_ShortenerTools_TinyBlazorAdmin>("admin")
	.WithExternalHttpEndpoints()
	.WithReference(manAPI);

This will make sure the TinyBlazorAdmin project starts with a reference to the API and have accessible endpoints. Without the .WithExternalHttpEndpoints() the project wouldn't be accessible once deployed to Azure.

To use the capability of .NET Aspire to orchestrate the different projects, we need to replace our previous HttpClient creation in the Program.cs of the TinyBlazorAdmin by the following code:

builder.Services.AddHttpClient<UrlManagerClient>(client => 
{
    client.BaseAddress = new Uri("https+http://api");
});

This will make sure the UrlManagerClient receives an httpClient using the correct address and port when calling the API. Let's have a look at the UrlManagerClient class and one of the method that will be used to call the API.

public class UrlManagerClient(HttpClient httpClient)
{

	public async Task<IQueryable<ShortUrlEntity>?> GetUrls()
    {
		IQueryable<ShortUrlEntity> urlList = null;
		try{
			using var response = await httpClient.GetAsync("/api/UrlList");
			if(response.IsSuccessStatusCode){
				var urls = await response.Content.ReadFromJsonAsync<ListResponse>();
				urlList = urls!.UrlList.AsQueryable<ShortUrlEntity>();
			}
		}
		catch(Exception ex){
			Console.WriteLine(ex.Message);
		}
        
		return urlList;
    }
	// ...
}

As the code shows the httpClient is injected in the constructor and used to call the API. The GetUrls method is a simple GET request that returns a list of ShortUrlEntity. The API is the one created in a previous post: How to use Azure Storage Table with .NET Aspire and a Minimal API, and all the classes are part of the Cloud5mins.ShortenerTools.Core project.

The URL Grid

Part of the migration was also to replace the Syncfusion grid by the new FluentUI Blazor Grid. Not that Syncfusion controls are not great, quite the contrary, but because the AzUrlShortener project has moved to a different owner, I think it would be better to use components that required no licenses.

For this initial iteration, the Syncfusion grid will be replace by the FluentUI Blazor Grid. In a future iteration the Syncfusion Charts component will also be replace. Thank you Syncfusion for the community license used in this project.

The code of UrlManager.razor changed quite a lot as the to grid were a bit different in there naming and usage. The sorting required a bit more code as the column name are not the same as the property name. To provide an example the "Vanity" column is in fact the RowKey property of the ShortUrlEntity class. To be able to sort the column, we need to create a GridSort object that will be used in the TemplateColumn definition.

Definition of the column in the grid:

<TemplateColumn Title="Vanity" Width="150px" Sortable="true" SortBy="@sortByVanities">
    <FluentAnchor Href="@context!.ShortUrl" Target="_blank" Aearance="Appearance.Hypeext">@context!.RowKey</FluentAnchor>
</TemplateColumn>

Definition of the GridSort object:

GridSort<ShortUrlEntity> sortByVanities = GridSort<ShortUrlEntity>.ByAscending(p => p.RowKey);

One big improvement that could be done in the future would be to use the virtual grid. The virtual grid is a great way to improve the performance of the grid when dealing with large amount of data as it only loads the data that is visible on the screen. I show how to use the virtual grid in a previous post: How use a Blazor QuickGrid with GraphQL.

Removing the fake popup div

One of the FluentUI Blazor component is the FluentUIDialogue. This component is used to display a popup window, and will help us keeping the code more structure and clean. Instead of having <div> in the code, we can typed <FluentUIDialog> and it will be rendered as a popup.

var dialog = await DialogService.ShowDialogAsync<NewUrlDialog>(shortUrlRequest, new DialogParameters()
	{
		Title = "Create a new Short Url",
		PreventDismissOnOverlayClick = true,
		PreventScroll = true
	});




Replacing the Authentication

Instead of having to implementing the authentication in the Blazor project, we will be using the a feature of Azure Container Apps that required no code changes! You don't need to change a single line of code to secure your application deployed on Azure Container Apps (ACA)! Instead, your application is automatically protected simply by enabling the authentication feature, called EasyAuth.

Once the solution is deployed to Azure the TinyBlazorAdmin will be installed in a container app named "admin". To secured it, navigate to the Azure Portal, and select the Container App you want to secure. In this case, it will be the "admin" container app. From the left menu, select Authentication and click Add identity provider.

You can choose between multiple providers, but let's use Microsoft since it's deployed in Azure and you are already logged in. Once Microsoft is chosen, you will see many configuration options. Select the recommended client secret expiration (e.g., 180 days). You can keep all the other default settings. Click Add. After a few seconds, you should see a notification in the top right corner that the identity provider was added successfully.

Voila! Your app now has authentication. Next time you navigate to the app, you will be prompted to log in with your Microsoft account. Notice that your entire app is protected. No page is accessible without authentication.

Conclusion

The migration from Blazor WebAssembly to FluentUI Blazor Server has been a successful journey that brought several meaningful improvements to the project:

  • Enhanced user interface with modern FluentUI components
  • Cleaner, more maintainable code structure
  • Simplified authentication using Azure Container Apps' EasyAuth
  • Improved local development experience with .NET Aspire orchestration

The end result is a modern, containerized application that's both easier to maintain and more pleasant to use. The addition of dark mode support and theming capabilities are great improvements to the user experience.

Want to Learn more?

To learn more about Azure Container Apps I strongly suggest this repository: Getting Started .NET on Azure Container Apps, it contains many step-by-step tutorials (with videos) to learn how to use Azure Container Apps with .NET.

Have questions about the migration process or want to share your own experiences with FluentUI Blazor? Feel free to reach out to me on @fboucheros.bsky.social or open an issue on the AzUrlShortener GitHub repository. I'd love to hear your thoughts!


References

Making AI smarter with an MCP server that manages short URLs

Have you ever wanted to give your AI assistants access to your own custom tools and data? That's exactly what Model Context Protocol (MCP) allows us to do, and I've been experimenting with it lately.

(Version française ici)

I read a lot recently about Model Context Protocol (MCP) and how it is changing the way AI interacts with external systems. I was curious to see how it works and how I can use it in my own projects. There are many tutorial available online but one of my favorite was written by James Montemagno Build a Model Context Protocol (MCP) server in C#. This post isn't a tutorial, but rather a summary of my experience and what I learned along the way while building a real MCP server that manages short URLs.

MCP doesn't change AI itself, it's a protocol that helps your AI model to interact with external resources: API, databases, etc. The protocol simplifies the way AI can access an external system, and it allows the AI to discover the available tools from those resources. Recently I was working on a project that manages short URLs, and I thought it would be a great opportunity to build an MCP server that manages short URLs. I wanted to see how easy it is to build and then use it in VSCode with GitHub Copilot Chat.

Code: All the code of this post is available in the branch exp/mcp-server of the AzUrlShortener repo on GitHub.

Setting Up: Adding an MCP Server to a .NET Aspire Solution

The AzUrlShortener is a web solution that uses .NET Aspire, so the first thing I did was create a new project using the command:

dotnet new web -n Cloud5mins.ShortenerTools.MCPServer -o ./mcpserver

Required Dependencies

To transform this into an MCP server, I added these essential NuGet packages:

  • Microsoft.Extensions.Hosting
  • ModelContextProtocol.AspNetCore

Since this project is part of a .NET Aspire solution, I also added references to:

  • The ServiceDefaults project (for consistent service configuration)
  • The ShortenerTools.Core project (where the business logic lives)

Integrating with Aspire

Next, I needed to integrate the MCP server into the AppHost project, which defines all services in our solution. Here's how I added it to the existing services:

var manAPI = builder.AddProject<Projects.Cloud5mins_ShortenerTools_Api>("api")
						.WithReference(strTables)
						.WaitFor(strTables)
						.WithEnvironment("CustomDomain",customDomain)
						.WithEnvironment("DefaultRedirectUrl",defaultRedirectUrl);

builder.AddProject<Projects.Cloud5mins_ShortenerTools_TinyBlazorAdmin>("admin")
		.WithExternalHttpEndpoints()
		.WithReference(manAPI);

// 👇👇👇 new code for MCP Server
builder.AddProject<Projects.Cloud5mins_ShortenerTools_MCPServer>("mcp")
		.WithReference(manAPI)
		.WithExternalHttpEndpoints();

Notice how I added the MCP server with a reference to the manAPI - this is crucial as it needs access to the URL management API.

Configuring the MCP Server

To complete the setup, I needed to configure the dependency injection in the program.cs file of the MCPServer project. The key part was specifying the BaseAddress of the httpClient:

var builder = WebApplication.CreateBuilder(args);       
builder.Logging.AddConsole(consoleLogOptions =>
{
    // Configure all logs to go to stderr
    consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services.AddMcpServer()
    .WithTools<UrlShortenerTool>();

builder.AddServiceDefaults();

builder.Services.AddHttpClient<UrlManagerClient>(client => 
            {
                client.BaseAddress = new Uri("https+http://api");
            });
            
var app = builder.Build();

app.MapMcp();

app.Run();

That's all that was needed! Thanks to .NET Aspire, integrating the MCP server was straightforward. When you run the solution, the MCP server starts alongside other projects and will be available at http://localhost:{some port}/sse. The /sse part of the endpoint means (Server-Sent Events) and is critical - it's the URL that AI assistants will use to discover available tools.

Implementing the MCP Server Tools

Looking at the code above, two key lines make everything work:

  1. builder.Services.AddMcpServer().WithTools<UrlShortenerTool>(); - registers the MCP server and specifies which tools will be available
  2. app.MapMcp(); - maps the MCP server to the ASP.NET Core pipeline

Defining Tools with Attributes

The UrlShortenerTool class contains all the methods that will be exposed to AI assistants. Let's examine the ListUrl method:

[McpServerTool, Description("Provide a list of all short URLs.")]
public List<ShortUrlEntity> ListUrl()
{
	var urlList = _urlManager.GetUrls().Result.ToList<ShortUrlEntity>();
	return urlList;
}

The [McpServerTool] attribute marks this method as a tool the AI can use. I prefer keeping tool definitions simple, delegating the actual implementation to the UrlManager class that's injected in the constructor: UrlShortenerTool(UrlManagerClient urlManager).

The URL Manager Client

The UrlManagerClient follows standard HttpClient patterns. It receives the pre-configured httpClient in its constructor and uses it to communicate with the API:

public class UrlManagerClient(HttpClient httpClient)
{
	public async Task<IQueryable<ShortUrlEntity>?> GetUrls()
    {
		IQueryable<ShortUrlEntity> urlList = null;
		try{
			using var response = await httpClient.GetAsync("/api/UrlList");
			if(response.IsSuccessStatusCode){
				var urls = await response.Content.ReadFromJsonAsync<ListResponse>();
				urlList = urls!.UrlList.AsQueryable<ShortUrlEntity>();
			}
		}
		catch(Exception ex){
			Console.WriteLine(ex.Message);
		}
        
		return urlList;
    }

	// other methods to manage short URLs
}

This separation of concerns keeps the code clean - tools handle the MCP interface, while the client handles the API communication.

Using the MCP Server with GitHub Copilot Chat

Now for the exciting part - connecting your MCP server to GitHub Copilot Chat! This is where you'll see your custom tools in action.

Configuring Copilot to Use Your MCP Server

Once the server is running (either deployed in Azure or locally), follow these steps:

  1. Open GitHub Copilot Chat in VS Code
  2. Change the mode to Agent by clicking the dropdown in the chat panel
  3. Click the Select Tools... button, then Add More Tools
Set GitHub Copilot mode to Agent

Selecting the Connection Type

GitHub Copilot supports several ways to connect to MCP servers:

All MCP Server types

There are multiple options available - you could have your server in a container or run it via command line. For our scenario, we'll use HTTP.

Note: At the time of writing this post, I needed to use the HTTP URL of the MCP server rather than HTTPS. You can get this URL from the Aspire dashboard by clicking on the resource and checking the available Endpoints.

After selecting your connection type, Copilot will display the configuration file, which you can modify anytime.

GitHub Copilot Chat Configuration

Interacting with Your Custom Tools

Now comes the fun part! You can interact with your MCP server in two ways:

  1. Natural language queries: Ask questions like "How many short URLs do I have?"
  2. Direct tool references: Use the pound sign to call specific tools: "With #azShortURL list all URLs"

The azShortURL is the name we gave to our MCP server in the configuration.

GitHub Copilot question and response example


Key Learnings and Future Directions

Building this MCP server for AzUrlShortener taught me several valuable lessons:

What Worked Well

  • Integration with .NET Aspire was remarkably straightforward
  • The attribute-based approach to defining tools is clean and intuitive
  • The separation of tool definitions from implementation logic keeps the code maintainable

Challenges and Considerations

  • The csharp-SDK is only a few weeks old and still in preview
  • OAuth authentication isn't defined yet (though it's being actively worked on)
  • Documentation is present but evolving rapidly as the technology matures, so some features may not be fully documented yet

For the AzUrlShortener project specifically, I'm keeping this MCP server implementation in the experimental branch mcp-server until I can properly secure it. However, I'm already envisioning numerous other scenarios where MCP servers could add great value.

If you're interested in exploring this technology, I encourage you to:

  • Check out the GitHub repo
  • Fork it and create your own MCP server
  • Experiment with different tools and capabilities

Join the Community

If you have questions or want to share your experiences with others, I invite you to join the Azure AI Community Discord server:

Join Azure AI Community Discord

The MCP ecosystem is growing rapidly, and it's an exciting time to be part of this community!


~Frank