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.

Inline Images with ASP.NET Core

The most common way to show an image in an HTML page is to use the <img> tag to load an external resource. Another option is to use a URL that is a Base64 encoded version of the image. There are some aspects worth considering:

  1. Using this approach the HTML will become larger, because it will also contain the image as a Base64 encoded string
  2. There is no need to load external resources together with the HTML page, which may make the loading faster
  3. The images can be cached together with the page
  4. Of course, if the Base64 string is hardcoded in the page, to change the image, we must change the page

I am a big fan of ASP.NET Core’s tag helpers, and I already wrote about them. This time, I propose a tag helper for rendering a Base64 image from a local file!

Here is the code

[HtmlTargetElement("inline-img")]
public class InlineImage : TagHelper
{
private readonly IWebHostEnvironment _environment
    public InlineImage(IWebHostEnvironment environment)
    {
        this._environment = environment;
}

[HtmlAttributeName("id")]
public string Id { get; set; }

[HtmlAttributeName("src")]
    public string Src { get; set; }

    [HtmlAttributeName("style")]
    public string Style { get; set; } 

   [HtmlAttributeName("class")]
    public string Class { get; set; }

    [HtmlAttributeName("width")]
    public string Width { get; set; } 

   [HtmlAttributeName("height")] 
   public string Height { get; set; } 

    [HtmlAttributeName("title")]
    public string Title { get; set; }

    [HtmlAttributeName("alt")] 
   public string Alt { get; set; } 

    public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 
   {
     if (!string.IsNullOrWhiteSpace(this.Src)) 
        {
          string absoluteSrc;

            if (this.Src.StartsWith("~/"))
            {
                absoluteSrc = this.Src.Replace("~/", this._environment.WebRootPath);
            }
            else 
            {
                var src = this.Src.StartsWith("/") ? this.Src.Substring(1) : this.Src;
                absoluteSrc = Path.Combine(this._environment.WebRootPath, src);
            }

            var img = File.ReadAllBytes(absoluteSrc); 
            var ext = Path.GetExtension(absoluteSrc).Substring(1);
            var urlEncoded = $"data:image/{ext};base64, {Convert.ToBase64String(img)}";

            output.TagName = "img";      
      output.TagMode = TagMode.SelfClosing;     
        output.Attributes.Add("src", urlEncoded);

if (!string.IsNullOrWhiteSpace(this.Id)) 
            {            
     output.Attributes.Add("id", this.Id);
            }

            if (!string.IsNullOrWhiteSpace(this.Style)) 
            { 
                output.Attributes.Add("style", this.Style); 
            }   

          if (!string.IsNullOrWhiteSpace(this.Class)) 
            {  
               output.Attributes.Add("class", this.Class); 
            } 

            if (!string.IsNullOrWhiteSpace(this.Width)) 
            { 
                output.Attributes.Add("width", this.Width); 
            }

if (!string.IsNullOrWhiteSpace(this.Height)) 
            { 
                output.Attributes.Add("Height", this.Height); 
            } 

            if (!string.IsNullOrWhiteSpace(this.Title)) 
            { 
                output.Attributes.Add("title", this.Title); 
            } 

            if (!string.IsNullOrWhiteSpace(this.Alt)) 
            { 
                output.Attributes.Add("alt", this.Alt); 
            } 
        } 

        return base.ProcessAsync(context, output); 
    }
}

First, this code has absolutely no error checking, you can add it as an exercise!

This class inherits from the TagHelper base class. It is declared as being triggered by an <inline-img> tag, and it requires a property Source to be passed and then checks to see if its relative to the virtual path (starts with ~/), in which case, it replaces it by the actual path. Otherwise, it considers it relative to the root folder.

There are a few properties that sometimes come in handy when dealing with images: Id, Style, Class, Width, Height, Alt and Title, all optional. Feel free to add others.

To use this, you need to register this tag helper, possibly on the _ViewImports.cshtml file:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, MyWebApplication

Just replace MyWebApplication by the full name of your assembly. After this, it’s just a matter of declaring an <inline-img> element on a Razor view:

<inline-img src="my.logo.png" />

And, presto, you will end up with something like this:

<img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEU…………." />

Hope you enjoy this trick! Winking smile

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?}");
    

Modern Web Development with ASP.NET Core 3 Discount Code

Modern Web Development with ASP.NET Core 3 - Second Edition

If you are interested in my book, Modern Web Development with ASP.NET Core 3, from Packt Publishing, you can use this code for a 25% discount: 25MODERNWEB.

Get it from Amazon: https://www.amazon.com/gp/mpc/A37G5RAWO3DSQX or Packt: https://www.packtpub.com/product/modern-web-development-with-asp-net-core-3-second-edition/9781789619768.

SharedFlat and Databases

Introduction

This post is part of a series on SharedFlat. See here the first (introduction) and here the second (UI). This time I will be explaining how SharedFlat handles multitenant databases.

There are essentially three strategies for doing multitenancy when it comes to databases:

  1. One connection string per tenant, which effectively means a different database for each tenant
  2. One schema per tenant, meaning, one table will exist on a schema for each tenant
  3. A single database where multitenant tables have a column that is used to distinguish tenants

Let’s see how we can have this with SharedFlat! First, it’s needed to say that this uses Entity Framework Core 3.1 and that there is a TenantDbContext abstract class that we should inherit from.

Database per Tenant

Let’s start with database per tenant. In this case, it is expected that we store in the configuration (typically the appsettings.json file) one connection string for each for each tenant. For example:

{ “ConnectionStrings”: {

    “abc”: “<connection string for abc>”,

    “xpto”: “<connection string for xpto>”

}

}

We just need, when registering the services, to use the DifferentConnectionPerTenant extension method:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.DifferentConnectionPerTenant();

And SharedFlat will know what to do. If, for any reason, you wish to use a differently-named connection string, there is an overload that lets you customize this:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.DifferentConnectionPerTenant(options =>

{

options.Mapping[“xpto”] = “xpto2”;

});

Schema per Tenant

Next is different schema per tenant. The extension method to call is DifferentSchemaPerTenant:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.DifferentSchemaPerTenant();

By default, the schema will be identical to the tenant; should we need to change that, there is an overload:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.DifferentSchemaPerTenant(options =>

{

options.Mapping[“xpto”] = “xpto2”;

});

This will set the schema on each entity that implements the ITenantEntity interface (a simple marker interface) when the TenantDbContext -derived class is initialized. To be clear, using SQL Server as an example, it will change the default (“dbo”) for the tenant value, so a table named “dbo.Product” will become “abc.Product”, “xpto.Product”, etc.

Filter by Tenant

The last technique depends on having a column on each table that we want to make multitenant, as dictated by its entity implementing ITenantEntity. The method to call is FilterByTenant, unsurprisingly:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.FilterByTenant();

As is usual, the method has some defaults, like, the name of the column (“Tenant”), but we can change it:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.FilterByTenant(“TenanantCol”);

Or, in the eventual case where you need to change the mapping:

services

.AddTenantService()

.AddTenantDbContextIdentification()

.FilterByTenant(options =>

{

options.Mapping[“xpto”] = “xpto2”;

});

Filtering by tenant means that whenever the context is querying for a multitenant-aware entity, a filter is added:

SELECT p.[ProductId], p.[Name], p.[Price]

FROM [dbo].[Product] p

WHERE p.[Tenant] = ‘abc’

Conclusion

And this is it for multitenant databases. Keep tuned for more on SharedFlat, and, as always, let me hear your thoughts! Winking smile

SharedFlat and Multitenant UI

Introduction

In my previous post, I introduced SharedFlat, a library for making multitenant ASP.NET Core apps easier to build. This time I’m going to talk about how can we customize the UI per tenant.

Using Conditional Logic in Views

One way to make UI modifications is to have conditional logic in views. There is an extension method, IsEnabledForTenant, that can be used to check if a specific property is set to true for the current tenant:

@if (this.IsEnabledForTenant(“Foo”))

{

<p>Bar</p>

}

This will look for an option registered using the Options Pattern that was described in the previous post.

Another option is to check for a specific tenant, using the IsTenant extension method:

@if (this.IsTenant(“abc”))

{

<p>abc</p>

}

For the exact same purpose we can also use the <tenant> tag helper:

<tenant name=”xpto”>

<p>xpto!</p>

</tenant>

If we wish to load a partial view, there’s another tag helper just for that, <tenant-partial>:

<tenant-partial tenant=”xyz” name=”xyzView”></tenant-partial>

This will conditionally load the xyzView file if the current tenant is xyz.

Using Different Files

A different approach would be to have different files (in different folders), one for each tenant. Say, for example, that you want to have a different folder per each tenant in your Views\<controller> folder. You just need to call AddTenantLocations when registering the services:

services

.AddTenantService()

.AddTenantLocations();

After this, if you have this folder structure:

  • Views
    • Home
      • abc
      • xyz

ASP.NET Core will load the views from the tenant-specific folder, (abc or xyz) and only if it cannot find the files there ASP.NET Core will it resort to the default ones (in Home or Shared).

Finally, if you wish to load static files from a well-known location that refers to the tenant, you can do this:

<script src=”~/@this.GetTenant()/index.js”></script>

Conclusion

This is as much as it goes for now for making the UI multitenant with SharedFlat. On the next post I will be talking about data, how to access databases in a multitenant way.

Introducing SharedFlat

Introduction

A multitenant web application is one that responds differently depending on how it is addressed (the tenant). This kind of architecture has become very popular, because a single code base and deployment can serve many different tenants.

There are a few libraries out there that help ASP.NET Core developers implement such architectures, but, since I wrote a book on ASP.NET Multitenant Applications a few years ago, I decided to write something from scratch for ASP.NET Core – and here is SharedFlat!

For the sake of the examples, let’s assume two tenants that correspond to two different domain names, abc.com (abc) and xyz.net (xyz).

Concepts

SharedFlat is available at GitHub and NuGet and you are free to have a look, fork and suggest improvements. It is still in early stages, mind you!

It was designed around the following key tenets:

What is a tenant?

A tenant is a unique entity, identified by a name, and the application must respond appropriately to in, in terms of:

  • User interface: each tenant may have one or more different UI aspects
  • Data: each tenant should have access to different data
  • Behavior: the application should behave (maybe slightly) differently, for each tenant

The current tenant is always returned by the implementation of the ITenantService interface:

public interface ITenantService

{

string GetCurrentTenant();

}

Simple, don’t you think? The returned tenant information is the tenant’s name (or code, if you prefer).

How do we identify a tenant?

Now, the first thing to know is, how do we identify the current tenant? Well, there are different strategies for this, but they all implement this interface:

public interface ITenantIdentificationService

{

string GetCurrentTenant(HttpContext context);

}

The strategies itself can be various:

  • Using the request’s HTTP Host header: useful for scenarios where we have multiple domains pointing to the same IP address and we want to distinguish the tenant by the requested host
  • Using a query string field: more for debugging purposes
  • Using the source IP of the request: for when we want to force IPs to be of a specific tenant
  • Static: for testing purposes only
  • Dynamic: when we decide in code, at runtime, what the tenant is
  • Some header’s value

TenantIdentificationServices

The ITenantService that we saw first will have to somehow delegate to one of these implementations for getting the current tenant. We need to register one implementation by using one of the following extension methods:

services.AddTenantIdentification()

.DynamicTenant(ctx => “abc.com”) //dynamic

.TenantForQueryString() //query string

.TenantForSourceIP() //source IP

.TenantForHost() //host header

.TenantForHeader() //some header

.StaticTenant(“abc.com”); //static

Of course, only one of these methods (DynamicTenant, TenantForQueryString, TenantForSourceIP, TenantForHost, TenantForHeader, StaticTenant) should be called.

Some of these different strategies need configuration. For example, to map a host name to a tenant name, we use code like this:

services.AddTenantIdentification()

.TenantForSourceIP(options =>

{

options.Mapping.Default = “abc”;

options.Mapping

.Add(“192.168.1.0”, “xyz”)

.Add(“10.0.0.0”, “abc”);

});

Some tenant identification services also expose an interface ITenantsEnumerationService, which allows listing all the known tenants (their name, or code). This service can be retrieved from the DI, but only if the registered identification service supports it:

public IActionResult Index([FromServices] ITenantsEnumerationService svc)

{

var tenants = svc.GetAllTenants();

return View(tenants);

}

Configuration per tenant

It is also very important to get different configuration depending on the current tenant. Almost all of the tenant identification strategies (except StaticTenantIdentificationService and DynamicTenantIdentificationService) support a mapping between “something” and a tenant code. This “something” may be the source IP (SourceIPTenantIdentificationService), the request’s Host header (HostTenantIdentificationService), etc. There is a standard configuration section that you can add to your appsettings.json file:

{

“Tenants”: {

“Default”: “”,

“Tenants”: {

“xyz.net”: “xyz”,

“abc.com”: “abc”

}

}

}

You can load the configuration using an extension method:

services.AddTenantIdentification()

.TenantForHost(options =>

{

Configuration.BindTenantsMapping(options.Mapping);

});

Or you can just set the values manually, as we’ve seen before.

If you want to set some per-tenant configuration, you should use the Options Pattern with a named configuration (one per tenant):

services.Configure<SomeClass>(“abc”, options =>

{

options.Foo = “bar”;

});

And then you need to retrieve it like this:

public HomeController(IOptionsSnapshot<SomeClass> optionsSnapshot, ITenantService tenantService)

{

var tenant = tenantService.GetCurrentTenant();

var options = optionsSnapshot.Get(tenant);

//…

}

There is yet another option: you can have a class that implements ITenantConfiguration, which is defined as:

public interface ITenantConfiguration

{

string Tenant { get; }

void ConfigureServices(IConfiguration configuration, IServiceCollection services);

}

You can have one class per tenant and in there you can register services that are specific for it. In order for it to work, you need to call the AddTenantConfiguration extension method:

services.AddTenantIdentification()

.TenantForHost(options =>

{

Configuration.BindTenantsMapping(options.Mapping);

})

.AddTenantConfiguration();

Conclusion

This is all I have for now, stay tuned for more on SharedFlat!