Showing posts with label ASP.NET Core. Show all posts
Showing posts with label ASP.NET Core. Show all posts

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!

Thursday, February 22, 2024

Minimal APIs vs Controller APIs: SerializerOptions.WriteIndented = true

Another thing added in ASP.NET Core / .NET 7.0 (yeah, I know it's been a while since it was released), is the "ConfigureHttpJsonOptions" extension method that lets us add JSON options to the services collection / dependency injection container. The setting that caught my eye is "WriteIndented = true". This gives a nice pretty output for JSON coming back from our APIs.

This is the difference between:

{"id":3,"givenName":"Leela","familyName":"Turanga","startDate":"1999-03-28T00:00:00-08:00","rating":8,"formatString":"{1} {0}"}

and
{
  "id": 3,
  "givenName": "Leela",
  "familyName": "Turanga",
  "startDate": "1999-03-28T00:00:00-08:00",
  "rating": 8,
  "formatString": "{1} {0}"
}
You may not want this because of the extra whitespace characters that have to come down the wire. But for the things that I work with, I want the pretty printing!

The good news is that in .NET 7.0, the new "ConfigureHttpJsonOptions" method lets us set this up (among quite a few other settings).

To use it, we just add the options to the Services in the Program.cs file of our project.


    // Set JSON indentation
    builder.Services.ConfigureHttpJsonOptions(
        options => options.SerializerOptions.WriteIndented = true);

You can check the documentation for JsonSerializerOptions Class to see what other options are available.

But there's a catch:
ConfigureHttpJsonOptions does work for Minimal APIs.
ConfigureHttpJsonOptions does not work for Controller APIs. 
Let's take a look at that. For code samples, you can check out this repository: https://github.com/jeremybytes/controller-vs-minimal-apis.

Sample Application

The sample code contains a "MinimalApi" project and a "ControllerApi" project. These were both started as fresh projects using the "dotnet new webapi" template (and the appropriate flags for minimal/controller). Both projects get their data from a 3rd project ("People.Library") that provides some hard-coded data.

Here is the minimal API that provides the data above:


    app.MapGet("/people/{id}",
        async (int id, IPeopleProvider provider) => await provider.GetPerson(id))
        .WithName("GetPerson")
        .WithOpenApi();

And here's the controller API:


    [HttpGet("{id}", Name = "GetPerson")]
    public async Task<Person?> GetPerson(
        [FromServices] IPeopleProvider provider, int id)
    {
        // This may return null
        return await provider.GetPerson(id);
    }

Both of these call the "GetPerson" method on the provider and pass in the appropriate ID.

Testing the Output
To test the output, I created a console application (the "ServiceTester" project). I did this to make sure I was not getting any automatic JSON formatting from a browser or other tool.

I created an extension method on the Uri type to make calling easier. You can find the complete code in the RawResponseString.cs file.


    public static async Task<string> GetResponse(
        this Uri uri, string endpoint)

The insides of this method make an HttpClient call and return the response.

The calling code is in the Program.cs file of the ServiceTester project.


        Uri controllerUri = new("http://localhost:5062");
        Uri minimalUri = new("http://localhost:5194");

        // Minimal API Call
        Console.WriteLine("Minimal /people/3");
        var response = await minimalUri.GetResponse("/people/3");
        Console.WriteLine(response);

        Console.WriteLine("----------");

        // Controller API Call
        Console.WriteLine("Controller /people/3");
        response = await controllerUri.GetResponse("/people/3");
        Console.WriteLine(response);

        Console.WriteLine("----------");

This creates 2 URIs (one for each API), calls "GetResponse" for each and then outputs the string to the console.

This application was also meant to show some other differences between Minimal APIs and Controller APIs. These will show up in future articles.

Without "WriteIndented = true"

Here is the output of the ServiceTester application without making any changes to the settings of the API projects.

Note that both the MinimalApi and ControllerApi services need to be running in order for the ServiceTester application to work. (I start the services in separate terminal tabs just so I can keep them running while I do my various tests.)


Minimal /people/3
{"id":3,"givenName":"Leela","familyName":"Turanga","startDate":"1999-03-28T00:00:00-08:00","rating":8,"formatString":"{1} {0}"}
----------
Controller /people/3
{"id":3,"givenName":"Leela","familyName":"Turanga","startDate":"1999-03-28T00:00:00-08:00","rating":8,"formatString":"{1} {0}"}
----------

So let's change some settings.

"WriteIndented = true"

So let's add the options setting to both projects (MinimalApi/Program.cs and ControllerApi/Program.cs):


    // Set JSON indentation
    builder.Services.ConfigureHttpJsonOptions(
        options => options.SerializerOptions.WriteIndented = true);

Then we just need to restart both services and rerun the ServiceTester application:


    Minimal /people/3
    {
      "id": 3,
      "givenName": "Leela",
      "familyName": "Turanga",
      "startDate": "1999-03-28T00:00:00-08:00",
      "rating": 8,
      "formatString": "{1} {0}"
    }
    ----------
    Controller /people/3
    {"id":3,"givenName":"Leela","familyName":"Turanga","startDate":
    "1999-03-28T00:00:00-08:00","rating":8,"formatString":"{1} {0}"}
    ----------
This shows us that the pretty formatting is applied to the Minimal API output, but not to the Controller API output.

So What's Going On?

I found out about this new setting the same way as the one in the previous article -- by hearing about it in a conference session. I immediately gave it a try and saw that it did not work on the API I was experimenting with. After few more tries, I figured out that the setting worked with the Minimal APIs that I had, but not the Controller APIs. (And I did try to get it to work with the Controller APIs in a whole bunch of different ways.)

Eventually, I came across this snippet in the documentation for the "ConfigureHttpJsonOptions" method (bold is mine):
Configures options used for reading and writing JSON when using Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync and Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync. JsonOptions uses default values from JsonSerializerDefaults.Web.
This tells us that these options are only used in certain circumstances. In this case, specifically when WriteAsJsonAsync is called.

I have not been able to find what uses "WriteAsJsonAsync" and what does not. Based on my observations, I assume that Minimal APIs do use WriteAsJsonAsync and Controller APIs do not use WriteAsJsonAsync.

So I add another item to my list of differences between Minimal APIs and Controller APIs. I guess it's time to publish this list somewhere.

Wrap Up

So, back to the beginning:
ConfigureHttpJsonOptions does work for Minimal APIs.
ConfigureHttpJsonOptions does not work for Controller APIs. 
It would be really nice if these capabilities matched or if there was a note somewhere that said "This is for minimal APIs".

Hopefully this will save you a bit of frustration in your own code. If you want to use "WriteIntented = true", then you will need to use Minimal APIs (not Controller APIs).

Happy Coding!

Wednesday, February 21, 2024

Method Injection in ASP.NET Core: API Controllers vs. MVC Controllers

Method Injection in ASP.NET Core got a little bit easier in .NET 7. Before .NET 7, we had to use the [FromServices] attribute on a method parameter in order for the parameter to be injected from the services collection / dependency injection container. Starting with .NET 7, the [FromServices] parameter became optional, but only in some places.

Short Version:
[FromServices] is no longer required for API Controllers. 
[FromServices] is still required for MVC Controllers.
The reason I'm writing about this is that I had a bit of confusion when I first heard about it, and I suspect others may have as well. If you'd like more details, and some samples, keep reading.

Confusion

I first heard about [FromServices] being optional from a conference talk on ASP.NET Core. The speaker said that [FromServices] was no longer required for method injection and removed it from the sample code to show it still worked.

Now ASP.NET Core is not one of my focus areas, but I do have some sample apps that use method injection. When I tried this out for myself on an MVC controller, it did not work. After a bit of digging (and conversation with the speaker), we found that the change only applied to API controllers.

Let's look at some samples. This code shows a list of data from a single source -- either through an API or displayed in an MVC view. (Sample code is available here: https://github.com/jeremybytes/method-injection-aspnetcore)

Data and Service Configuration

Both projects use the same library code for the data. This code is in the "People.Library" folder/project.

IPeopleProvider:
    public interface IPeopleProvider
    {
        Task<List<Person>> GetPeople();
        Task<Person?> GetPerson(int id);
    }

The method we care about is the "GetPeople" method that returns a list of "Person" objects. There is also a "HardCodedPeopleProvider" class in the same project that implements the interface. This returns a hard-coded list of objects.

Both the API project and the MVC project configure the "IPeopleProvider" the same way. (API code: ControllerAPI/Program.cs -- MVC code: PeopleViewer/Program.cs


    // Add services to the container.
    builder.Services.AddScoped<IPeopleProvider, HardCodedPeopleProvider>();

This registers the HardCodedPeopleProvider with the dependency injection container so that we can inject it into our controllers.

API Controller

Using [FromServices]
Prior to .NET 7, we could inject something from our dependency injection container by using the [FromServices] attribute. Here's what that code looks like in the API controller (from the "PeopleController.cs" file)

    [HttpGet(Name = "GetPeople")]
    public async Task<IEnumerable<Person>> Get(
        [FromServices] IPeopleProvider provider)
    {
        return await provider.GetPeople();
    }

When we call this method, the "HardCodedPeopleProvider" is automatically used for the "provider" parameter.

The result is the following:


[{"id":1,"givenName":"John","familyName":"Koenig","startDate":"1975-10-17T00:00:00-07:00","rating":6,"formatString":""},{"id":2,"givenName":"Dylan","familyName":"Hunt","startDate":"2000-10-02T00:00:00-07:00","rating":8,"formatString":""},...,{"id":14,"givenName":"Devon","familyName":"","startDate":"1973-09-23T00:00:00-07:00","rating":4,"formatString":"{0}"}]

Note: I'm sorry about the JSON formatting. "SerializerOptions.WriteIndented" is a topic for another day.
Update: Here's an article on that: Minimal APIs vs Controller APIs: SerializerOptions.WriteIndented = true.

Removing [FromServices]
As noted, the "FromServices" is now optional:


    [HttpGet(Name = "GetPeople")]
    public async Task<IEnumerable<Person>> Get(
        IPeopleProvider provider)
    {
        return await provider.GetPeople();
    }

The output is the same as we saw above:


[{"id":1,"givenName":"John","familyName":"Koenig","startDate":"1975-10-17T00:00:00-07:00","rating":6,"formatString":""},{"id":2,"givenName":"Dylan","familyName":"Hunt","startDate":"2000-10-02T00:00:00-07:00","rating":8,"formatString":""},...,{"id":14,"givenName":"Devon","familyName":"","startDate":"1973-09-23T00:00:00-07:00","rating":4,"formatString":"{0}"}]
[FromServices] is no longer required for API Controllers.
Now let's take a look at an MVC controller.

MVC Controller

Using [FromServices]
The [FromServices] attribute lets us use method injection in MVC controllers as well. Here is an action method from the "PeopleViewer" project (from the "PeopleController.cs" file):


    public async Task<IActionResult> GetPeople(
        [FromServices] IPeopleProvider provider)
    {
        ViewData["Title"] = "Method Injection";
        ViewData["ReaderType"] = provider.GetType().ToString();

        var people = await provider.GetPeople();
        return View("Index", people);
    }

As with the API controller, the "HardCodedPeopleProvider" is automatically passed for the method parameter. Here is the output of the view:

Web browser showing output of People objects in a grid. Example item is "John Koenig 1975 6/10 Stars"


This shows the same data as the API in a colorful grid format (as a side note, the background color of each item corresponds with the decade of the date.)

Removing [FromServices]
Because we can remove [FromServices] from the API controller, I would guess that we can remove it from the MVC controller as well.


    public async Task<IActionResult> GetPeople(
        IPeopleProvider provider)
    {
        ViewData["Title"] = "Method Injection";
        ViewData["ReaderType"] = provider.GetType().ToString();

        var people = await provider.GetPeople();
        return View("Index", people);
    }

However, when we run this application, we get a runtime exception:


InvalidOperationException: Could not create an instance of type 'People.Library.IPeopleProvider'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Record types must have a single primary constructor. Alternatively, give the 'provider' parameter a non-null default value.

ASP.NET Core MVC does not automatically look in the dependency injection container for parameters that it cannot otherwise bind. So, without the [FromServices] attribute, this method fails.
[FromServices] is still required for MVC Controllers.

Messaging and Documentation Frustrations

If you've read this far, then you're looking for a bit more than just the "Here's how things are", so I'll give a bit of my opinions and frustrations.

I've had a problem with Microsoft's messaging for a while now. It seems like they are very good at saying "Look at this cool new thing" without mentioning how it differs from the old thing or what is not impacted by the new thing. (I had a similar experience with minimal APIs and behavior that is different from controller APIs but not really called out anywhere in the documentation: Returning HTTP 204 (No Content) from .NET Minimal API.)

I see the same thing happening with [FromServices].

Here is the documentation (from "What's New in ASP.NET Core 7.0")
Parameter binding with DI in API controllers
Parameter binding for API controller actions binds parameters through dependency injection when the type is configured as a service. This means it's no longer required to explicitly apply the [FromServices] attribute to a parameter.
Unfortunately, the "hype" shortens this message:
[FromServices] is no longer required.
Please don't write to me and say "It's obvious from the documentation this only applies to API controllers. There is no reason to believe it would apply to anything else." This is easy to say when you already know this. But what about if you don't know?

Let's look at it from an average developer's perspective. They have used API controllers (and may be using minimal APIs) and they have used MVC controllers. The methods and behaviors of these controllers are very similar: parameter mapping, routing, return types, and other bits. It is not a very far leap to assume that changes to how a controller works (whether API or MVC) would apply in both scenarios.

As noted above, the speaker I originally heard this from did not realize the limitations at the time, and this speaker is an expert in ASP.NET Core (including writing books and teaching multi-day workshops). They had missed the distinction as well.

And unfortunately, the documentation is lacking (at least as of the writing of this article in Feb 2024): 

  • The "FromServicesAttribute" documentation does not have any usage notes that would indicate that it is required in some places and optional in others. This is the type of note I expect to see (as an example  the UseShellExecute default value changed between .NET Framework and .NET Core, and it is noted in the language doc.)
  • The Overview of ASP.NET Core MVC documentation does have a "Dependency Injection" topic that mentions method injection. However, the corresponding Create web APIs with ASP.NET Core documentation does not have a "Dependency Injection" topic.

Stop Complaining and Fix It, Jeremy

I don't like to rant without having a solution. But this has been building up for a while now. The obvious answer is "Documentation is open source. Anyone can contribute. Update the articles, Jeremy."

If you know me, then you know that I have a passion for helping people learn about interfaces and how to use them appropriately. If I have "expertise" in any part of the C# language, it is interfaces. When it came to a set of major changes (specifically C# 8), I looked into them and wrote about them (A Closer Look at C# 8 Interfaces), and I have also spoken quite a bit about those changes: Caching Up with C# Interfaces, What You Know may be Wrong.

I have noted that the documentation is lacking in a lot of the areas regarding the updates that were made to interfaces. So why haven't I contributed?
I do not have enough information to write the documentation.
I can write about my observations. I can point to things that look like bugs to me. But I do not know what the feature is supposed to do. It may be working as intended (as I have discovered about "bugs" I have come across in the past). I am not part of the language team; I am not part of the documentation team; I do not have access to the resources needed to correctly document features.

Wrap Up

I will apologize for ranting without a solution. I was a corporate developer for many years. I understand the pressures of having to get code completed. I see how difficult it is to keep up with changing environments while releasing applications. I know what it is like to support multiple applications written over a number of years that may or may not take similar approaches.

I have a love and concern for the developers working in those environments. There are times when I have difficulty keeping up with things (and keeping up with things is a big part of my job). If I have difficulty with this, what chance does the average developer have? Something has got to give.

Anyway, I have some more posts coming up about some other things that I've come across. Hopefully those will be helpful. (No ranting, I promise.)

Happy Coding!