ASP.NET Core Pitfalls – Null Models in Post Requests

Sometimes, when AJAX posting to a controller, you may get a null model. Why?

Let’s exclude the obvious – you didn’t send a null payload. The most typical reason is, the serializer could not deserialize the payload into the target type, and it just silently sets it to null, but no exception is thrown. Take this example:

public class Data

{

    public string X { get; set; }

    public int Y { get; set; }

}

If you try to send something totally unrelated, such as this:

<script>

     function process() {
         $.ajax({
             url: '/Home/Process',
             headers: {
                 'Content-Type': 'application/json',
             },
             data: { 'foo': 'bar' },
             type: 'POST',
             success: function (data) {

             },
             complete: function () {
             },
             error: function (error) {
             }
         });
     }

</script>

It should come as no surprise that the content you send is unrelated to the model you are expecting in your action method, therefore there is a mismatch, but you may be surprised to get a null value instead of an exception.

The code responsible for binding the request to the .NET class is the input formatter that can be specified in the AddMvc extension method:

services.AddMvc(options =>

{
    options.InputFormatters.Clear();
    options.InputFormatters.Add(new MyInputFormatter());
}):

In ASP.NET Core 5, the only included one is SystemTextJsonInputFormatter, which uses the System.Text.Json API for parsing the request as JSON and deserializing it into a .NET class. You can add as much input formatters as you want, the first one that can process the request is used and all the others are discarded, so the order matters.

ASP.NET Core Pitfalls – Async File Uploads

When performing file upload(s) to an asynchronous action, it may happen that the uploaded file(s) may not be accessible, resulting in an ObjectDisposedException. This is because the uploaded files are saved to the filesystem temporarily by ASP.NET Core and then removed, after the request has been processed, which is generally what we want. If we issue an asynchronous task with an uploaded file and don’t wait for it to finish, it may happen that it occurs outside of the request’s scope, meaning, the uploaded file is already gone.

One way to address this is to have code like this:

[HttpPost]
public async Task<IActionResult> Upload(IFormFileCollection files)
{
    var f = files.ToArray();
    Array.ForEach(f, async (file) =>
    {
        using var stream = file.OpenReadStream();
        using var newStream = new MemoryStream();
        await stream.CopyToAsync(newStream);
        await ProcessFile(newStream);
    });
    return RedirectLocal("UploadSuccess");
}

For precaution, we make a copy of the original stream and use this copy instead in the upload processing code. I have successfully done this without the copy – which, of course, results in more memory usage, but occasionally I got errors as well. This way, I never had any problems.

ASP.NET Core Pitfalls – Dependency Injection Lifetime Validation

As you can imagine, it doesn’t make much sense to have a service that is registered as singleton to depend on another service that is registered as scoped, because the singleton instantiation will only happen once. Take this example:

public interface IScopedService { }

public interface ISingletonService { }

public class SingletonService : ISingletonService

{

public SingletonService(IScopedService svc) { }

}

And this registration:

services.AddSingleton<ISingletonService, SingletonService>();

services.AddScoped<IScopedService, ScopedService>();

The actual implementation does not matter here. What is important is that you will only see this problem when you try to instantiate the ISingletonService, normally through constructor injection.

One way to prevent this problem is, of course, to take care with the services registration. Another one is to have ASP.NET Core validate the registrations for us, when building the service provider. This can be achieved on bootstrap, before the Startup class is instantiated, when the application starts:

public static IHostBuilder CreateHostBuilder(string[] args) =>

Host.CreateDefaultBuilder(args)

.UseDefaultServiceProvider((context, options) =>

{

options.ValidateScopes = context.HostingEnvironment.IsDevelopment();

options.ValidateOnBuild = true;

})

.ConfigureWebHostDefaults(webBuilder =>

{

webBuilder.UseStartup<Startup>();

});

Notice the ValidateScopes and ValidateOnBuild properties. The first will prevent a singleton service from taking a scoped service dependency, and the second actually does the validation upon startup. To be clear, one can be used without the other.

When ValidateScopes is enabled, you won’t be able to inject a singleton service that takes as its dependency a scoped one, the application will crash; with ValidateOnBuild you actually specify when it will crash: if set to true, it will crash at startup, otherwise, it will only crash when the injection occurs.

If you really need to instantiate a scoped service from a singleton, you need to use code such as this:

IServiceScopeFactory scopedFactory = … //obtained from dependency injection

using (var scope = scopedFactory.CreateScope())

{

var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();

//if IScopedService implements IDisposable, it will be disposed at the end of the scope

}

Note that this will not validate open generic types, as stated in the documentation for ValidateOnBuild.

Finally, using the ApplicationServices collection of IApplicationBuilder, in the Configure method, you can only retrieve transient or singleton services, not scoped: the reason for this is that when Configure executes there is not yet a scope. This will fail:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

var scoped = app.ApplicationServices.GetRequiredService<IScopedService>();

//rest goes here

}

Interestingly, you can inject a scoped service as a parameter to Configure, e.g., this will not crash:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IScopedService svs)

{

//rest goes here

}

ASP.NET Core Pitfalls – Returning a Custom Service Provider from ConfigureServices

In pre-3.1 versions of ASP.NET Core, you could return your own service provider (AutoFac, Ninject, etc) by returning some IServiceProvider-implementing class from the ConfigureServices method. This is no longer supporting, and having code like this results in an NotSupportedException being thrown at startup:

public IServiceProvider ConfigureServices(IServiceCollection services)

{

//something

return myCustomServiceProvider;

}

The way to do it now is by registering a service provider factory at bootstrap, something like this:

public static IHostBuilder CreateHostBuilder(string[] args) =>

Host.CreateDefaultBuilder(args)      .UseServiceProviderFactory(new CustomServiceProviderFactory())         .ConfigureWebHostDefaults(webBuilder =>         {          webBuilder.UseStartup<Startup>();         });

A custom service provider factory must implement IServiceProviderFactory<T>, for an example, you can see Autfac’s implementation: https://github.com/autofac/Autofac.Extensions.DependencyInjection/blob/develop/src/Autofac.Extensions.DependencyInjection/AutofacServiceProviderFactory.cs and https://github.com/autofac/Autofac.Extensions.DependencyInjection/blob/develop/src/Autofac.Extensions.DependencyInjection/AutofacChildLifetimeScopeServiceProviderFactory.cs.

ASP.NET Core Pitfalls – Localization with Shared Resources

When localizing a web application using resources, there are two common ways:

  • Using a resource per Razor view or page
  • Using shared resources, e.g., not bound to a specific view or page

By default, ASP.NET Core’s localization expects resource files(.resx) to be located inside a Resources folder on the root of the web application. When using shared resources, we often create a dummy class (say, SharedResources) to which we can associate resources that are not associated with a specific view or page, for example, resources used in the page layout, so, we may be tempted to place this class inside this Resources folder. It turns out, however, that that won’t work: even if you change the namespace of this class to be the root assembly namespace (usually the web application’s name), the satellite resource assembly will not be generated properly. The solution is to place the dummy class at the root of the application (for example) and leave the resource files inside the Resources fimageThe code goes like this, in ConfigureServices:

services.AddMvc()
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);

And in Configure:

var supportedCultures = new[] { “en”, “pt” };

var localizationOptions = new RequestLocalizationOptions() { ApplyCurrentCultureToResponseHeaders = true } .SetDefaultCulture(supportedCultures[0]) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); app.UseRequestLocalization(localizationOptions);

Finally, to use in a Razor view or page:

@inject IHtmlLocalizer<SharedResources> Localizer

<h1>@Localizer[“Hello”]</h1>

Worth saying that this happens in ASP.NET Core 3.1 as well as in ASP.NET Core 5.

 

ASP.NET Core Pitfalls – Areas

There are a few problems with using areas:

  1. The _ViewImport.cshtml and _ViewStart.cshtml files are not loaded by views inside an area, which means that, for example tag helpers registrations are lost and page layouts are not set. The solution is either to copy these files to a view folder inside the area, such as:
    Areas\Admin\Views\Home\_ViewImport.cshtml

    or to copy the files to the root of the application.

  2. You should register the area routes before the other routes:
    endpoints.MapControllerRoute("areas", "{area:exists}/{controller=Home}/{action=Index}/{id?}");
    

ASP.NET Core Pitfalls – Session Storage

Previous versions of ASP.NET featured several ways to persist sessions:

  • InProc: sessions would be stored on the server’s process memory
  • SQL Server: sessions would be serialized and stored in a SQL Server database; other vendors offered similar functionality
  • State Server: sessions would be serialized and stored on an instance of the ASP.NET State Service
  • Custom: we had to implement our own persistence mechanism

InProc was probably the most commonly used; it was the fastest, as the items in the session weren’t serialized, but on the other side they would not survive server crashes. Using this approach things usually worked well, because the session object merely provided a reference to the items stored in memory, so manipulating these items didn’t mandate that the session be explicitly saved.

In ASP.NET Core, all of this is gone. By default, sessions are still stored in memory but one can also use one of the available distributed cache mechanisms. The main difference, however, is that even when the session objects are stored in memory, they still need to be serialized and deserialized prior to persisting or retrieving them. It is no longer possible to just store a pointer to a memory object and keep manipulating it transparently; the object to be stored needs to be converted into a byte array first. You can use any serializer you want.

If we think about it seriously, it was probably a good decision: I’ve seen applications where a lot of data was being stored on the session using InProc mode, but then there was a need to switch to another mode to improve scalability, and the application would just stop working, as the objects being stored weren’t serializable. This time, we need to carefully think about it beforehand.

ASP.NET Core Pitfalls – Redirect to Action Keeps Route Parameters

When you redirect after a POST – following the famous Post-Redirect-Get pattern – but your previous view was constructed using a route parameter, then it will be sent to the redirect action as well.

For example, say you are responding to a request for /Filter/Smartphone, where Smartphone is a route parameter, you POST it to some controller action and at the end you redirect to the Index action using the RedirectToAction method:

return this.RedirectToAction(nameof(Index));

The browser will issue the GET request for Index but keeps the Smartphone route parameter, which is not good.

The solution is to pass a routeValues parameter to RedirectToAction that doesn’t contain any of the possible route parameters. One way to do it would be to create a dictionay with all action parameters nullified:

return this.RedirectToAction(nameof(Index), MethodBase.GetCurrentMethod().GetParameters().ToDictionary(x => x.Name, x => (object) null));

The solution to have this done automatically lies in the MethodBase.GetCurrentMethod() method. This way, you are sure to avoid any unwanted route parameters on your next request.

In case you are wondering, passing null, a dictionary without entries or object won’t work, the only other way is to pass an anonymous value with the parameters explicitly set to null.

Entity Framework Core Pitfalls: No TransactionScope Support

Entity Framework Core, as of version 2.0, does not support enlisting in ambient transactions, like those provided by TransactionScope. It is being tracked by issue #9561. This is by design and is related to a limitation of System.Data.SqlClient, which will be fixed when .NET Core 2.1 comes out. The ticket for SqlClient is #3114.

I already talked about the hard relation between Entity Framework and TransactionScope before. This time, however, with EF Core 2.1, it seems that it will be fixed, and this is a good thing. For now, however, you will have to roll out your own transaction management strategy which includes starting your own transactions explicitly and either committing them or rolling them back at the end. Please refer to the Microsoft documentation available at https://docs.microsoft.com/en-us/ef/core/saving/transactions.

Entity Framework Pitfalls: Skip/Take Position Matters

In LINQ, you normally do paging through the Skip and Take operators. These are the LINQ counterparts to whatever the relational database uses for pagination – ROW_NUMBER(), LIMIT…OFFSET, TOP, ROWNUM, etc.

There are some gotchas to it:

  • When you do paging, Entity Framework demands that you add ordering to your query. This makes sense as without an explicit ORDER BY clause, search results may come in an unexpected order;
  • Skip and Take yield different results if added to different places in the LINQ expression.

Consider these two LINQ queries:

ctx
.MyEntities
.Where(x => x.Name == "")
.OrderBy(x => x.Id)
.Skip(10)
.Take(5)
.ToList();

And:

ctx
.MyEntities
.OrderBy(x => x.Id)
.Skip(10)
.Take(5)
.Where(x => x.Name == "")
.ToList();

The first one will produce this SQL:

SELECT TOP (5)
[Filter1].[Id] AS [Id],
[Filter1].[Name] AS [Name],
[Filter1].[Date] AS [Date]
FROM ( SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].
[Date] AS [Date], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number
]
FROM [dbo].[MyEntities] AS [Extent1]
WHERE N'' = [Extent1].[Name]
) AS [Filter1]
WHERE [Filter1].[row_number] > 10
ORDER BY [Filter1].[Id] ASC

Whereas the second will produce this one:

SELECT
[Limit1].[Id] AS [Id],
[Limit1].[Name] AS [Name],
[Limit1].[Date] AS [Date]
FROM ( SELECT TOP (5) [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Date] AS [Date]
FROM ( SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Date] AS [Date], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
FROM [dbo].[MyEntities] AS [Extent1]
) AS [Extent1]
WHERE [Extent1].[row_number] > 10
ORDER BY [Extent1].[Id] ASC
) AS [Limit1]
WHERE N'' = [Limit1].[Name]
ORDER BY [Limit1].[Id] ASC

Basically, the TOP(5) expression is placed in the wrong place. So, in LINQ, it’s not just the presence of a Skip and Take operator, it’s where you place it that matters!