Monday, December 9, 2024

Make an ASP.NET Core Controller API Culture-Sensitive with IValueProviderFactory

In the last article, "Are Your ASP.NET Core Routes and Query Strings Culture-Invariant?", we looked at a globalization issue that came up because ASP.NET Core model binding for route and query string data is culture-invariant. In that article, we solved the issue by making the route data culture-invariant, but we have another option: make the ASP.NET Core controller API culture sensitive.

IValueProviderFactory lets us use culture-sensitive route and query string data.

Note: This methodology applies to controller APIs only. I have not found an equivalent method that works with minimal APIs (but be sure to tell me if you know of one). For minimal APIs, use a culture-invariant URL.

The code for this article is available on GitHub: https://github.com/jeremybytes/aspnetcore-api-globalization.

Globalization Issue

Check out the previous article for a full description of the issue. For this article, we will go back to square one and show the basics of the globalization issue.

With the regional format settings on my machine set to United States English, the application runs fine.

  (From Controller Service) Sunset Tomorrow: 12/8/2024 4:26:50 PM
  (From Minimal API Service) Sunset Tomorrow: 12/8/2024 4:26:50 PM

But when I change the regional format to Icelandic, we get a runtime error.

If we debug to get the URL and run that manually, we see that the service returns an error:


Here is the URL and the results:

  http://localhost:8973/SolarCalculator/45,6382/-122,7013/2024-12-08

  {"results":null,"status":"ERROR"}

As a reminder, the 2 numeric route parameters represent latitude and longitude. Since we are using the Icelandic regional format, the comma (",") is used as a decimal separator. This is incorrectly parsed (and ignored) by the API. The result is that the latitude and longitude values are out of range.

If you want the full details, see the previous article.

IValueProviderFactory

A Microsoft Learn article (Globalization behavior of model binding route data and query strings) tells us what the problem is. But in addition to telling us that the model binding in this scenario is culture-invariant, it also tells how to make the model binding culture-sensitive -- by using IValueProviderFactory.

In short, we create custom value provider factories for route and query string data, then replace the default factories in the API startup.

Custom IValueProviderFactory

Here is the code for 2 custom value provider factories (in the GitHub repo CultureFactories.cs file):

  public class CultureQueryStringValueProviderFactory : IValueProviderFactory
  {
      public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
      {
          ArgumentNullException.ThrowIfNull(context);

          var query = context.ActionContext.HttpContext.Request.Query;
          if (query?.Count > 0)
          {
              context.ValueProviders.Add(
                  new QueryStringValueProvider(
                      BindingSource.Query,
                      query,
                      CultureInfo.CurrentCulture));
          }

          return Task.CompletedTask;
      }
  }

  public class CultureRouteValueProviderFactory : IValueProviderFactory
  {
      public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
      {
          ArgumentNullException.ThrowIfNull(context);

          context.ValueProviders.Add(
              new RouteValueProvider(
                  BindingSource.Path,
                  context.ActionContext.RouteData.Values,
                  CultureInfo.CurrentCulture));

          return Task.CompletedTask;
      }
  }

These classes create value providers that include culture information (set to CultureInfo.CurrentCulture) for the query string value provider and route value provider, respectively.

Replacing the Default Value Provider Factories

The last step is to replace the default value provider factories with the custom factories. This happens in the Program.cs file of the controller API.

We replace the existing AddControllers:

    builder.Services.AddControllers();

With one that takes an options delegate parameter:

  builder.Services.AddControllers(options =>
  {
      var index = options.ValueProviderFactories.IndexOf(
          options.ValueProviderFactories
              .OfType<QueryStringValueProviderFactory>()
              .Single());

      options.ValueProviderFactories[index] =
          new CultureQueryStringValueProviderFactory();

      index = options.ValueProviderFactories.IndexOf(
          options.ValueProviderFactories
              .OfType<RouteValueProviderFactory>()
              .Single());

      options.ValueProviderFactories[index] =
          new CultureRouteValueProviderFactory();
  });

This code finds the index of the existing value provider factory in the options and replaces it with the custom value provider factory (once for the query string and once for the route).

Working API

With the value provider factories in place, we can re-run the API using the same URL and get working results:


Here is the URL and results:

  http://localhost:8973/SolarCalculator/45,6382/-122,7013/2024-12-08

  {"results":{"sunrise":"07:38:51","sunset":"16:26:50","solar_noon":"12:02:50",
  "day_length":"08:47:58.5373487"},"status":"OK"}

The route parameters are still using the Icelandic regional format (45,6382 and -122,7013), but now we have valid results.

The application is using the current culture for the values when creating the route, and the API is using the current culture to read the route data.

And the application is back in a working state as well:

  (From Controller Service) Sunset Tomorrow: 8.12.2024 16:26:50
  (From Minimal API Service) Sunset Tomorrow: 8.12.2024 16:26:50

Two Approaches

Now that we have 2 approaches to solving the same globalization issue, which should we use? Here are my thoughts. I'd love to hear your thoughts as well.

In General: Culture-Invariant

I understand why route and query string data is culture-invariant by default. As stated in the documentation, this is to try to make URLs as "neutral" as possible to support the largest number of clients.

The implication of this is that now I feel like I need to specifically mark all of my API calls as culture-invariant since its possible that my application code could be running on a machine with different regional format settings.

I have not been doing this -- instead relying on the US-centric default. Going forward, I need to make sure that my API calls are appropriately culture-invariant.

For Specific Cases: Culture-Sensitive

For the specific scenario that brought this issue up, I'm leaning toward making the APIs culture-sensitive.

The scenario is that I have workshop lab files that run on a user's machine. Both the API calling code and the API itself are on the same machine. My inclination is to make sure that both ends use the current culture of the machine. This is what I assumed (very incorrectly) was happening and why the issue caught me off guard.

But I'm not completely set on this yet; it's just my initial inclination.

Wrap Up

For those of us in the US, we often forget about globalization issues. This is because we are the default culture -- even when we specify "neutral" culture (i.e., culture-invariant), it is still our culture.

Now that we are aware that route and query string data for APIs is culture-invariant, we need to take some action: either ensuring that our routes and query strings are culture-invariant or by adjusting the API itself.

Ultimately, it comes down to whether to put the onus on the client (API calling code) or the service (the API). Let me know your thoughts.

Happy Coding!


Sunday, December 8, 2024

Are Your ASP.NET Core Routes and Query Strings Culture-Invariant?

In a recent workshop, I ran into an interesting issue with ASP.NET Core and globalization. One of the attendees was from Iceland, and she was getting errors when running an application hitting a local API. Later, I was able to hunt down the globalization issue, and it has to do with the way model binding works.

Model binding in ASP.NET Core for route and query string data is culture-invariant.

This caught me a bit off guard. I expected that the globalization settings on the computer would be used throughout the code. But because of the culture-invariant behavior of route and query string data in ASP.NET Core, things do not work that way.

Note: This specifically refers to model binding for route and query string data (i.e., data that is part of a URL). Model binding for form data is culture sensitive.

If you are biased toward United States English (as too many of us are), this issue does not come up locally. But if your API calls are made from non-US culture machines, you may run into issues if the routes or query strings are not specifically created as culture-invariant.

In this article, we'll take a look at making sure that our API calls are culture-invariant. In a future article, we'll look at how we can add a custom value provider that respects CultureInfo settings. (Update: Article is now available: Make an ASP.NET Core Controller API Culture-Sensitive with IValueProviderFactory.)

Source code for this article is available on GitHub: https://github.com/jeremybytes/aspnetcore-api-globalization.

The workshop code that brought this issue to light is available here: https://github.com/jeremybytes/vslive2024-orlando-workshop-labs.

Happy Path for US English

We'll start with the happy path. The scenario is that I need to get sunset information for my home automation software. The application calls a (local) service that provides sunset time based on latitude, longitude, and date. (For purposes of the sample code in the workshop, the service is run on the local machine. In the original implementation, this was a third-party service.)

Application Output

Running the application produces the following output.


  (From Controller Service) Sunset Tomorrow: 12/7/2024 4:26:56 PM
  (From Minimal API Service) Sunset Tomorrow: 12/7/2024 4:26:56 PM

This shows "tomorrow" sunset time as 4:26 p.m. (this is where I live in Vancouver, WA, USA). And since I am in the US, the date represents December 7, 2024.

API Call

Here is the API call that produces the sunset data:

Here is the URL:
    http://localhost:8973/SolarCalculator/45.6382/-122.7013/2024-12-07

And the JSON result:

  {"results":{"sunrise":"7:37:52 AM","sunset":"4:26:56
  PM","solar_noon":"12:02:24 PM","day_length":"08:49:03.8810000"},"status":"OK"}

The route in the URL has the latitude (45.6382), the longitude (-122.7013), and the date (2024-12-07).

The latitude and longitude values are culturally significant.

The URL / route is created in the code:


  string endpoint =
      $"SolarCalculator/{latitude:F4}/{longitude:F4}/{date:yyyy-MM-dd}";
  HttpResponseMessage response = 
      client.GetAsync(endpoint).Result;

The formatting on the latitude and longitude parameters specify that they should include 4 decimal places.

Changing Culture to Iceland

To duplicate the issue, I changed my computer's "Regional format" to Icelandic in the "Language & Region" settings (I'm using Windows 11).


This changes the number format to use a comma (",") for the decimal separator and a dot (".") for the thousands separator.

Re-running the application throws an error at this point. We can trace through the application to generate the URL and run it manually:

Here is the URL and the JSON result:

  http://localhost:8973/SolarCalculator/45,6382/-122,7013/2024-12-07
  {"results":null,"status":"ERROR"}

This shows the latitude value as "45,6382" and longitude as "-122,7013". These are the proper formats for the Icelandic regional format setting.

However, we see that the API now returns an error. For some reason, it does not work with these route parameters.

API Route Model Binding

It took a bit of debugging to find what was happening. Ultimately, I ended up in the HTTPGet code of the API controller.

Note: Minimal APIs exhibit this same behavior. This is not specific to Controller APIs.

Here is the endpoint:

    [HttpGet("{lat}/{lng}/{date}")]
    public SolarData GetWithRoute(double lat, double lng, DateTime date)
    {
        var result = SolarCalculatorProvider.GetSolarTimes(date, lat, lng);
        return result;
    }

When debugging into this method, I found that the "lat" and "lng" parameters did not have the values I expected:


  lat = 456382
  lng = -1227013
  date = (7.12.2024 00:00:00)

This is where I hit the unexpected. The decimal separator (",") from the route was not being used.

Route & Query Strings are Culture-Invariant

After much digging, I found a relevant section on the Microsoft Learn site (https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-9.0#globalization-behavior-of-model-binding-route-data-and-query-strings):

Globalization behavior of model binding route data and query strings
The ASP.NET Core route value provider and query string value provider:
o Treat values as invariant culture.
o Expect that URLs are culture-invariant.
In contrast, values coming from form data undergo a culture-sensitive conversion. This is by design so that URLs are shareable across locales.

Culture-invariant means US English (because the world revolves around the US </sarcasm>). So, the comma is not recognized as a decimal separator here and ends up being ignored.

The result in this code is that the values overflow latitude and longitude values (which range from -180 to +180). So the API returns an error result.

The sample code also has an HTTPGet that uses a query string rather than a route. When I was investigating the problem, I tried this endpoint and had the same issue as the endpoint that used route parameters. As noted in the documentation above, both route parameters and query string parameters are mapped the same way -- with invariant culture.

Creating a Culture-Invariant API Call

Based on what we know now, one solution is to make sure that the parameters of our API calls are culture-invariant.

Here is the revised calling code:

  string endpoint = string.Format(
                      CultureInfo.InvariantCulture, 
                      "SolarCalculator/{0:F4}/{1:F4}/{2:yyyy-MM-dd}",
                      latitude, longitude, date);

Note that we cannot use the default string interpolation like we had previously. There are ways to create a string that respects culture while using string interpolation, but I find it easier to fall back to String.Format. This is probably out of habit, but it's an approach that works. You can use whatever method you like to create a culture-invariant string.

Working Application with Icelandic Regional Format

Now that we have an invariant culture specified on the string, the URL will be formatted just like the original US English version that we started with (using a dot as the decimal separator).

Here is the resulting application output:


  (From Controller Service) Sunset Tomorrow: 7.12.2024 16:26:56
  (From Minimal API Service) Sunset Tomorrow: 7.12.2024 16:26:56

This output also shows the Icelandic general date format: day dot month dot year.

Another Alternative

Instead of using a culture-invariant route / query string, we can also update the service so that it is culture sensitive. How to do this is described in the same Microsoft Learn article mentioned above (https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-9.0#globalization-behavior-of-model-binding-route-data-and-query-strings).

I have done this as well. The general idea is that you create a custom IValueProviderFactory that parses the parameters based on the current culture (or other desired culture). You do this for both query string values and route values. Then in the "AddControllers" part of the API builder process, you replace the default route and query string value providers with the custom value providers. (Update: Article is now available: Make an ASP.NET Core Controller API Culture-Sensitive with IValueProviderFactory.)

This solution only works with Controller APIs. I have been unable to find a similar option for Minimal APIs.

The sample code has an implementation of this, and I'll go through it in a future article.

The Important Question

So the important question is "Are your ASP.NET Core route and query strings culture-invariant?" If not, you may need to take a look at them. You may not need to worry about it if you are using an unambiguous date format (like the year/month/day format used here). But if your parameters have decimals or thousands separators, then you will want to make sure that those parameters are represented in a culture-invariant way.

Wrap Up

Localization and globalization are important topics. I haven't touched on localization here (where we make sure that the text in our applications is available in other languages). In my particular situation (coding workshops), the workshops are conducted in English, and so the application text can also be in English.

But when it comes to globalization, folks use different number and date formats. I'm often conscious of date formats when I present outside of the US (since we have the worst of all "standard" date formats). But this is the first time I have run into an issue with globalization around a decimal separator.

I understand the desire to have URLs culture-invariant. But it also grates on me a bit. The idea of making the API culture sensitive seems like a better solution for my scenario. Stay tuned for the next article, and you can help me decide by telling me which option you prefer.

Happy Coding!

Saturday, December 7, 2024

Trying and Failing with GitHub Copilot

Or, How Jeremy failed to create a Triangle Classifier with appropriate tests.

So, I spent about 2 hours trying to get GitHub Copilot (specifically GitHub Copilot chat within Visual Studio 2022) to create a function and test suite as described in The Art of Software Testing by Glenford J. Myers (1st edition, Wiley. & Sons, 1979).

I failed miserably.

In the end, I have a function that looks pretty close to what I was trying to get and a suite of unit tests that do not all pass.

If you would like to enjoy the output of my failure, feel free to check out the code on GitHub: https://github.com/jeremybytes/copilot-triangle-testing.

Goal

The beginning of The Art of Software Testing has a self-assessment test to see how good you are at testing a function. Here is the text (from p. 1):

Before beginning this book, it is strongly recommended that you take the following short test. The problem is the testing of the following program:

The program reads three integer values from a card. The three values are interpreted as representing the lengths of the sides of a triangle. The program prints a message that states whether the triangle is scalene, isosceles, or equilateral.

On a sheet of paper, write a set of test cases (i.e., specific sets of data) that you feel would adequately test this program. When you have completed this, turn the page to analyze your tests.

The next page has a set of 14 test cases including adequate test coverage for the success cases as well as correctly dealing with potentially invalid parameter sets (sets that could not make a valid triangle, parameters out of range, and others).

Since it is no longer 1979, I asked for a C# method with 3 parameters (not "integer values from a card") and requested the output be an enum (rather than "print[ing] a message").

Process

I got the initial function up pretty quickly (including checks for out-of-range parameters and invalid parameter sets). And I spent a lot longer on the test suite. GitHub Copilot created the initial tests, and then I kept asking for adjustments for edge cases, out of range parameters, and invalid parameter sets. In the end, the tests do not pass. Some of the parameter sets are invalid (when they are meant to be out-of-range examples). I tried getting GitHub Copilot to reorder things so that the proper exceptions are tested for based on the parameter sets, but it was beyond its (or my) abilities. Eventually, GitHub Copilot kept providing the same code over and over again.

Note: In the initial request to create the function, GitHub Copilot used double for the parameters (rather than int as noted in the book). I left this since part of the testing scenarios in the book included non-integer values.

End State

The tests do not pass, and with the amount of time I spent trying to get GitHub Copilot to build what I wanted, I could have done it multiple times manually.

I could fix the code and tests manually, but this was to see exactly how useful GitHub Copilot is in a fairly straight-forward scenario. The answer is that it was just frustrating for me.

Feel free to tell me "you're doing it wrong", I've heard that before. But I would rather work with an intern that was brand new to programming that try to coerce GitHub Copilot into generating what I want.

The Code

Anyway, the code is in this repository if you're interested in looking at the final product (or more correctly, the code in its incomplete state when I finally gave up and had to ask myself how much electricity and clean water were used for this experiment).

I'm putting this out as something to point to; I'm not looking for corrections or advice at this point. (If I personally know you and you want to chat about it, feel free to contact me through the normal channels.)

Update: Cursory Analysis

So, I couldn't leave the code alone, so I went back a few days later to take a look at some of the issues.

The problem with using double

Let's start with a failing test:

    [Theory]
    [InlineData(1.1, 2.2, 3.3)] // Non-integer values
    // other test cases removed
    public void ClassifyTriangle_InvalidTriangles_ThrowsArgumentException(double side1, double side2, double side3)
    {
        // Arrange
        var classifier = new TriangleClassifier();

        // Act & Assert
        var exception = Assert.Throws<ArgumentException>(() => classifier.ClassifyTriangle(side1, side2, side3));
        Assert.Equal("The given sides do not form a valid triangle.", exception.Message);
    }

This particular test makes sure that the side lengths form a valid triangle. Adding any 2 sides together needs to be greater than the 3rd side. So, for example, lengths of 1, 2, 3 would be invalid because 1 + 2 is not greater than 3.

At first glance, the test case above (1.1, 2.2, 3.3) seems okay: 1.1 + 2.2 is not greater than 3.3. But when we look at the actual test that runs (and fails), we see the problem:

TriangleTests.TriangleClassifierTests.ClassifyTriangle_InvalidTriangles_ThrowsArgumentException(side1: 1.1000000000000001, side2: 2.2000000000000002, side3: 3.2999999999999998)

Because of the imprecision of the double type, adding side1 and side2 is greater than side3, so it does not throw the expected exception.

One answer to this is to change the double types to decimal types. This would ensure that the values of 1.1, 2.2, and 3.3 are precise.

Incorrect Overflow Check

Another strange part of the code is checking for overflow values. Here's the generated code:

    // Check for potential overflow in addition
    if (side1 > double.MaxValue - side2 || side1 > double.MaxValue - side3 || side2 > double.MaxValue - side3)
    {
        throw new ArgumentOutOfRangeException("Sides are too large.");
    }

The generated comment says that it is checking for potential overflow in addition; however, the conditions do not reflect that. Instead, they use MaxValue with subtraction. This is just weird. This code would not detect an overflow.

Tests for Overflow Check

Even stranger than the code that checks for overflow are the test cases for that code:

    [Theory]
    [InlineData(double.MaxValue / 2, double.MaxValue / 2, double.MaxValue / 2)] // Edge case: large double values
    [InlineData(double.MaxValue / 2, double.MaxValue / 2, 1)] // Edge case: large double values with a small side
    [InlineData(double.MaxValue / 2, 1, 1)] // Edge case: large double value with two small sides
    [InlineData(double.MinValue, double.MinValue, double.MinValue)] // Edge case: minimum double values
    [InlineData(double.MinValue, double.MinValue, -1)] // Edge case: minimum double values with a small negative side
    [InlineData(double.MinValue, -1, -1)] // Edge case: minimum double value with two small negative sides
    public void ClassifyTriangle_OverflowValues_ThrowsArgumentOutOfRangeException(double side1, double side2, double side3)
    {
        // Arrange
        var classifier = new TriangleClassifier();

        // Act & Assert
        var exception = Assert.Throws<ArgumentOutOfRangeE>ception>(() => classifier.ClassifyTriangle(side1, side2, side3));
        Assert.Equal("Sides are too large.", exception.ParamName);
    }

Every one of these test cases fails. This was after I asked GitHub Copilot chat to fix the failing tests. It said OK and then updated the test cases to the ones shows above.

Looking at the first case ("MaxValue / 2" for all 3 sides), This would not be expected to overflow double. In the valid triange check (noted previously), any 2 sides are added together. In theory, that means that adding any 2 of these sides would result in the MaxValue (i.e., not an overflow value). For this test case, no exception is thrown, so the test fails.

The test cases that use MinValue are completely invalid for this code. The code is checking for overflow (theoretically) but not for underflow. The test cases with MinValue all fail because the sides are less than or equal to zero (which is a separate check in the method). So these test cases will always fail because the wrong exception is thrown.

Updated Summary

What really worries me about the code is that my cursory analysis only looks at the failing tests in the test suite. This has led me to find invalid test cases and also code that doesn't do what it thinks it is doing.

So what if I were to dig through all of the passing tests? (There are 36 of them.) I expect that I will find the same issues: invalid test cases that lead to code that doesn't do what it thinks it does.

And that's the problem. Without any trust at all in the code, this is pretty useless. The last thing I need as part of my development process is a random excrement generator.

Happy Coding!

Wednesday, November 13, 2024

The C# "field" Keyword and Visual Studio Tooling

Today's frustration is with Visual Studio 2022 tooling and the release of C# 13/.NET 9.0. I know that these were released yesterday, but that is what made it frustrating.

Short Version:

Visual Studio 2022 (17.12.0) makes it look like the "field" keyword is usable. But it is only usable if you set the language version to "preview".

Note: this is specifically related to Visual Studio 2022 version 17.12.0 and .NET 9.0.100. I will keep an eye on future versions.

Quick Fix:

Set <LangVersion>preview</LangVersion> in the project settings.

The field Keyword and Semi-Auto Properties

First, let's look at the "field" keyword. It is a C# proposal that was a potential feature of C# 13. You can see the proposal here: https://github.com/dotnet/csharplang/issues/140.

The "field" keyword would allow us to create semi-automatic properties. If you do some searches online, you will find lots of articles about this proposed feature.

Automatic Properties

Today we have automatic properties that give us a shorthand for creating properties:

     public Customer OrderCustomer { get; set; }

When this is code is compiled, the compiler automatically creates a backing field with default methods for getting and setting the value. This saves us typing (and code) if we want default functionality.

If we want more functionality, then we would need to create a full property with backing field and custom getter and setter.

But the "field" keyword lets us do something a bit in between.

Semi-Automatic Property

Rather the creating a backing field manually, we can use the "field" keyword to represent the backing field in a getter or setter -- similar to how we use the "value" keyword to represent the incoming value in a setter.

Here's a sample of that:

    public Customer OrderCustomer
    {
        get;
        set => field = value ?? field;
    }

Here I have modified the setter of the property. In this example, I check to see if the incoming "value" is null. If it is not null, then I assign the value to the backing field. But if "value" is null, then I keep the backing field unchanged.

The "get" is still automatic, so it has the default behavior of returning whatever is in the backing field.

Visual Studio 2022 Tooling Issue

So, here's the tooling issue that I ran into. I had heard about the "field" keyword and wanted to do some experimentation with it. I generally do not install preview versions of .NET, so I did this on release day (Nov 12, 2024).

Updating Visual Studio 2022 got me version 17.12.0 (along with .NET version 9.0.100).

When I started typing, Visual Studio helpfully gave me code completion, along with a note that "field" was a "keyword".

Visual Studio popup with "field" as a completion option and note "field keyword"

This is what I expected. But when I completed the code, I got an error: "The name 'field' does not exist in the current context"

Code with "field" underlined in red with error "The name 'field' does not exist in current context"

This is when I went to do some searching and found that the GitHub proposal (linked above) is still "Open", meaning it is not yet complete.

The Fix

After some digging, I found that you can use the "field" keyword in Visual Studio 2022 (with .NET 9.0) as long as you have the language version set to "Preview".

The language version can be set as part of the project options:

	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net9.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
		<LangVersion>preview</LangVersion>
	</PropertyGroup>

When this value is set, the error goes away, and the code works as expected.

Full code to the semi-automatic property with no error messages.

Why I Care

I figure that if I ran into this issue that other folks may have as well. Messaging is really important to me, and if things are confusing, I tend to get frustrated.

From the Microsoft side, there is nothing in "What's new in C# 13" that would indicate that the "field" keyword is included. This is technically correct, but I wish that there was a little more as part of the "What's New" messaging (such as "What we mentioned but isn't ready yet" or "What's Still in Preview").

The reason I would like this additional messaging from Microsoft is that when the "field" keyword was proposed, there were a lot of articles written about it. And when it was available in the preview versions of .NET and C#, more articles were written about it.

I appreciate that the feature was held back from release -- this normally indicates that more work needs to be done, and I am glad that the language teams do whatever they can to make sure that they get the feature right.

But here I have Visual Studio 2022 telling me that "field" is a keyword in the code completion. But it is only valid when using the correct language preview setting. It would be great if Visual Studio could have the note "field Keyword (preview)" instead of "field Keyword". That would at least give me a clue that it is not yet valid.

Instead, Visual Studio is offering to auto-complete to code that will not compile.

Wrap Up

I expect that this will be fixed rather quickly. It may be that the feature is released out-of-band (meaning before C# 14).

In the meantime, you can set the language version to "preview" if you want to experiment with the "field" keyword and semi-automatic properties.

Happy Coding!

Sunday, July 14, 2024

"Very Bad Idea" prototype v0.1

One day, a couple years ago, Jeremy had a Very Bad Idea...

A wearable keyboard for on-stage presentations.

Here is the prototype in action:

The Problem

I speak at developer events pretty regularly; it's one of the things I really love to do. For a new talk, I spend a lot of time planning and practicing. And I work on getting the right amount of content for a time slot -- generally 60 minutes.

In June 2022, I was at the dev up conference in St. Charles, MO, and I gave a talk on golang: "Go for the C# Developer". I managed to mess up the timing on the talk -- I had to skip some of the planned content because otherwise the talk would have run long. (I don't think the audience noticed that I skipped anything, but I did.)

Note: I will be speaking at dev up again this year - August 14-16, 2024. Be sure to take a look at the event; it's always a lot of good content, good learning, and good people.

Back in the speaker room, I talked with some folks including Barry Stahl (https://cognitiveinheritance.com/) trying to figure out what went wrong. I was about 10 minutes off on my timing. I had done the talk several times before and been fine on the time. It really bothered me that I was so far off this time.

And then I figured out what went wrong: COVID.

No, I didn't have COVID. COVID moved developer events online for quite a while (much too long -- I really missed people during that time).

"Go for the C# Developer" was a new talk at the time. And although I had given the talk several times before, they had always been virtual presentations. This was the first time I had given the talk in person on-stage.

That's when I realized what really went wrong: I didn't take transit time into account.

I am a bit animated when talking on stage, and I tend to walk around. This particular talk is all demo, so I type out a few lines of code, then walk away from the podium to talk about what that code does, then walk back to the podium to write a few more lines of code.

What I didn't take into account for this talk was the time spent walking back to the podium to type more code.

The Idea

That's when I came up with the "Very Bad Idea": a wearable keyboard that I could use for presentations. The general design was to have a 2-part keyboard with the pieces placed fairly ergonomically so my hands would fall on them naturally.

Unfortunately, Barry encouraged me on the "Very Bad Idea".

I purchased a 2-part keyboard not long after this event. And I ran through a few thoughts on how to put things together. But I never built anything -- until now.

The Prototype

Materials

  • Kinesis Freestyle 2 Blue keyboard (link)
  • Logitech Ergo M575 Wireless Trackball (link)
  • LEGO bricks (link)
  • Ribbon
  • Bungee cord

Assembly

I needed a frame to hold the keyboard. I thought that LEGO bricks might work, but I am not the best builder. Then I came across the Space Roller Coaster set. When I was building the LEGO set, the base seemed quite sturdy and just what I needed. (Pieces of that base are in the photo above.)

That path was a dead end. Although the base was sturdy for what it was designed for, it did not hold up well to twisting motion. When I tried using it, the joints came apart pretty quickly. I tried a few modifications before deciding that that design was not the right way to go.

Then I had the idea to use MILS plates. This is a technique the LEGO community uses to build sturdy bases that can be moved without worrying about things flexing. I didn't use any specific designs -- just the general one of creating a sandwich of bricks and plates.

Here is the inside of the first plate I built sized for one half of the keyboard:


Then I built a second:


Then I assembled them with the keyboard:


There are quite a few things shown here. First, I opened the keyboard and ran ribbons through both halves. This provides a general way to attach the keyboard to something else. I ran the ribbon through the LEGO plates and sealed them up. Then I tied the ribbon at the end to hold the keyboard onto the plate.

Once I had the two halves, I connected them with some cross pieces. Here's the (almost) completed build.


The arrangement of the keyboard halves is the key to this particular design/idea. When I place this on my chest, my hands fall fairly naturally onto the keyboard.


You can get a better idea of how this works from the video at the top of the article.

It Works!

I'm actually surprised this worked as well as it did. I am pretty happy with this as a first prototype. There are still quite a few issues to address, but this is a start.

Here are some things to improve:

  • General sturdiness
    It's pretty sturdy, but pieces tend to come loose.
  • Keyboard position
    The position of the keyboard halves in relation to each other is not quite right. And a bit of an angle would be good, too.
  • Attachment points
    Bungee cords hooked to an unreliable connection points are not a great solution. My initial idea was to use a guitar strap or something similar.
  • Trackball mounting
    A usable solution needs a mouse/trackball/trackpad. Current thought is to mount the trackball mouse to the side of my leg (like a gunslinger).

Will I Actually Use This?

I would love to use a version of this on stage at least once. If nothing else, people would remember me (although I hope they remember me for the cool things that I show them in my coding demos).

I will also need to practice -- A LOT. I am a good touch typist (there's no way this would work if I wasn't). But I would need to do dozens (if not hundreds) of hours of practice in order to be proficient at using it. Programming is particularly interesting because there are a lot of symbols used. I also use a lot of multi-key shortcuts while programming. So, lots of practice.

I'll be sure to post updates, and we'll see if "Very Bad Idea" ever makes it into production (meaning, one of my presentations).

Happy Building!

Wednesday, April 24, 2024

Thoughts on Primary Constructors in C#

Primary constructors seems to be one of those polarizing features in C#. Some folks really love them; some folks really hate them. As in most cases, there is probably a majority of developers who are in the "meh, whatever" group. I have been experimenting and reading a bunch of articles to try to figure out where/if I will use them in my own code.

I have not reached any conclusions yet. I have a few examples of where I would not use them, and a few examples of where I would. And I've come across some scenarios where I can't use them (which affects usage in other areas because I tend to write for consistency).

Getting Started

To get started understanding primary constructors, I recommend a couple of articles:

The first article is on Microsoft devblogs, and David walks through changing existing code to use primary constructors (including using Visual Studio's refactoring tools). This is a good overview of what they are and how to use them.

In the second article, Marek takes a look at primary constructors and adds some thoughts. The "Initialization vs. capture" section was particularly interesting to me. This is mentioned in David's article as well, but Marek takes it a step further and shows what happens if you mix approaches. We'll look at this more closely below.

Very Short Introduction

Primary constructors allow us to put parameters on a class declaration itself. The parameters become available to the class, and we can use this feature to eliminate constructors that only take parameters and assign them to local fields.

Here's an example of a class that does not use a primary constructor:


public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }
    
    public IActionResult Index()
    {
        _logger.Log(LogLevel.Trace, "Index action called");
        return View();
    }
    // other code
}

In a new ASP.NET Core MVC project, the HomeController has a _logger field that is initialized by a constructor parameter. I have updated the code to use the _logger field in the Index action method.

The code can be shorted by using a primary constructor. Here is one way to do that:


public class HomeController(ILogger<HomeController> logger) 
    : Controller
{
    private readonly ILogger<HomeController> _logger = logger;
    
    public IActionResult Index()
    {
        _logger.Log(LogLevel.Trace, "Index action called");
        return View();
    }
    // other code
}

Notice that the class declaration now has a parameter (logger), and the separate constructor is gone. The logger parameter can be used throughout the class. In this case, it is used to initialize the _logger field, and we use the _logger in the Index action method just like before. (There is another way to handle this that we'll see a bit later).

So by using a primary constructor, we no longer need a separate constructor, and the code is a bit more compact.

Fields or no Fields?

As David notes in his article, Visual Studio 2022 offers 2 different refactoring options: (1) "Use primary constructor" and (2) "Use primary constructor (and remove fields)". (Sorry, I couldn't get a screen shot of that here because the Windows Snipping Tool was fighting me.)

The example above shows what happens if we choose the first option. Here is what we get if we chose the second (and remove the fields):


public class HomeController(ILogger<HomeController> logger) 
    : Controller
{
    public IActionResult Index()
    {
        logger.Log(LogLevel.Trace, "Index action called");
        return View();
    }
    // other code
}

In this code, the separate _logger field has been removed. Instead, the parameter on the primary constructor (logger) is used directly in the class. A parameter on the primary constructor is available throughout the class (similar to a class-level field), so we can use it anywhere in the class.

Guidance?

Here's where I hit my first question: fields or no fields? The main difference between the 2 options is that a primary constructor parameter is mutable.

This means that if we use the parameter directly (option 2), then it is possible to assign a new value to the "logger" parameter. This is not likely in this scenario, but it is possible.

If we maintain a separate field (option 1), then we can set the field readonly. Now the value cannot be changed after it is initialized.

One problem I ran into is that there is not a lot of guidance in the Microsoft documentation or tutorials. They present both options and note the mutability, but don't really suggest a preference for one over the other.

The Real Difference

Reading Marek's article opened up the real difference between these two approaches. (Go read the section on "Initialization vs. capture" for his explanation.)

The compiler treats the 2 options differently. With option 1 (use the parameter to initialize a field), the parameter is discarded after initialization. With option 2 (use the parameter directly in the class), the parameter is captured (similar to a captured variable in a lambda expression or anonymous delegate) and can be used throughout the class. 

As Marek notes, this is fine unless you try to mix the 2 approaches with the same parameter. For example, if we use the parameter to initialize a field and then also use the parameter directly in our code, we end up in a bit of a strange state. The field is initialized, but if we change the value of the parameter later, then the field and parameter will have different values.

If you try the mix and match approach in Visual Studio 2022, you will get a compiler warning. In our example, if we assign the logger parameter to a field and then use the logger parameter directly in the Index action method, we get the following warning:

Warning CS9124  Parameter 'ILogger<HomeController> logger' is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.

So this tells us about the danger, but it is a warning -- meaning the code will still compile and run. I would rather see this treated as an error. I pay pretty close attention to warnings in my code, but a lot of folks do not.

Fields or No Fields?

I honestly have not landed on which way I'll tend to go with this one. There may be other factors involved. I look at consistency and criticality below -- these will help me make my decisions.

Consistency and Dependency Injection

A primary use case for primary constructors is dependency injection -- specifically constructor injection. Often our constructors will take the constructor parameters (the dependencies) and assign them to local fields. This is what we've seen with the example using ILogger.

So, when I first started experimenting with primary constructors, I used my dependency injection sample code. Let's look at a few samples. You can find code samples on GitHub: https://github.com/jeremybytes/sdd-2024/tree/main/04-dependency-injection/completed, and I'll provide links to specific files.

Primary Constructor with a View Model

In a desktop application sample, I use constructor injection to get a data reader into a view model. This happens in the "PeopleViewModel" class in the "PeopleViewer.Presentation" project (link to the PeopleViewModel.cs file).


public class PeopleViewModel : INotifyPropertyChanged
{
    protected IPersonReader DataReader;

    public PeopleViewModel(IPersonReader reader)
    {
        DataReader = reader;
    }

    public async Task RefreshPeople()
    {
        People = await DataReader.GetPeople();
    }
    // other code
}

This class has a constructor parameter (IPersonReader) that is assigned to a protected field.

We can use a primary constructor to reduce the code a bit:


public class PeopleViewModel(IPersonReader reader) 
    : INotifyPropertyChanged
{
    protected IPersonReader DataReader = reader;

    public async Task RefreshPeople()
    {
        People = await DataReader.GetPeople();
    }
    // other code
}

This moves the IPersonReader parameter to a primary constructor.

Using a primary constructor here could be seen as a plus as it reduces the code.

As a side note: since the field is protected (and not private), Visual Studio 2022 refactoring does not offer the option to "remove fields".

Primary Constructor with a View?

For this application, the View needs to have a View Model injected. This happens in the "PeopleViewerWindow" class of the "PeopleViewer.Desktop" project (link to the PeopleViewerWindow.xaml.cs file).


public partial class PeopleViewerWindow : Window
{
    PeopleViewModel viewModel { get; init; }

    public PeopleViewerWindow(PeopleViewModel peopleViewModel)
    {
        InitializeComponent();
        viewModel = peopleViewModel;
        this.DataContext = viewModel;
    }

    private async void FetchButton_Click(object sender, RoutedEventArgs e)
    {
        await viewModel.RefreshPeople();
        ShowRepositoryType();
    }
    // other code
}

The constructor for the PeopleViewerWindow class does a bit more than just assign constructor parameters to fields. We need to call InitializeComponent because this is a WPF Window. In addition, we set a view model field and also set the DataContext for the window.

Because of this additional code, this class is not a good candidate for a primary constructor. As far as I can tell, it is not possible to use a primary constructor here. I have tried several approaches, but I have not found a way to do it.

Consistency

I place consistency pretty high on my list of qualities I want in my code. I learned this from a team that I spent many years with. We had about a dozen developers who built and supported a hundred applications of various sizes. Because we had a very consistent approach to our code, it was easy to open up any project and get to work. The consistency between the applications made things familiar, and you could find the bits that were important to that specific application.

I still lean towards consistency because humans are really good at recognizing patterns and registering things as "familiar". I want to take advantage of that.

So, this particular application, my tendency would be to not use primary constructors. Constructor injection is used throughout the application, and I would like it to look similar between the classes. I emphasize "this particular application" because my view could be different if primary constructors could be used across the DI bits.

Incidental vs. Critical Parameters

When it comes to deciding whether to have a class-level field, I've also been thinking about the difference between incidental parameters and critical parameters.

Let me explain with an example. This is an ASP.NET Core MVC controller that has 2 constructor parameters:


public class PeopleController : Controller
{
    private readonly ILogger<PeopleController> logger;
    private readonly IPersonReader reader;

    public PeopleController(ILogger<PeopleController> logger, 
        IPersonReader personReader)
    {
        this.logger = logger;
        reader = personReader;
    }

    public async Task<IActionResult> UseConstructorReader()
    {
        logger.Log(LogLevel.Information, "UseContructorReader action called");
        ViewData["Title"] = "Using Constructor Reader";
        ViewData["ReaderType"] = reader.GetTypeName();

        IEnumerable<Person> people = await reader.GetPeople();
        return View("Index", people.ToList());
    }
    // other code
}

The constructor has 2 parameters: an ILogger and an IPersonReader. In my mind, one of these is more important than the other.

IPersonReader is a critical parameter. This is the data source for the controller, and none of the controller actions will work without a valid IPersonReader.

ILogger is an incidental parameter. Yes, logging is important. But if the logger is not working, my controller could still operate as usual.

In this case, I might do something strange: use the ILogger parameter directly, and assign the IPersonReader to a field.

Here's what that code would look like:


public class PeopleController(ILogger<PeopleController> logger,
    IPersonReader personReader) : Controller
{
    private readonly IPersonReader reader = personReader;

    public async Task<IActionResult> UseConstructorReader()
    {
        logger.Log(LogLevel.Information, "UseContructorReader action called");
        ViewData["Title"] = "Using Constructor Reader";
        ViewData["ReaderType"] = reader.GetTypeName();

        IEnumerable<Person> people = await reader.GetPeople();
        return View("Index", people.ToList());
    }
    // other code
}

I'm not sure how much I like this code. The logger parameter is used directly (as noted above, this is a captured value that can be used throughout the class). The personReader parameter is used for initialization (assigned to the reader field).

Note: Even though it looks like we are mixing initialization and capture, we are not. The logger is captured, and the personReader is used for initialization. We are okay here because we have made a decision (initialize or capture) for each parameter, so we will not get the compiler warning here.

To me (emphasizing personal choice here), this makes the IPersonReader more obvious -- it has a separate field right at the top of the class. The assignment is very intentional.

In contrast, the ILogger is just there. It is available for use, but it is not directly called out.

These are just thoughts at this point. It does conflict a bit with consistency, but everything we do involves trade-offs.

Thoughts on Primary Constructors and C#

I have spent a lot of time thinking about this and experimenting with different bits of code. But my thoughts are not fully formed yet. If you ask me about this in 6 months, I may have a completely different view.

I do not find typing to be onerous. When it comes to code reduction, I do not think about it from the initial creation standpoint; I think about it from the readability standpoint. How readable is this code? Is it simply unfamiliar right now? Will it get more comfortable with use?

These are all questions that we need to deal with when we write code.

Idiomatic C#?

In Marek's article, he worries that we are near the end of idiomatic C# -- meaning a standard / accepted / recommended way to write C# code (see the section "End of idiomatic C#?"). Personally, I think that we are already past that point. C# is a language of options that are different syntactically without being different substantially.

Expression-Bodied Members

For example, whether we use expression-bodied members or block-bodied members does not matter to the compiler. They mean the same thing. But they are visually different to the developer, and human minds tend to think of them as being different.

var

People get too attached to whether to use var or not. I have given my personal views in the past (Demystifying the "var" Keyword in C#), but I don't worry whether anyone else adopts them. This has been treated as a syntax difference without a compiler difference. But that changed with the introduction of nullable reference types: var is now automatically nullable (C# "var" with a Reference Type is Always Nullable). There is still a question of whether developers need to care about this or not. I've only heard about one edge case so far.

Target-typed new

Another example is target-typed new expressions. Do you prefer "List<int>? ids = new()" or "var ids = new List<int>()"? The compiler does not care; the result is the same. But humans do care. (And your choice may be determined by how attached to "var" you are.)

No More Idiomatic C#

These are just 3 examples. Others include the various ways to declare properties, top level statements, and whether to use collection initialization syntax (and on that last one, Visual Studio has given me some really weird suggestions where "ToList()" is fine.)

In the end, we need to decide how we write C# moving forward. I have my preferences based on my history, experience, competence, approach to code, personal definition of readability, etc. And when we work with other developers, we don't just have to take our preferences into account, but also the preferences and abilities of the rest of the team. Ultimately, we each need to pick a path and move foreward.

Happy Coding!

Monday, April 22, 2024

Event Spotlight: Software Design & Development 2024 (May 2024)

Software Design & Development 2024

Dates: May 13-17, 2024
Location: Barbican Centre, London

In just a few weeks, this will be my 6th time at SDD, and I'm looking forward to a great week. Register by Friday, April 26 to save a bit. https://sddconf.com/register

My Sessions 

I will be presenting 5 breakout sessions, plus a full-day workshop. There's a theme to my talks this year: techniques of abstraction in C#, and how to (hopefully) get it right.

Abstract Art: Getting Abstraction "Just Right"
Abstraction is awesome, and abstraction is awful. Let's look at some techniques to get it right for each application.

IEnumerable, ISaveable, IDontGetIt: Understanding C# Interfaces
Interfaces (one type of abstraction) are a way to make our code more flexible, maintainable, and testable. We'll use lots of code to explore these benefits.

Practical Reflection in C#
Reflection has lots of uses (some safe and some not so safe). We'll look at the dangers, and also some specific use cases that allow us to extend applications without recompiling.

DI Why? Getting a Grip on Dependency Injection
Dependency Injection (and the associated patterns) let us write loosely coupled code. And this can make our applications easier to maintain, extend, and test. We'll see how this is important by looking at lots of code.

LINQ - It's not Just for Databases
Language Integrated Query (LINQ) is one of my favorite things in C#. Most people think it's mainly for databases, but I use it all the time to filter, sort, and group data in my applications without going back to the data source.

Take Your C# Skills to the Next Level -- Full-day Workshop
Let's make our software easy to extend, change, and debug. Interfaces, dependency injection, delegates, and unit testing can get us there. You'll leave with practical experience you can apply to your own projects.

Get more information on Software Design & Development 2024 here: https://sddconf.com/
And don't forget to register by Apr 26 for a discount: https://sddconf.com/register

Happy Coding!