Saturday, October 17, 2026

ASP.NET Core: Persisting Serilog Logs from a Docker Container to the Host


Serilog is a popular logging library for ASP.NET Core applications. It integrates easily with the built-in logging infrastructure and offers flexible configuration options.

When running an application in Docker—and later, potentially, in Kubernetes—logging to standard output (stdout) is generally the preferred approach. It allows you to access application logs using standard Docker commands, such as:

docker logs <container_name>

Fortunately, Serilog's Console sink makes this straightforward.

However, things become more complicated when an application also uses the File sink and we need to persist log files on the host machine. Without persistent storage, files written inside a container can be lost when the container is removed and recreated.

The problem is particularly relevant when following security best practices and running application processes as a non-root user.

In this article, we'll configure Serilog in an ASP.NET Core MVC application, run it in Docker under a dedicated non-root user, and investigate how to persist log files on the host. We'll also look at two approaches to managing directory permissions, including an initialization container.

1. Configuring Serilog in ASP.NET Core

First, install the Serilog.AspNetCore NuGet package:

Install-Package Serilog.AspNetCore

Next, configure Serilog in Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Configure Serilog
builder.Logging.ClearProviders();

builder.Host.UseSerilog((hostingContext, loggerConfiguration) =>
{
    loggerConfiguration
        .ReadFrom.Configuration(hostingContext.Configuration)
        .Enrich.FromLogContext();
});

This configuration removes the default logging providers and registers Serilog as the application's logging provider. Logger settings are loaded from the application configuration, and FromLogContext() enables contextual properties to be included in log events.

Now, let's add the Serilog configuration to appsettings.json:

{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Information",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
          "outputTemplate": "{Timestamp:HH:mm:ss}|{RequestId}|{SourceContext}|{Level:u3}|{Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "App_Data/logs/log.txt",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 31,
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.ffff}|{RequestId}|{SourceContext}|{Level:u3}|{Message:lj}{NewLine}{Exception}",
          "restrictedToMinimumLevel": "Information"
        }
      }
    ]
  }
}

This configuration defines two logging destinations:

  • Console: Writes log events to the console, making them available through docker logs.

  • File: Writes log events to files in App_Data/logs.

The default minimum logging level is Debug. Messages from the Microsoft namespace are logged at Information and above, while messages from System are logged at Warning and above.

The file sink has its own minimum level of Information, so debug-level events are not written to the files.

The rollingInterval: "Day" setting creates a new log file every day. With the specified path, the resulting files follow a date-based naming convention, such as log20261001.txt.

The retainedFileCountLimit: 31 setting limits the number of retained log files to 31. Older files are removed as the retention limit is exceeded.

Finally, we can use the standard ILogger<T> interface in our application code:

using Microsoft.AspNetCore.Mvc;

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

    public HomeController(ILogger<HomeController> logger)
    {
        this.logger = logger;
    }

    public IActionResult Index()
    {
        this.logger.LogInformation("Visit home page");
        return View();
    }
}

That's all it takes to get Serilog working with both console and file logging.

2. Running the Application as a Non-Root User

Next, let's package the application into a Docker image.

For security reasons, application processes should generally run with the minimum privileges they need. Instead of running our ASP.NET Core application as root, we'll create a dedicated user and group in the Dockerfile.

Here's a multi-stage Dockerfile targeting .NET 10:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build

COPY . /src

RUN dotnet restore "/src/MyApp.csproj"
RUN dotnet publish "/src/MyApp.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final

RUN apt-get update && apt-get install -y adduser

WORKDIR /app

ENV ASPNETCORE_URLS=http://+:8080
ENV COMPlus_EnableDiagnostics=0

COPY --from=build /app/publish .

RUN addgroup --group my-group --gid 2001 \
    && adduser --uid 2000 --gid 2001 my-user \
    && chown -R 2000:2001 /app

# Run the application as a non-root user
USER 2000:2001

ENTRYPOINT ["dotnet", "MyApp.dll"]

The Dockerfile performs the following steps:

  1. Builds and publishes the application in a dedicated build stage.

  2. Copies the published application into /app in the runtime image.

  3. Creates a group with GID 2001 and a user with UID 2000.

  4. Assigns ownership of /app to the new user and group.

  5. Starts the application as UID 2000 and GID 2001.

The COMPlus_EnableDiagnostics=0 environment variable disables .NET diagnostics functionality. Whether this setting is appropriate depends on your operational requirements, as it can limit certain debugging and diagnostic capabilities.

Let's start the container and verify that the application is running under the expected user.

Open a shell inside the container:

docker exec -it <container_name> sh

Check the current user:

id

Expected output:

uid=2000(my-user) gid=2001(my-group) groups=2001(my-group)

We can also inspect the running processes:

ps aux

Example output:

USER       PID %CPU %MEM    VSZ   RSS TTY   STAT START   TIME COMMAND
my-user      1  0.0  3.4 274066700 70844 ?   Ssl  Sep21   5:17 dotnet MyApp.dll
...

The exact output will vary, but the important point is that the application process runs under the dedicated non-root user.

3. Verifying That Log Files Are Created

Let's check whether Serilog has successfully created a log file inside the container:

ls /app/App_Data/logs

Example output:

log20261001.txt

Now inspect the file:

cat /app/App_Data/logs/log20261001.txt

For example:

2026-10-01 14:40:03.0288||Microsoft.AspNetCore.Server.Kestrel|WRN|As of "2026-10-01T14:40:02.9559252+00:00", the heartbeat has been running for "00:00:01.38..." which is longer than "00:00:01". This could be caused by thread pool starvation.

The exact log content depends on the application and runtime environment.

So far, everything works as expected. Serilog writes log events to files inside the container.

We can also check the directory's ownership:

ls -ld /app/App_Data/logs

Example output:

drwxr-xr-x 2 my-user my-group 4096 Oct 1 14:40 /app/App_Data/logs

The directory belongs to my-user:my-group, corresponding to UID 2000 and GID 2001.

Now let's make the logs persistent.

4. Mounting a Host Directory into the Container

At the moment, log files are stored in the container's writable filesystem. If the container is removed and recreated, those files will not necessarily survive.

The simplest way to persist them is to mount a directory on the host into the container.

For this example, we'll use /var/log/MyApp on the host.

Add a bind mount to docker-compose.yml:

services:
  web:
    image: MyImage
    build:
      context: ../
      dockerfile: ./MyApp/Dockerfile
    volumes:
      - /var/log/MyApp:/app/App_Data/logs

This maps the host directory /var/log/MyApp to /app/App_Data/logs inside the container.

However, after restarting the container, we discover that the log directory is empty.

Let's investigate.

Open a shell inside the container and inspect the directory:

docker exec -it <container_name> sh
ls /app/App_Data/logs

There are no log files.

Now check the directory's ownership:

ls -ld /app/App_Data/logs

Example output:

drwxr-xr-x 2 root root 4096 Oct 2 07:29 /app/App_Data/logs

We've found the problem.

The mounted directory is owned by root:root, while the application runs as UID 2000 and GID 2001.

When Docker mounts an existing host directory, the container sees the directory's underlying ownership and permissions. The ownership assigned to /app in the Dockerfile does not automatically override the permissions of the mounted directory.

As a result, the application may be unable to create or write log files in the mounted directory.

5. Solution 1: Change Directory Ownership on the Host

One straightforward solution is to assign ownership of the host directory to the UID and GID used by the application.

On the host, run:

sudo mkdir -p /var/log/MyApp
sudo chown -R 2000:2001 /var/log/MyApp

Then restart the container.

The application should now be able to write log files to /var/log/MyApp on the host, and those files will persist independently of the container's lifecycle.

However, this approach requires caution in production environments.

Before assigning ownership, check whether UID 2000 and GID 2001 are already used by other accounts or services on the host. Numeric user and group IDs are what matter for filesystem permission checks; matching names inside the container do not guarantee that the corresponding host account is the same entity.

Changing ownership recursively can also affect existing files and directories.

This solution is useful for demonstrating the underlying issue, but it may not be the best operational approach for every deployment.

Can we automate the permission setup instead?

6. Solution 2: Use an Initialization Container

Another approach is to run a short-lived initialization container that adjusts the log directory's ownership before the application starts.

This avoids requiring an administrator to run chown manually every time the environment is prepared.

Here's the updated docker-compose.yml:

services:
  init-logs:
    image: alpine:latest
    user: "0:0"
    volumes:
      - /var/log/MyApp:/logs
    command: ["sh", "-c", "chown -R 2000:2001 /logs"]
    restart: "no"

  web:
    image: MyImage
    build:
      context: ../
      dockerfile: ./MyApp/Dockerfile
    depends_on:
      init-logs:
        condition: service_completed_successfully
    volumes:
      - /var/log/MyApp:/app/App_Data/logs

Here's how this configuration works.

The init-logs service

The initialization container uses Alpine Linux and runs as root inside the container:

user: "0:0"

It mounts the host directory at /logs and executes:

chown -R 2000:2001 /logs

The command assigns ownership of the log directory and its contents to the UID and GID expected by the application.

The container then exits. The restart: "no" policy prevents Docker from automatically restarting it after completion.

The web service

The main application mounts the same host directory at /app/App_Data/logs.

The depends_on condition:

depends_on:
  init-logs:
    condition: service_completed_successfully

instructs Docker Compose to wait until init-logs has completed successfully before starting web.

This ensures that the permission initialization runs before the application starts.

Notice that the two services use different images. This is intentional: the initialization task needs only a lightweight Linux environment with the required filesystem utilities, while the application runs in its own .NET runtime image.

If the initialization container and the application need to share the same application image for some other reason, that is also possible, but it is not required for this solution.

Important considerations

Although the initialization container automates permission management, it does not eliminate every operational concern.

  • UID and GID conflicts: Verify that the chosen IDs are appropriate for your host environment.

  • Recursive ownership changes: chown -R changes ownership of existing files as well as the directory itself. Avoid using it blindly on directories containing unrelated data.

  • Startup behavior: Docker Compose waits for the initialization service to complete successfully during startup. If the initialization fails, the dependent service will not start.

  • Image reproducibility: For production deployments, consider pinning the Alpine image to a specific version or digest rather than relying on alpine:latest.

For more complex deployments, you may want to manage host directory ownership as part of infrastructure provisioning instead. In Kubernetes, the equivalent problem is typically handled through volume permissions and security-context configuration, depending on the storage provider and workload requirements.

Conclusion

Persisting Serilog log files from a Dockerized ASP.NET Core application is straightforward once filesystem permissions are configured correctly.

The key points are:

  1. Configure Serilog to write to both the console and files when both outputs are needed.

  2. Run the application as a non-root user to reduce its privileges.

  3. Use a Docker bind mount to persist log files on the host.

  4. Remember that mounting a host directory does not automatically grant write access to the container's user.

  5. Choose a permission-management strategy that fits your deployment environment.

For a quick demonstration, changing the directory's ownership on the host is sufficient. For an automated Docker Compose deployment, a dedicated initialization container can handle the setup before the application starts.

Neither approach should be applied blindly in production. Review host UID/GID assignments, directory ownership, and the scope of permission changes before deploying.

If you have experience with other approaches to managing bind-mount permissions for non-root containers, I'd be interested to hear how you handle this in your own environments.

Saturday, October 3, 2026

Caching Image Responses in ASP.NET Core MVC

Hi everyone! In this article, I’d like to share my experience implementing client-side image caching in an ASP.NET Core MVC application.

In the SaaS world, reducing resource consumption is an important task, especially as the number of customers served by a single server increases. Image generation and delivery on the backend can be relatively CPU- and memory-intensive operations. Adding client-side caching with the HTTP Cache-Control header can significantly reduce the load on the server.

Suppose we have an ImageController with an action that retrieves an image from a database and dynamically resizes it so that its dimensions do not exceed the specified maxWidth and maxHeight:

public class ImageController : Controller
{
    [HttpGet]
    public ActionResult ViewResized(int id, int maxWidth, int maxHeight)
    {
        ...
        return new ImageResult
        {
            FileName = fileName,
            Content = memoryStream.ToArray()
        };
    }
}

I won’t go into the image-processing implementation here, since the main topic of this article is caching rather than image manipulation.

Adding client-side caching

We want the server to include a Cache-Control header in the response. There are several ways to do this.

The first option is to use the ResponseCache attribute:

[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
public ActionResult ViewResized(int id, int maxWidth, int maxHeight)

This produces the following header:

Cache-Control: public, max-age=60

Another option is to modify the action itself:

public ActionResult ViewResized(int id, int maxWidth, int maxHeight)
{
    Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue
    {
        Public = true,
        MaxAge = 60
    };

    ...
}

However, I prefer not to mix functional and non-functional concerns. Therefore, I chose a third approach: implementing the caching logic as middleware.

This keeps the controller action clean and also allows the cache lifetime to be changed dynamically—for example, by reading it from a database or configuration file.

Implementing the middleware

The middleware can look like this:

internal class ImageCacheMiddleware : MiddlewareWithService
{
    private readonly RequestDelegate next;

    public ImageCacheMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        var settingsProvider =
            context.RequestServices.GetService<ISettingsProvider>();

        int imageCacheIntervalInSeconds =
            settingsProvider.Get("ImageCacheIntervalInSeconds");

        if (
        
        > 0)
        {
            context.Response.OnStarting(() =>
            {
                // add the header only if the controller hasn't set it already
                if (!context.Response.Headers.ContainsKey("Cache-Control"))
                {
                    context.Response.Headers.Append(
                        "Cache-Control",
                        $"public, max-age={imageCacheIntervalInSeconds}");
                }

                return Task.CompletedTask;
            });
        }

        await next.Invoke(context);
    }
}

First, we obtain an ISettingsProvider, an application-specific abstraction responsible for accessing configuration settings.

Its implementation is outside the scope of this article. Depending on the application, it could read settings from a database, configuration file, environment variables, or another source.

We then retrieve the value of ImageCacheIntervalInSeconds.

If the value is greater than zero, we register a Response.OnStarting callback that adds the following header:

Cache-Control: public, max-age={imageCacheIntervalInSeconds}

The public directive means that the response can be cached not only by the browser but also by intermediate proxies and CDNs.

If this behavior isn't appropriate for your scenario, consider using other directives such as no-cache, no-store, or private.

Registering the middleware

Next, we need an extension method that makes it easier to register the middleware during application startup:

internal static class ImageCacheMiddlewareExtension
{
    public static IApplicationBuilder UseImageCacheMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ImageCacheMiddleware>();
    }
}

We can then add it to Program.cs:

var builder = WebApplication.CreateBuilder(args);

...

var app = builder.Build();

...

app.UseWhen(
    context => context.Request.Path
        .ToString()
        .ToLower()
        .Contains("/image/view"),
    appBranch =>
    {
        appBranch.UseImageCacheMiddleware();
    });

That's it—the caching middleware is now in place.

You can verify that it works using the browser's Network tab. The first response should contain the Cache-Control header, while subsequent requests should be served from the browser cache.

Unit testing

The remaining step is to cover the middleware with unit tests.

For this example, we can use NUnit together with the popular open-source mocking library Moq.

There are two tricky parts we need to handle:

  1. Mocking the retrieval of the cache lifetime from the database.

  2. Triggering the Response.OnStarting callback.

Mocking the settings provider

The first problem comes from the following call chain:

HttpContext.RequestServices.GetService<T>()

To make the middleware easier to test, we can extract service resolution into a separate virtual method:

protected virtual T GetService<T>(HttpContext ctx)
{
    return ctx.RequestServices.GetService<T>();
}

We can then create a test-specific subclass and override this method:

internal class ImageCacheMiddlewareForTesting : ImageCacheMiddleware
{
    private readonly ISettingsProvider settingsProvider;

    public ImageCacheMiddlewareForTesting(
        RequestDelegate next,
        ISettingsProvider settingsProvider)
        : base(next)
    {
        this.settingsProvider = settingsProvider;
    }

    protected override T GetService<T>(HttpContext context)
    {
        if (typeof(T) == typeof(ISettingsProvider))
            return (T)this.settingsProvider;

        throw new NotSupportedException();
    }
}

The mock ISettingsProvider can now be passed directly to the test middleware.

Testing Response.OnStarting

For the second problem, we can use Moq to capture the callback passed to OnStarting.

The callback is stored in a local variable, and the test can then simulate the beginning of the response by invoking it directly:

[TestFixture]
public class TestImageCacheMiddleware
{
    [Test]
    public void Test_WhenCacheLifetimeSpecified_ThenItIsAddedToHeaders()
    {
        // Configure a one-second cache lifetime
        var settingsProvider = new Mock<ISettingsProvider>();
        settingsProvider
            .Setup(x => x.Get(It.IsAny<string>()))
            .Returns(1);

        var headers = new HeaderDictionary();

        var response = new Mock<HttpResponse>();
        response
            .Setup(x => x.Headers)
            .Returns(headers);

        // Capture the callback
        Func<Task> capturedCallback = null;

        response
            .Setup(r => r.OnStarting(It.IsAny<Func<Task>>()))
            .Callback<Func<Task>>(callback =>
                capturedCallback = callback);

        var ctx = new Mock<HttpContext>();
        ctx
            .Setup(x => x.Response)
            .Returns(response.Object);

        var requestDelegate = new Mock<RequestDelegate>();

        var middleware = new ImageCacheMiddlewareForTesting(
            requestDelegate.Object,
            settingsProvider.Object);

        middleware.Invoke(ctx.Object)
            .GetAwaiter()
            .GetResult();

        // Simulate the beginning of the server response
        if (capturedCallback != null)
        {
            capturedCallback()
                .GetAwaiter()
                .GetResult();
        }

        // Verify that the cache header was added
        ClassicAssert.AreEqual(1, headers.Count);
        ClassicAssert.AreEqual(
            "public, max-age=1",
            headers["Cache-Control"]);
    }

    [Test]
    public void Test_WhenCacheLifetimeNotSpecified_ThenItIsNotAddedToHeaders()
    {
        // Cache lifetime is disabled
        var settingsProvider = new Mock<ISettingsProvider>();

        settingsProvider
            .Setup(x => x.Get(It.IsAny<string>()))
            .Returns(0);

        var headers = new HeaderDictionary();

        var response = new Mock<HttpResponse>();
        response
            .Setup(x => x.Headers)
            .Returns(headers);

        // Capture the callback
        Func<Task> capturedCallback = null;

        response
            .Setup(r => r.OnStarting(It.IsAny<Func<Task>>()))
            .Callback<Func<Task>>(callback =>
                capturedCallback = callback);

        var ctx = new Mock<HttpContext>();
        ctx
            .Setup(x => x.Response)
            .Returns(response.Object);

        var requestDelegate = new Mock<RequestDelegate>();

        var middleware = new ImageCacheMiddlewareForTesting(
            requestDelegate.Object,
            settingsProvider.Object);

        middleware.Invoke(ctx.Object)
            .GetAwaiter()
            .GetResult();

        // Simulate the beginning of the server response
        if (capturedCallback != null)
        {
            capturedCallback()
                .GetAwaiter()
                .GetResult();
        }

        // Verify that no cache header was added
        ClassicAssert.AreEqual(0, headers.Count);
    }
}

Conclusion

We have added client-side caching to the image-serving endpoint, reducing the amount of work performed by the server.

By moving the caching logic into middleware, we also kept the controller action focused on its primary responsibility while retaining the ability to configure the cache lifetime dynamically.

Finally, the middleware is covered by unit tests, helping prevent regressions as the application evolves.

 

Tuesday, October 8, 2024

Use nginx running in Docker container to serve php sites running on the host

In one of my previous articles I showed how to use nginx running in Docker container to serve .Net sites running on Kestrel web server on the host. In the current article I will show how to do similar thing with php sites.

So idea is the same: we have nginx running in Docker container and number of php sites running on the host and want containerized nginx to serve these sites. For example we will use Mantis (popular bug tracker) and Wordpress (popular CMS engine based on php) sites. For simplicity we will use assumption that MySQL databases used by php sites are running on the same host.

On the host php sites are served by php fpm daemon (php FastCGI process manager) which may listen for IP:port or unix sockets. One option is to use this php fpm from the host, however e.g. in case of unix sockets it will require tricky rights setups. Also since we anyway eager to move to containers it will be preferable to create lightweight php fpm container which will serve our php sites from the host. First of all we need to create Docker image based on php fpm with MySQL extension installed (so php will be able to connect to MySQL db). It can be created with the following Dockerilfe:

FROM php:7.4-fpm
RUN docker-php-ext-install mysqli pdo_mysql

On the first line we tell Docker that our image should be based on php fpm v7.4 and on the 2nd line we install MySQL extension for php so it will be able to connect to the database. Note that by default php fpm containers listen port 9000 - it will be used when we will configure nginx.conf file for our site.

Then build the image and push it to our Docker images registry:

docker build -t {php-image-tag}
docker push {php-image-tag}

On the next step we need to setup nginx and php containers with the following docker compose file:

name: myservice
services:
  nginx:
    image: nginx:latest
volumes:
- /var/www/myphpsite:/var/www/myphpsite ports: - 80:80 restart: always networks: - mynetwork extra_hosts: - host.docker.internal:host-gateway
php:
image: {php-image-tag}
volumes:
- /var/www/myphpsite:/var/www/myphpsite
restart: unless-stopped
networks:
- mynetwork
extra_hosts:
- host.docker.internal:host-gateway

Important line here is where we add a volume which points to the folder with php scripts (/var/www/myphpsite) both to nginx and php. With this line nginx container will be able to serve also static files (css, images, js, etc) from our php site and at the same time php container will have access to php scripts. Also it is important that nginx and php containers are located in the same network (mynetwork) (in this case containers will be able to communicate using containers names) and that we added host.docker.internal extra host (so php fpm container will be able to connect to the database running on the host).

Next step is to setup nginx.conf for our php site:

server {
    listen 80;
    server_name example.com;
    root /var/www/myphpsite;
    ...
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass myservice-php-1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
    }
}

Here with root directive we specify that php sources are located in /var/www/myphpsite folder (remember that we added this folder to php and nginx containers volumes) and configure php fpm to process php scripts. Directive fastcgi_pass instructs nginx to execute a CGI script (php in our case) using the FastCGI protocol (which is different from proxy_pass directive used for .Net sites running on Kestrel - which sends an HTTP request to another web server). Since both containers are located in the same network we may use container name (myservice-php-1) and port number (9000) for fastcgi_pass directive.

And last but not least - since php scripts will be processed inside container we need to change host in database connection string from localhost to host.docker.internal (we can do that because added extra_host to our containers in docker compose files). Location where it is stored depends on php site. E.g. for Mantis it is stored in /.../{site_folder}/config/config_inc.php in $g_hostname variable, for Wordpress-based sites in /var/www/{site_folder}/wp-config.php in DB_HOST variable. After that your php sites will work under nginx running in Docker container.

Wednesday, September 18, 2024

Use nginx running in Docker container to serve .NET sites running on Kestrel both in host and Docker containers

In one of my previous articles I made brief introduction to Docker – containerization technology which gives more control over infrastructure. Suppose that we have number of .NET sites running on Kestrel in Linux and published via nginx to the internet like shown on the following schema (ports numbers are only for example here):

And we want to move all our infrastructure to Docker (backend, database, nginx itself, etc) so it will look like this:

On practice however this switch will take time and at some time period we will have both sites running still in the Linux host and sites running in Docker containers, and we will need to serve both types:

Since only one app may listen to 80 port we need to decide whether we want to keep nginx service running on the host or move nginx to Docker and configure it to serve both sites from the host and sites from Docker containers. In this article I will describe this last option.

Since nginx is running in Docker there won’t be many problems with hosting sites running also in Docker. Just run these sites and nginx container in the same network:

#docker-compose-nginx.yml
name: myservice
services:
  nginx:
    image: nginx:latest
    ports:
      - 80:80
    …
    networks:
      - mynetwork

and nginx will be able to resolve containers by names. I.e. if site’s container name is mysite-web-1 you may specify nginx.conf like this:

server {
	listen 80;
	listen [::]:80;
	server_name mysite.ru www. mysite.ru;
	location / {
		proxy_pass http://mysite-web-1:80;
		…
	}
}

However with sites running in host it is not that straightforward. Since nginx is running in Docker container which has own IP address we need to instruct it to which IP it should forward requests if they came for sites running on the host.

We can do that by adding special extra host host.docker.internal to nginx docker compose:

name: myservice
services:
  nginx:
    image: nginx:latest
    ports:
      - 80:80
    restart: always
    networks:
      - mynetwork
    extra_hosts:
      - host.docker.internal:host-gateway

If we will check /etc/hosts file inside nginx container we will see that host.docker.internal points to 172.17.0.1 IP address which is default IP used by Docker for host:

docker exec -it myservice-nginx-1 sh
more /etc/hosts
…
host.docker.internal 172.17.0.1

Next step is to instruct nginx to forward request to the host if it came for site running there. For doing that we need to modify nginx.conf and specify host.docker.internal with appropriate port in proxy_pass property:

server {
	listen 80;
	listen [::]:80;
	server_name mysite.ru www.mysite.ru;
    	location / {
		proxy_pass http://host.docker.internal:5000;
		…
    	}
}

However that is still not enough. If your .NET site is running on Kestrel you most probably configured to run it as a daemon via the following commands:

systemctl enable mysite.ru.service
systemctl start mysite.ru.service

where mysite.ru.service file contains the following line:

Environment=ASPNETCORE_URLS=http://localhost:5000

With our current configuration if we will open shell inside our nginx container and try to reach our site via curl we will get Connection refused:

docker exec -it myservice-nginx-1 sh
curl -X GET http://host.docker.internal:5000
Connection refused

The problem here is that Kestrel is currently listening only for localhost IP (127.0.0.1). We can check it on the host using the following command:

netstat -tulpn | grep 5000
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      22496/dotnet
tcp6       0      0 ::1:5000                :::*                    LISTEN      22496/dotnet

but request from nginx container goes to 172.17.0.1. To solve that we need to modify /etc/systemd/system/mysite.ru.service and add http://172.17.0.1:5000 to ASPNETCORE_URLS after semicolon:

Environment=ASPNETCORE_URLS=http://localhost:5000;http://172.17.0.1:5000

and reload the service:

systemctl stop mysite.ru.service
systemctl daemon-reload
systemctl start mysite.ru.service
systemctl status mysite.ru.service

After that check that it listen 5000 port also on 172.17.0.1:

netstat -tulpn | grep 5000
tcp        0      0 172.17.0.1:5000         0.0.0.0:*               LISTEN      22496/dotnet
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      22496/dotnet
tcp6       0      0 ::1:5000  

And now if we go inside container shell and try to reach the site connection should be successful:

docker exec -it myservice-nginx-1 sh
curl -X GET http://host.docker.internal:5000
Connection successful

which means that nginx is now able to serve sites running both in containers and on the host itself.

Tuesday, August 27, 2024

Nginx uses 1st available server configuration when domain name not resolved

If you host several sites on nginx (which means that you also have several domain names pointing to your server's IP address) then you may face with the following issue. Suppose that we have 3 domain names pointing to our server's IP with running nginx:

example1.com
example2.com
example3.com

Sites example1.com and example2.com are configured in nginx, but there is no example3.com. The problem is that by default nginx will still proxy example3.com to 1st available server config (depending in which order they are defined in nginx.conf file, let's say example1.com in our example) which may be quite confusing because we requested example3.com but see content of example1.com in browser window (in browser address bar we will still see example3.com).

In order to avoid this behavior we need to add default server configuration to nginx.conf which will return 404 like that:

server {
	listen 80 default_server;
	server_name  _;
	return 404;
}

Now if we will try to open example3.com we will see 404 page returned by nginx:

which is more clear and expected behavior from my point of view.

Wednesday, June 19, 2024

Obfuscar: how to stop build of .NET app with error when obfuscation failed

In my previous post I showed how to obfuscate ASP.Net Core web app using Obfuscar. There was however one problem: if obfuscation will fail it will be ignored by VS by default. In the current post I will show how to make such errors more visible i.e. how to cause build to fail if obfuscation failed.

First of all we need to move obfuscation logic to PowerShell file (postpublish.ps1 in this example)  located in project folder and specify it in post publish event:

  <Target Name="PostBuild" AfterTargets="Publish" Condition=" '$(Configuration)' == 'Release' ">
    <Exec Command="pwsh -ExecutionPolicy Unrestricted $(ProjectDir)postpublish.ps1 -targetFramework $(TargetFramework) -buildConfig $(Configuration) -projectDir $(ProjectDir) -publishDir $(PublishDir)" />
  </Target>

(if you are interested why pwsh command is used instead of powershell check the following article Use PowerShell in Visual Studio build events when build Docker image for .Net app)

It is up to you to implement actual obfuscation logic, in my example I will use cross platform Obfuscar dotnet global tool:

$s = dotnet tool list -g | Out-String
if (!$s.Contains("obfuscar.globaltool")) {
	Write-Host "Obfuscar global tool not installed. Installing it..."
	& dotnet tool install --global Obfuscar.GlobalTool
} else {
	Write-Host "Obfuscar global tool is already installed"
}

$targetDir = [System.IO.Path]::Combine($projectDir, $publishDir)
$obfuscarXmlPath = [System.IO.Path]::Combine($targetDir, "obfuscar.xml")
Write-Host "Obfuscating assemblies..."
[void]($output = & obfuscar.console $obfuscarXmlPath)
$output
if ($LASTEXITCODE -ne 0) {
	Write-Host "postbuild.ps1: General error Code: Obfuscation failed"
	return
}

Here we first check whether obfuscar.globaltool already installed, if not we install it. Then we run obfuscar.globaltool with specified config file (obfuscar.xml) and check exit code: if it is not 0 (0 code means success) we write the following message to the output:

 postbuild.ps1: General error Code: Obfuscation failed

That is actual trick: message has special format recognized by Visual Studio. In this case VS will understand that error happened in post publish event and will show error in the build output. More details can be found here: Emitting custom warnings/errors in Visual Studio custom build events. With this approach you will see build errors if obfuscation failed.

Wednesday, June 5, 2024

Use PowerShell in Visual Studio build events when build Docker image for .Net app

Imagine that we have .NET application and created Docker file for it which often contains build and runtime stages (in this example I use .Net6 but it is also valid for higher versions):

# build stage
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
COPY ...
RUN dotnet restore ...
RUN dotnet publish ...

# runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS final
...

and that our VS solution contains build steps which use PowerShell (assume that it is cross platform PowerShell v.7.x). These steps will run during "dotnet publish" step on build stage. You may face with the following error:

powershell command not found

Quick way to fix it is to use "pwsh" command instead of "powershell". It will work because PowerShell became part of .Net SDK docker images since 3.0 preview like described in the following article: Installing PowerShell with one line as a .NET Core global tool. Note also that .Net SDK images are Linux based where "pwsh" command is usual for running PowerShell.

If because of some reason you don't want to change "powershell" to "pwsh" in the code (e.g. if you need to build apps both on Windows and Linux) you may use the following approach: in Docker file add the following command:

RUN ln -s pwsh /usr/bin/powershell

It will add symlink "powershell" which in turn will run pwsh. After that your powershell commands in build events will work both on Windows and Linux including Docker images.

Sunday, April 7, 2024

Introduction to Docker: what is Docker and what problems it solves

Software development is not only about writing the code. From my experience I may tell that writing code itself is not that difficult (get some data from database, show it in UI, process this data, save back to DB, call some external services, etc - tasks are quite common). But that is not all. We also need to ensure that non-functional requirements are met. I.e. that:

  • app handles errors/exceptions correctly
  • works safely (authentication/authorization/policies etc)
  • has good performance
  • scales well for increased loads
  • allows to quickly find the cause of an error/problem in production environment
  • supports updates (preferably without downtime)
  • not expensive for maintenance and support during its life cycle
  • fault-tolerant to hardware failures
  • etc.

And if we will put it this together story becomes not that easy. Let's look at an example. Assume that we have some system which consists of several components which run on the same single server:

How to ensure scalability and fault tolerance of this system? The first thing that comes to mind is to move application components from one physical server to virtual machines, run then on multiple physical servers and provide orchestration between them (setup cluster):

If one VM or server will fall, the rest will continue to work. We won't go here into details how to organize the orchestration of such a cluster (routing of http requests with network load balancer, replication of database servers, distributed caching, logging, etc.) - it is out of scope of the current article. Here we just need to understand the problem so let's continue.

How we just saw with such a cluster scaling and fault tolerance of the system got improved. However if we will look inside virtual machines we'll find that over situation with components and dependencies didn't change much. I.e. different components use different dependencies and it is possible that one application needs certain version of some library, while another needs a different version of the same library. I.e. we need to keep several different versions of the same library in one system.

Also how to update such cluster? Upload updates to each VM and update all components one by one. Yes it is possible to do it that way but what will happen when number of components will grow and number of environments where these components need to be run will also increase? In this case dependencies for different applications are accumulated and we get so-called "matrix of hell":

Maintenance cost of such system will increase as much as many components and environments will be added.

How we can improve that? E.g. if we would be able to package component/service of our distributed system along with all the necessary dependencies, configuration, environment variables, etc. into "something" that would allow us to run this component/service in any environment on any OS (on development machine, standalone server, in cluster, on production stand), then we could simply transfer this "something" between environments:

Here containers and Docker come to the scene. When we talk about containers, first thing which we may imagine is a huge barge carrying cargo containers:

In context of software development this image is a pretty good analogy. As we will see below Docker image is kind of cargo container which contains components and all its dependencies inside. That's why Docker logo looks like a whale carrying cargo containers on its back:

The term "container" came from UNIX-based operating systems. Originally term "jail" was used, but "container" has become the preferred term since 2005 with the release of Sun Solaris 10 and Sun Containers. Container is isolated runtime environment for an app that prevents that app from accessing resources outside its container (allowing access only to those resources that are explicitly allowed).

However manual creation and configuration of containers is quite complex and error-prone process. Docker is used to solve this problem. In context of Docker containers are child processes of Docker background service (Docker daemon). Any software running with Docker runs inside a container.

Containers are launched from images. As it was mentioned above Docker image is good analogue of the cargo container. Images are stored in repositories, which in turn are organized into registries. The most well-known public image registry is DockerHub. Also it is possible to run own private local image registry within the company.

Docker consists from several parts:

  • CLI tool
  • background service (daemon)
  • set of remote services (DockerHub, JFrog, etc.)

Together they simplify management of containers and allow to build own container management infrastructure:

Docker is open source. Although it came from the Linux world, but also runs on Windows (on top of Hyper-V or WSL2) and MacOS. Note however that although it is quite easy to run Linux containers (containers with Linux runtime) in Docker running in Windows (as well as Windows containers of course) since under the hood WSL2 is a lightweight virtual machine with a real Linux Kernel, but running a Windows container in Docker on Linux is not that easy:

There are solutions for that but not that straightforward (e.g. you may run Windows Server Core OS inside VirtualBox, which in turn runs inside Docker container on Linux or use Wine shell). Also licensing issues should be solved since Windows is not free.

Note that container is not the same as virtual machine:

Virtual machines:

  • launch own OS in which the installed software runs
  • require more resources (an average PC can only run a few VMs)
  • start slower
  • support snapshots which is good. But snapshots have own problems: large size, issues with diffs tracking and versioning
  • from one set of VMX/VMDK files only one VM can be launched.

Containers from other side:

  • run on the same host OS kernel
  • require less resources (on an average PC you can run many containers at the same time)
  • start within a few seconds
  • changes are added as an additional layer in union file system: possible to track changes and view history
  • it is possible to start many containers from one image.

Now with this knowledge we may solve matrix of hell mentioned above using Docker containers:

But there is new question: how to manage this matrix? 🙂 Here we come to containers orchestration technologies like Kubernetes, Docker Swarm, etc. This topic is out of scope of the current article (plan to write about that later as well).

And at the end example of how Docker may help developers at everyday work. As developer you may need to run different versions of some database engine simultaneously in order to test functionalities on these versions. Docker is perfect tool for that. E.g. if you run PostgreSQL 16 on you host OS and want to test code on older PostgreSQL 10 you need only 2 commands for that:

docker pull postgres:10
docker run -d -p 5432:5432 --name postgres10 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -it postgres:10

Here I used host port 5432 because don't have any Postgres version running on my host (to be true with Docker I don't want to install any db engines to my host anymore ðŸ™‚ ) i.e. this port is not busy. Otherwise just use different host port and map it to internal port 5432 used by Postgres inside container e.g. "-p 6432:5432"). After that you may connect to db engine and work with it as usual:

 

That's all I wanted to write about Docker here. Hope that this information will help you to understand this technology, will motive to learn it further and use it in your work.

Wednesday, March 20, 2024

Inject class to Python module dynamically

As you probably know in Python after you import some module you may call functions defined in this module. But if you work with more complex scenarios (e.g. when imported module is provided automatically by infrastructure (i.e. you don't control it) or when classes are generated in runtime using Python meta classes) you may face with situation when some class is missing in imported module. In this case Python will show the following error:

AttributeError: module 'SomeExternalModule' has no attribute 'SomeAPI'

(in this example we assume that module name is SomeExternalModule and class name is SomeAPI). Of course if you need functionality defined in missing class you have to resolve this issue properly to ensure that missing class will exist in imported module. But if it is not critical and you just want to pass through these calls you may use trick with injecting class to module dynamically. E.g. class with single static method (method without "self" first argument) can be added to module like that:

def Log(msg):
    print(msg)

SomeExternalModule.SomeAPI = type("SomeAPI", (object, ), {
    "Log": Log
})

Here we injected class SomeAPI to module SomeExternalModule. Class contains one method called Log which just prints message to console. In addition to static methods we may also add instance methods using similar technique - just add "self" first argument to the method in this case.

After that if you will run code which relies on SomeExternalModule.SomeAPI() calls error should gone.

Sunday, March 10, 2024

Cinnamon: first thing you may want to install when switched from Windows to Linux

If you worked in Windows and then switched to Linux it may be painful in beginning since user experience is a bit different (and here we are talking about graphical UX in Linux, not command line). E.g. this is how RHEL8 (Red Hat Enterprise Linux) with default GNOME desktop environment looks like:

Yes, there are windows also but no minimize/maximize icons nor taskbar. The good news is that there is more Windows-like desktop environment available called Cinnamon. Here you may find instructions how to install it on RHEL (you may find more complete list also here). After installation and reboot you will be able to select Cinnamon from available desktop environments list on login screen:


And system will look more familiar for those who worked with Windows:


There will be minimize/maximize icons, taskbar and other familiar things. Hopefully with them transition from Windows to Linux will go smoother.

Tuesday, February 6, 2024

Fix problem with Git client for Linux which asks for credentials on every push with installed SSH key

Recently I faced with the problem that Git client for Linux (CentOS) always asked for user credentials on every push even though SSH key was installed. In general SSH key is installed exactly for avoiding that. So what went wrong?

Let's briefly check whole process. First of all we need to install SSH key pair. On Linux it can be done with ssh-keygen tool. If you don't want to enter passphrase on every push just click Enter on each step. By default it will save public/private key files (id_rsa.pub and id_rsa) into ~/.ssh folder (where ~ means local user folder - usually under /home/local/...). After that copy content of public key file id_rsa.pub and go GitHub > your profile Settings > SSH and GPG keys > SSH keys and paste content there:

Installation of SSH key has been completed. But if you will try to clone some repository and try to push changes there (assuming that you have write permission in this repository) git may still ask for username/pw credentials and every push. As it turned out it depends on how repository was cloned. There are several ways to clone repositories: HTTPS, SSH and GitHub CLI (HTTPS tab goes first in UI)

Mentioned problem with credentials appears when repository is cloned via HTTPS. Solution here is to clone repository with SSH instead:

After that git should not ask you for credentials anymore.

Tuesday, January 9, 2024

Verify JWT tokens with EdDSA encryption algorithm

In my previous posts of this series I showed how to generate EdDSA private and public keys pair and how to sign JWT tokens using private EdDSA key. In this last post of the series I will show how to verify signed JWT token with public key.

Let's remind that EdDSA key pair may look like that in JSON format:

{
	"kty": "OKP",
	"alg": "EdDSA",
	"crv": "Ed25519",
	"x": "...",
	"d": "..."
}

where "x" property is used for public key and "d" is for private key. Private key (d) was used for signing. For verification we need to use public key (x).
For token validation we will use JsonWebTokenHandler.ValidateTokenAsync() method from Microsoft.IdentityModel.JsonWebTokens. Here is the code which decodes token:

string token = ...;
var jwk = ...; // get EdDSA keys pair
var pubKey = new EdDsaSecurityKey(new Ed25519PublicKeyParameters(Base64UrlEncoder.DecodeBytes(jwk.X), 0));
pubKey.KeyId = jwk.KeyId;
var result = await new JsonWebTokenHandler().ValidateTokenAsync(token, new TokenValidationParameters()
{
  ValidIssuer = JwtHelper.GetServiceName(jwk),
  AudienceValidator = (AudienceValidator) ((audiences, securityToken, validationParameters) => true), // or whatever logic is needed for verifying aud claimm
  IssuerSigningKey = (SecurityKey) pubKey
});
if (!result.IsValid)
  throw result.Exception;
json = JWT.Payload(token);

Here we use EdDsaSecurityKey class from ScottBrady.IdentityModel.Tokens.
If public key matches private key which was used for signing then result.IsValid will be true (otherwise code will throw exception). At the end we call JWT.Payload() from jose-jwt to get JSON token representation (from which we may get needed claims and other data).

With these techniques you may generate EdDSA keys, sign tokens and verify them. Hopefully information in these posts will help you.

Monday, December 25, 2023

Sign JWT tokens with EdDSA encryption algorithm

In my previous post of this series I showed how to generate key pair for EdDSA encryption algorithm. Let's now go further and use these keys to sign JWT token. If you remember from previous post "d" property of json object with keys pair belongs to private key. We will use this private key for signing our JWT token.

For creating JWT token we need to define claims. They are app/domain specific. We can add e.g. iss (issuer), exp (expired) and other standard claims (standard claims are defined in RFC 7519). Also we may add custom claims as we need in the app:

List<Claim> claims = ...; // fill claims

Then we need to load private key (from some secrets storage/vault usually):

var jwk = ...; // load private key

this jwk object may be json object showed in my previous post (plus it should have keyId string property for key identifier which may contain e.g. some guid).

Then we need to create EdDSA security key object and create signed token. We can do that using ScottBrady.IdentityModel nuget package (it uses Portable.BouncyCastle internally):

var edDsaSecurityKey = new EdDsaSecurityKey(new Ed25519PrivateKeyParameters(Base64UrlEncoder.DecodeBytes(jwk.d), 0));
edDsaSecurityKey.KeyId = jwk.keyId;
var securityTokenHandler = new JwtSecurityTokenHandler();
string token = securityTokenHandler.WriteToken(securityTokenHandler.CreateToken(new SecurityTokenDescriptor
{
    Subject = new ClaimsIdentity(claims),
    Issuer = ..., // define issuer (iss) claim as you need
    Expires = new DateTime(DateTime.UtcNow.AddMinutes(1)), // add expired date as you need
    SigningCredentials = new SigningCredentials(edDsaSecurityKey, "EdDSA")
}));

This code will create JWT token signed with EdDSA private key. In the next post I will show how to verify this token using public EdDSA key.

Update 2024-01-09: see also Verify JWT tokens with EdDSA encryption algorithm.

Tuesday, December 12, 2023

Fix Linq 2 NHibernate for MySQL

If you use NHibernate with MySQL and Linq 2 NHibernate to simplify fetching data you may face with problem: queries created by Linq2NH use square brackets by default. That is fine for SQL Server but won't work in MySQL which uses backticks ``.

For MySQL we need to instruct NHibernate to use backticks instead of square brackets. It can be done by setting interceptor in NH config:

public class NHConfiguration
{
    public static Configuration Build(string connStr)
    {
        var config = Fluently.Configure()
            .Database(
                MySQLConfiguration.Standard
                    .ConnectionString(connStr)
                    .AdoNetBatchSize(100)
                    .DoNot.ShowSql()
            )
            .Mappings(cfg =>
            {
                // add mappings
            })
            .ExposeConfiguration(x =>
            {
with backticks for MySQL
                x.SetInterceptor(new ReplaceBracesWithBackticksInterceptor());
            });

        return config.BuildConfiguration();
    }
}

in its OnPrepareStatement method we just replace square brackets with backticks:

public class ReplaceBracesWithBackticksInterceptor : EmptyInterceptor
{
    public override NHibernate.SqlCommand.SqlString OnPrepareStatement(NHibernate.SqlCommand.SqlString sql)
    {
        return sql.Replace("[", "`").Replace("]", "`");
    }
}

After that Linq2NH will start working in MySQL.

Saturday, November 18, 2023

Use Obfuscar for obfuscating ASP.Net Core web apps

Obfuscar is free and open source code obfuscation library for .NET. However if you want to use it in web app (ASP.Net Core) it is not that straightforward. First of all you should be ready that Obfuscar may break many things in runtime quite easily. I.e. project will be still compiled successfully but when you will try to run it may get unclear errors. Second - in web apps it is even more tricky to use Obfuscar comparing e.g. with console apps or dll class libraries. In this post I will describe some findings which may help you for using Obfuscar in ASP.Net Core web app.

For using Obfuscar you need to have xml config file. Here is obfuscar.xml file which can be used for obfuscating ASP.Net Core web app (below I will describe it in more details):

<?xml version='1.0'?>
<Obfuscator>
  <Var name="InPath" value="..." />
  <Var name="OutPath" value="..." />
  
  <!-- don't obfuscate Public classes and methods to avoid runtime exceptions -->
  <Var name="KeepPublicApi" value="true" />
  <Var name="HidePrivateApi" value="true" />

  <!-- Additional search path for ASP.Net Core dlls (to avoid) depending on currently used .NET version -->
  <AssemblySearchPath path="C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\6.0.25\ref\net6.0" />

  <Module file="$(InPath)\MyWebApp.dll">
    <!-- don't obfuscate anonymous types to have less problems when pass objects from server side to client side (e.g. in json format) -->
    <SkipType name="*AnonymousType*" skipProperties="true" skipMethods="true" skipFields="true" skipEvents="true" skipStringHiding="true" />
    <!-- don't obfuscate classes generated from cshtml to avoid runtime exceptions -->
    <SkipNamespace name="AspNetCoreGeneratedDocument" />
    <!-- don't obfuscate middlewares to avoid runtime exceptions -->
    <SkipNamespace name="MyWebApp.Middleware" />
  </Module>
</Obfuscator>

First of all we need to use default Obfuscar behavior when public classes and methods are not changed and only private (and internal) classes/methods are obfuscated:

  <!-- don't obfuscate public classes and methods to avoid runtime exceptions -->
  <Var name="KeepPublicApi" value="true" />
  <Var name="HidePrivateApi" value="true" />

Otherwise runtime exceptions will occur.

Second - we need to add additional search path where Obfuscar will search for references ASP.Net Core assemblies:

  <!-- Additional search path for ASP.Net Core dlls (to avoid) depending on currently used .NET version -->
  <AssemblySearchPath path="C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\6.0.25\ref\net6.0" />

Without that we will get "Unable to resolve dependency Microsoft.AspNetCore.Mvc.Razor" error during obfuscation. This path depends on actual .NET version which you use in the project. In above example I used .NET 6.0.25:

(I will write separate blog post how to get this .NET version automatically instead of hard coding it in obfuscar.xml)

Third - we need to specify our dll module with few rules:

  <Module file="$(InPath)\MyWebApp.dll">
    <!-- don't obfuscate anonymous types to have less problems -->
    <SkipType name="*AnonymousType*" skipProperties="true" skipMethods="true" skipFields="true" skipEvents="true" skipStringHiding="true" />
    <!-- don't obfuscate classes generated from cshtml to avoid runtime exceptions -->
    <SkipNamespace name="AspNetCoreGeneratedDocument" />
    <!-- don't obfuscate middlewares to avoid runtime exceptions -->
    <SkipNamespace name="MyWebApp.Middleware" />
  </Module>

1. For skipping obfuscation of anonymous types (e.g. if you have controller action which returns JsonResult and returns object of anonymous type from there (which is quite common practice) it will stop working because properties names of returned object will be changed by Obfuscar and since your client side (javascript) scripts will still expect the same properties names in received json object which you specified in C# code they will stop working).

2. For skipping obfuscation of classes from AspNetCoreGeneratedDocument namespace. If you use Razor view engine and cshtml files for views compiler will generate view classes for these cshtml files under the hood. These classes by default have AspNetCoreGeneratedDocument namespace. Obfuscation of these classes also should be skipped because otherwise you will just get empty browser window.

3. For skipping obfuscation of middlewares - otherwise will get runtime exceptions because middleware classes should have predefined public method (async Task Invoke(HttpContext context)). This is needed only if middlewares are defined as internal classes.

With this config basic ASP.Net Core web app should work. Of course I could forgot something or didn't find it yet - in this case will update this post. Also there may be other things which may be needed. If you know them please share in comments.


Tuesday, November 7, 2023

Bind complex view model objects in ASP.Net Core using ComplexObjectModelBinder

The way how view model objects are bound has been changed in ASP.Net Core. Now we need to create custom model binder class which inherits IModelBinder interface and implement binding logic there (in ASP.Net Core there is no DefaultModelBinder base class anymore which we may inherit in custom binders). However for most cases binding logic itself will be the same still - only custom logic will differ (like validation specific to view model class). Instead of implementing this binding logic for each view model class by ourselves we may use builtin ComplexObjectModelBinder which will do actual work for us. In this post I will show how to do that.

Let's say we have RequestModel view model class which can be used for asking contact information (name, email, phone):

public class RequestModel
{
    public string Name { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
}

For binding this model we need to implement some infrastructural classes. First of all we need custom model binder provider which inherits IModelBinderProvider. But instead of creating own binder provider for each view model class let's create generic ComplexModelBinderProvider class which can be used with any view model:

public class ComplexModelBinderProvider<TModel, TBinder> : IModelBinderProvider
{
    private ComplexObjectModelBinderProvider complexObjectBinderProvider;

    public ComplexModelBinderProvider(ComplexObjectModelBinderProvider complexObjectBinderProvider)
    {
        this.complexObjectBinderProvider = complexObjectBinderProvider;
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(TModel))
        {
            return (IModelBinder)Activator.CreateInstance(typeof(TBinder),
                new object[] { complexObjectBinderProvider.GetBinder(context) });
        }
        return null;
    }
}

It use 2 generic types: one for view model class (TModel) and another for its model binder which will be used for binding objects of TModel. Note that when it creates model binder it passes instance of ComplexObjectModelBinder to constructor (complexObjectBinderProvider.GetBinder(context)).

We also need custom model binder class RequestModelBinder for our view model - but instead of implementing binding logic there we will just call ComplexObjectModelBinder which was injected in constructor and which will do all complex work (get values from form values, query string params, etc):

public class RequestModelBinder : IModelBinder
{
    protected IModelBinder complexObjectBinder;
	
    public RequestModelBinder(IModelBinder complexObjectBinder)
    {
	    this.complexObjectBinder = complexObjectBinder;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var model = new RequestModel();
        bindingContext.Model = model;
        await this.complexObjectBinder.BindModelAsync(bindingContext);
		
		// add custom logic there if needed
    }
}

Now we just need to link all of that together in Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(
    o =>
    {
        var complexObjectBinderProvider = o.ModelBinderProviders.First(p => p is ComplexObjectModelBinderProvider) as ComplexObjectModelBinderProvider;
        o.ModelBinderProviders.Insert(0, new ComplexModelBinderProvider<RequestModel, RequestModelBinder>(complexObjectBinderProvider));
        // add more ComplexModelBinderProvider instances for other view models
        ...
    });

Here we added our generic ComplexModelBinderProvider for RequestModel view model which during binding will create instance of RequestModelBinder which in turn will bind model by delegating work to ComplexObjectModelBinder. The more view models we have in the code the more advantages this approach will bring since we will just reuse the same binding logic for all models.

Wednesday, October 11, 2023

Generate EdDSA key pair in .NET and save it to json

EdDSA is one of the commonly used encryption algorithms atm. There is a trend to use it instead of older RSA alg. In .Net there are not that many resources about it. E.g. popular nuget package jose-jwt (Javascript Object Signing and Encryption) still doesn't support EdDSA. Fortunately there are another packages which support it:

but many basic examples are still missing. In this post I will show how to generate EdDSA key pair in .NET6 using above packages and save it in json format for later use.

Here is the code which generates key pair (before to run it install both nuget packages):

var keyPairGenerator = new Ed25519KeyPairGenerator();
keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
var keyPairParams = keyPairGenerator.GenerateKeyPair();

var privateKeyParams = (Ed25519PrivateKeyParameters)keyPairParams.Private;
var publicKeyParams = (Ed25519PublicKeyParameters)keyPairParams.Public;

var keyPair = new EddsaKeyPair { d = Base64UrlEncoder.Encode(privateKeyParams.GetEncoded()), x = Base64UrlEncoder.Encode(publicKeyParams.GetEncoded()) };

File.WriteAllText("keys.json", JsonConvert.SerializeObject(keyPair));

public class EddsaKeyPair
{
    public string kty => "OKP";
    public string alg => "EdDSA";
    public string crv => "Ed25519";
    public string x { get; set; } // public key
    public string d { get; set; } // private key
}

As result it will save EdDSA keys pair to keys.json file which will look like this:

{
	"kty": "OKP",
	"alg": "EdDSA",
	"crv": "Ed25519",
	"x": "...",
	"d": "..."
}

Here "x" property is used for public key and "d" is for private key. Having key pair you will be able e.g. sign and verify jwt tokens in your app.

Update 2024-01-09: see also next posts from this series Sign JWT tokens with EdDSA encryption algorithm and Verify JWT tokens with EdDSA encryption algorithm.


Wednesday, September 27, 2023

Problem with datetime format used in %date% environment variable in Windows scheduled tasks

Recently I've faced with interesting problem. Let's say you have Russian datetime format set in Windows (Control panel > Region > Formats) and create new Windows scheduled task which runs the following cmd script:

echo %date%

It will use Russian datetime as it should. Now we change OS datetime format to English. If we will run the same script manually it will immediately use new format. But if we will run our existing scheduled task it will still use old Russian datetime format. Even if we will restart OS and will try to run scheduled task again result will be the same: it will still use old Russian datetime format.

Solution which I found so far is to recreate scheduled task (export it, delete and then import). After that it will use current (English) datetime format.

Wednesday, September 20, 2023

Use Github secrets to restore nuget packages from private packages source with authentication in Dockerfile via Github actions

If you use private nuget packages source with authentication and Docker in your project you may need to restore packages from this custom packages source within Docker file. In this post I will show how to use Github secrets for that when you build Docker image via docker/build-push-action Github action.

First of all in the yaml file of our Github action we need to pass necessary secrets references to the build action using the following syntax:

name: Build
id: docker_build
uses: docker/build-push-action@v5
with:
  ...
  secrets: |
    "NUGET_USERNAME=${{ secrets.NUGET_USERNAME }}"
    "NUGET_PWD=${{ secrets.NUGET_PWD }}"

After that in Docker file we fetch passed secrets (they are stored to special files under /run/secrets/... path which is available during Docker image build) and will store them to environment variables using export command. After that we will add our private packages source with username and password (using dotnet nuget add source). When it will be done we will be able to run "dotnet restore" command which will restore project dependencies including those which come from private nuget source:

COPY Foo.csproj src/

RUN --mount=type=secret,id=NUGET_USERNAME \
	--mount=type=secret,id=NUGET_PWD \
	export NUGET_USERNAME=$(cat /run/secrets/NUGET_USERNAME) && \
	export NUGET_PWD=$(cat /run/secrets/NUGET_PWD) && \
	dotnet nuget add source https://my-private-packages-source/index.json --name FooPackages --username "${NUGET_USERNAME}" --password "${NUGET_PWD}" --store-password-in-clear-text

RUN dotnet restore "src/Foo.csproj" /p:IsDockerBuild=true

Note that it is important to pipe commands which export environment variables and then use them to the same single RUN command. If you will try to use these variables in separate RUN command "nuget add source" will tell that "Package source with Name: ... added successfully" but then you will get confusing error when will try to run "dotnet restore":

Error NU1301: Unable to load the service index for source

But if everything is done in the way how it is described above then your project dependencies should be restored successfully for your Docker image.