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.

Tuesday, August 15, 2023

Problem with MissingManifestResourceException for embedded resx files in subfolders

If you worked with .Net applications you most probably know what is embedded resources:

Such resources are embedded directly to output assembly. We may check them e.g. with decompiler:

 

Sometime we may need to change namespace of generated resources: e.g. in above example Foo.resx file is located in Resources sub folder so by default it will generate strongly typed class in namespace TestApp.Resources:

Note also line 42 when it created ResourceManager object - it also uses TestApp.Resources namespace. Now in properties of resx file we will explicitly specify Custom tool namespace = TestApp:

After that namespace of automatically generated class will be changed from TestApp.Resources to TestApp. However ResourceManager will be still created with the same old namespace TestApp.Resources:

We may modify .Designer.cs file manually and change namespace of ResourceManager to TestApp (note that if custom tool will run again it will override manual changes in cs file and they should be done again):

But if we will try to get string from generated class Foo.Bar we will get MissingManifestResourceException:

Unhandled exception. System.Resources.MissingManifestResourceException: Could not find the resource "TestApp.Foo.resources" among the resources "TestApp.Resources.Foo.resources" embedded in the assembly "TestApp", nor among the resources in any satellite assemblies for the specified culture. Perhaps the resources were embedded with an incorrect name.

The problem is that after our changes resx file is still embedded to the assembly as TestApp.Resources.Foo.resources:

In order to fix this error we need to edit csproj file and add LogicalName element under EmbeddedResource element for our resx file with correct name:

<EmbeddedResource Update="Resources\Foo.resx">
  <Generator>ResXFileCodeGenerator</Generator>
  <LastGenOutput>Foo.Designer.cs</LastGenOutput>
  <CustomToolNamespace>TestApp</CustomToolNamespace>
  <LogicalName>TestApp.Foo.resources</LogicalName> 
</EmbeddedResource>

After that resource will be embedded to the assembly with correct name TestApp.Foo.resources:

and exception will gone

Tuesday, August 8, 2023

Camlex and Camlex.Client 5.4.2 released

New version 5.4.2 of Camlex library has been released. Starting with this version it became possible to generate CAML queries with string operators BeginsWith and Contains for ContentTypeId field type e.g. the following C# code:

Camlex.Query().Where(x => ((DataTypes.ContentTypeId)x["ContentTypeId"]).StartsWith("0x123")).ToString(true);

will generate the following CAML query:

<Query>
  <Where>
    <BeginsWith>
      <FieldRef Name="ContentTypeId" />
      <Value Type="ContentTypeId">0x123</Value>
    </BeginsWith>
  </Where>
</Query>

This is useful since when you add some site content type to SharePoint list under the hood SharePoint creates inherited content type (which has ContentTypeId which starts with ContentTypeId of parent site content type with appended 00 symbols and guid without dashes) and exactly this inherited content type is then used for list items created in this list. In order to fetch all items created with original site content type we may use CAML query with BeginsWith operator and ContentTypeIdof parent site content type.

As usual new version is available via Nuget.

Tuesday, July 4, 2023

Generate database schema for MySQL using NHibernate hbm2ddl tool

NHibernate has hbm2ddl tool which allows automatically export database tables schema based on provided mappings. I.e. we may define mapping using C# for some POCO class:

public class User
{
    public virtual int Id { get; set; }
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
}

like that:

public class UserMap : ClassMap<User>
{
    public UserMap()
    {
        Table("[User]");
        Id(x => x.Id, "UserId");
        Map(x => x.FirstName);
        Map(x => x.LastName);
     }
}

and then based on this mapping hbm2ddl will generate the following SQL code for creating User table:

create table `User` (
    UserId INTEGER NOT NULL AUTO_INCREMENT,
    FirstName TEXT,
    LastName TEXT,
    primary key (UserId)
);

That is convenient because we don't need to maintain database schema separately - any change in C# models and mappings will be automatically reflected in db schema. However in order to use hbm2ddl for MySQL we need to add few tweaks to the export code:

  • MySQL uses back ticks instead of square brackets
  • MySQL syntax requires semicolon after each code line in generated SQL

First of all we need to create Fluent NHibernate configuration for MySQL syntax:

var config = Fluently.Configure()
    .Database(
        MySQLConfiguration.Standard
            .ConnectionString(connectionString)
            .AdoNetBatchSize(100)
            .DoNot.ShowSql()
    )
    .Mappings(cfg =>
    {
        cfg.FluentMappings.AddFromAssemblyOf<UserMap>()
            .Conventions.Setup(mappings =>
            {
                mappings.AddAssembly(typeof(UserMap).Assembly);
            });
    });

With this Fluent NHibernate config export code will look like this:

var nhConfig = ... // see above
var export = new SchemaExport(nhConfig);
var sb = new StringBuilder();
export.Create(schema =>
{
    schema = schema.Replace("[", "`").Replace("]", "`");
    if (schema.EndsWith("\r\n"))
    {
        schema = schema.Substring(0, schema.Length - 2) + ";\r\n";
    }
    else
    {
        schema += ";";
    }
    sb.Append(schema);
}, false);

Console.WriteLine(sb.ToString());

Here we replace square brackets by back ticks and add semicolon to the end of each added line. After that we will have valid SQL code for MySQL syntax with database schema.

Thursday, June 22, 2023

Implement TAP/multithread friendly logging scopes for Microsoft.Extensions.Logging.ILogger

Some time ago I wrote a post how to implement custom logger which writes logs to Azure storage blob container: Custom logger for .Net Core for writing logs to Azure BLOB storage. This logger implements ILogger interface from Microsoft.Extensions.Logging namespace. It works quite well but doesn't support logging scopes:

public IDisposable BeginScope<TState>(TState state) => default!;

Logging scopes are quite useful - they allow to specify additional information e.g. from each method log record was added, etc:

public void Foo()
{
    using (logger.BeginScope("Outer scope"))
    {
        ...
        using (logger.BeginScope("Inner scope"))
        {
        }
    }
}

Important requirement for logging scopes is that it should work properly in Task-based asynchronous pattern (TAP) and multithread code (which is widely used nowadays). For that we will use AsyncLocal<T> class from .NET. For implementing scopes themselves we will use linked list (child-parent relation).

For implementing it we will create LogScopeProvider class which implements Microsoft.Extensions.Logging.IExternalScopeProvider interface (for creating this custom LogScopeProvider I used code from Microsoft.Extensions.Logging.Console namespace as base example):

public class LogScopeProvider : IExternalScopeProvider
{
    private readonly AsyncLocal<LogScope> currentScope = new AsyncLocal<LogScope>();

    public object Current => this.currentScope.Value?.State;

    public LogScopeProvider() {}

    public void ForEachScope<TState>(Action<object, TState> callback, TState state)
    {
        void Report(LogScope current)
        {
            if (current == null)
            {
                return;
            }
            Report(current.Parent);
            callback(current.State, state);
        }

        Report(this.currentScope.Value);
    }

    public IDisposable Push(object state)
    {
        LogScope parent = this.currentScope.Value;
        var newScope = new LogScope(this, state, parent);
        this.currentScope.Value = newScope;

        return newScope;
    }

    private class LogScope : IDisposable
    {
        private readonly LogScopeProvider provider;
        private bool isDisposed;

        internal LogScope(LogScopeProvider provider, object state, LogScope parent)
        {
            this.provider = provider;
            State = state;
            Parent = parent;
        }

        public LogScope Parent { get; }

        public object State { get; }

        public override string ToString()
        {
            return State?.ToString();
        }

        public void Dispose()
        {
            if (!this.isDisposed)
            {
                this.provider.currentScope.Value = Parent;
                this.isDisposed = true;
            }
        }
    }
}

Note that LogScopeProvider stores scopes as AsyncLocal<LogScope> which allows to use it in TAP code. So e.g. if we have await inside using(scope) it will be handled correctly:

public async Task Foo()
{
    using (logger.BeginScope("Outer scope"))
    {
        var result = await Bar();
        ...
    }
}

Now returning to our BlobStorage: all we have to is to pass LogScopeProviderto its constructor, add current scope to the log record and return new scope when it is requested (check bold code):

public class BlobLogger : ILogger
{
    private const string CONTAINER_NAME = "custom-logs";
    private string connStr;
    private string categoryName;
    private LogScopeProvider scopeProvider;
 
    public BlobLogger(string categoryName, string connStr, LogScopeProvider scopeProvider)
    {
        this.connStr = connStr;
        this.categoryName = categoryName;
        this.scopeProvider = scopeProvider;
    }
 
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
        Func<TState, Exception?, string> formatter)
    {
        if (!IsEnabled(logLevel))
        {
            return;
        }
 
        string scope = this.scopeProvider.Current as string;
        using (var ms = new MemoryStream(Encoding.UTF8.GetBytes($"[{this.categoryName}: {logLevel,-12}] {scope} {formatter(state, exception)}{Environment.NewLine}")))
        {
            var container = this.ensureContainer();
            var now = DateTime.UtcNow;
            var blob = container.GetAppendBlobClient($"{now:yyyyMMdd}/log.txt");
            blob.CreateIfNotExists();
            blob.AppendBlock(ms);
        }
    }
 
    private BlobContainerClient ensureContainer()
    {
        var container = new BlobContainerClient(this.connStr, CONTAINER_NAME);
        container.CreateIfNotExists();
        return container;
    }
 
    public bool IsEnabled(LogLevel logLevel) => true;
 
    public IDisposable BeginScope<TState>(TState state) => this.scopeProvider.Push(state);
}

That's it: now our logger also supports logging scopes.

Saturday, June 17, 2023

Profile MySQL db with Neor Profile SQL

If you need to profile MySQL db you may use builtin MySQL shell profiler (CLI). Also there is free GUI alternative - Neor Profile SQL. There are few things which should be done before to use it.

When you launch Profile SQL it asks to establish connection using default parameters for localhost:

If you click Test button you may get "Test is failed" error even with correct credentials of the root user. In order to avoid it you need to enable native MySQL password for the root user:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '...'

after that connection should be successful.

But that is not all. If after that you will run application which connects to MySQL you won't see sessions and queries in profiler - because profiler works as a proxy with own port (4040 by default):

and in order to collect data your application should connect to profiler (not to MySQL directly). I.e. we need to change port in connection string from MySQL port (3306 by default) to profiler port (4040):

{
  "ConnectionStrings": {
    "Default": "Server=localhost;Port=4040;Database=Test;User ID=test;Password=..."
  }
}

If after that connection will fail with System.IO.IOException (from MySql.Data.Common.Ssl namespace methods) add ";SSL Mode=None" to connection string:

{
  "ConnectionStrings": {
    "Default": "Server=localhost;Port=4040;Database=Test;User ID=test;Password=...;SSL Mode=None"
  }
}

After that application should connect to profiler successfully and you should see profiled data.

StackOverflowException when use hierarchical object mapping with AutoMapper and Fluent NHibernate lazy loading

Suppose that we have the following class Category with self-reference (regular parent-child hierarchy):

public class Category
{
    public virtual T Id { get; set; }
    public virtual string Name { get; set; }
    public virtual Category ParentCategory { get; set; }
    public virtual IList<Category> ChildCategories { get; set; }
}

(properties are virtual which is required by NHibernate). Regular Fluent NHibernate mapping for this class looks like this:

public class CategoryMap : ClassMap<Category>
{
    public CategoryMap()
    {
        Table("[Category]");
        Id(x => x.Id);
        
        Map(x => x.Name);

        References(x => x.ParentCategory)
            .Column("ParentCategoryId");
        
        HasMany(x => x.ChildCategories)
            .KeyColumn("ParentCategoryId")
            .Inverse()
            .AsBag();
    }
}

Then we want to show category in the view of ASP.Net Core MVC app and use the following view model for that:

public class CategoryModel
{
    public int? Id { get; set; }

    public string Name { get; set; }

    public string ParentCategoryName { get; set; }
}

For mapping Category to CategoryModel the following AutoMapper mapping is used:

public class ViewModelProfile : Profile
{
    public ViewModelProfile()
    {
        CreateMap<Category, CategoryModel>()
            .ForMember(x => x.ParentCategoryName, o => o.MapFrom(c => c.ParentCategory == null ? "" : c.ParentCategory.Name))
    }
}

i.e. when ParentCategory is null then ParentCategoryName property of view model is empty string, otherwise ParentCategoryName will return name of parent category. Until now everything looks good. Problems will start when we will try to create category of nesting level which is greater than 1, i.e. this will work:

child category -> parent category

but with deeper hierarchy:

child category1 -> child category2 -> parent category

we will get StackOverflowException when will try to pass view model (created by AutoMapper) to the view. Googling this problem suggests to use MaxDepth(2) call when create AotuMapper map profile to limit number of processed hierarchy levels but it didn't help because of some reason.

Solution which worked was to specify to not use lazy loading for ParentCategory property in Fluent NHibernate mapping (NHibernate uses lazy loading by default):

public class CategoryMap : ClassMap<Category>
{
    public CategoryMap()
    {
        Table("[Category]");
        Id(x => x.Id);
        
        Map(x => x.Name);

        References(x => x.ParentCategory)
            .Column("ParentCategoryId")
            .Not.LazyLoad();
        
        HasMany(x => x.ChildCategories)
            .KeyColumn("ParentCategoryId")
            .Inverse()
            .AsBag();
    }
}

In theory it may cause performance problems with large amount of data and deep hierarchy levels but with reasonable amount of data should work quite well.

Monday, May 29, 2023

Run ASP.Net Core Web API on Kestrel dev web server with https on Windows

In this post I will describe how to run ASP.Net Core Web API on Kestrel development web server under https. First of all we need to create self-signed SSL certificate. We may generate it with PowerShell (see Use self-signed SSL certificate for web API hosted in Azure App service) or openssl tool:

openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout test.key -out test.crt -config test.conf -passin pass:123
openssl pkcs12 -export -out test.pfx -inkey test.key -in test.crt -passout pass:123

For running above commands we will need config file test.conf with information about domain name. It may look like that:

[req]
default_bits = 2048
default_keyfile = test.key
distinguished_name = req_distinguished_name
req_extensions = req_ext
x509_extensions = v3_ca

[req_distinguished_name]
countryName =
countryName_default =
stateOrProvinceName =
stateOrProvinceName_default =
localityName =
localityName_default =
organizationName = Test
organizationName_default = Test
organizationalUnitName = organizationalunit
organizationalUnitName_default = Development
commonName = api.example.com
commonName_default = api.example.com
commonName_max = 64

[req_ext]
subjectAltName = @alt_names

[v3_ca]
subjectAltName = @alt_names

[alt_names]
DNS.1 = api.example.com

Once private key (pfx) is created we may install it to the local certificates store: double click pfx, and follow certificate installation wizard with default settings:


We will also need to provide password for private key in this wizard (in above example "123").

Once certificate is installed to the certificates store we need to set the following parameters in appsettings.json file of our ASP.Net Core Web API project:

{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://api.example.com:5057"
      },
      "HttpsInlineCertStore": {
        "Url": "https://api.example.com:5058",
        "Certificate": {
          "Subject": "api.example.com",
          "Store": "My",
          "Location": "CurrentUser",
          "AllowInvalid": true
        }
      }
    }
  }
}

(since in our example self-signed certificate is used we need to set AllowInvalid: true parameter). If everything is done correctly Web API will run on local Kestrel dev server under https.

Thursday, May 18, 2023

Generate action urls from C# lambda expressions in ASP.Net MVC Core: good news for those who missed it there

In ASP.Net MVC (on top on .Net Framework) there was useful mechanism for generating actions urls from C# lambda expressions. Let's say we have controller UserController for managing users which has List action with 3 params:

  • page number (in case if users list is large and we need to use pagination)
  • sort by (first name, last name, etc)
  • sort direction (asc or desc)
public enum SortDirection
{
    Asc,
    Desc
}

public class UserController : Controller
{
    public ActionResult List(int pageNumber, string sortBy, SortDirection sortDirection)
    {
        ...
    }
}

In this case we could generate url for this action using generic ActionLink<T> method like this:

Html.ActionLink<UserController>(c => c.List(0, "FirstName", SortDirection.Asc), "All users")

which will generate the following url:

/user/list?pageNumber=0&sortBy=FirstName&sortDirection=SortDirection.Asc

That is convenient method since we have compile-time check for the code. Compare it with classic way:

Html.ActionLink("All users", "List", "Users", new { pageNumber = 0, sortBy = "FirstName", sortDirection = SortDirection.Asc })

In the last case if e.g. action name, parameters names or number of params will be changed we won't get any errors or warning during compilation. Instead we may get unexpected behavior or runtime error (e.g. if new parameter was added it will have default value if we won't explicitly add it to link generation code).

The problem is that strongly-typed method is not available in ASP.Net Core. Don't know what was the reason for not adding it there (except mentioned advantage of having compile-time check it may also be a problem during migration of old ASP.Net MVC app to ASP.Net Core) but fortunately it is possible to get it back there.

Lets check steps which are needed for generating action url from expression: we need to get controller name (/user), action name (/list), list action parameters names (or get route values from expression how it is called in ASP.Net MVC) and (most tricky one) get value of each parameter passed to expression. And then concatenate all parts to one string. First 3 steps are relatively easy: they can be done by basic reflection. But last step (get values of parameters passed to lambda expression) needs extra attention. In the past I already faced with that need in Camlex.NET (open source library for Sharepoint developers which I maintain in free time): Runtime evaluation of lambda expressions. We can use the same technique here as well. Also (as I found out during experiments) code for generating routing values from expression (list parameters names) can be reused with small changes from internal method of ASP.Net MVC Microsoft.Web.Mvc.Internal.ExpressionHelper.GetRouteValuesFromExpression in ASP.NET Core - it will help to save our time.

Now if we will combine all of this we may create helper class for generating urls from expressions in ASP.Net Core:

public static class HtmlHelperExtensions
{
    public static string ActionLink<TController>(this IHtmlHelper html, Expression<Action<TController>> actionExpression)
    {
        return ActionLink(actionExpression, GetRouteValuesFromExpression(actionExpression));
    }

    public static string ActionLink<TController>(this IHtmlHelper html, Expression<Action<TController>> actionExpression, RouteValueDictionary routeValues)
    {
        return ActionLink(actionExpression, routeValues);
    }

    public static string ActionLink<TController>(Expression<Action<TController>> actionExpression, RouteValueDictionary routeValues)
    {
        string controllerName = typeof(TController).GetControllerName();
        string actionName = actionExpression.GetActionName();
        var sb = new StringBuilder($"/{controllerName}/{actionName}");
        if (routeValues != null)
        {
            bool isFirst = true;
            foreach (var routeValue in routeValues)
            {
                sb.Append(isFirst ? "?" : "&");
                sb.Append($"{routeValue.Key}={routeValue.Value}");
                isFirst = false;
            }
        }
        return sb.ToString();
    }

    private static string GetControllerName(this Type controllerType)
    {
        return controllerType.Name.Replace("Controller", string.Empty);
    }

    private static string GetActionName(this LambdaExpression actionExpression)
    {
        return ((MethodCallExpression)actionExpression.Body).Method.Name;
    }

    // copy of Microsoft.Web.Mvc.Internal.ExpressionHelper.GetRouteValuesFromExpression
    private static RouteValueDictionary GetRouteValuesFromExpression<TController>(
        Expression<Action<TController>> action)
    {
        if (action == null)
            throw new ArgumentNullException(nameof(action));
        if (!(action.Body is MethodCallExpression body))
            throw new ArgumentException("MustBeMethodCall");
        string name = typeof(TController).Name;
        string str = name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)
            ? name.Substring(0, name.Length - "Controller".Length)
            : throw new ArgumentException("TargetMustEndInController");
        if (str.Length == 0)
            throw new ArgumentException("CannotRouteToController");
        string targetActionName = GetTargetActionName(body.Method);
        var rvd = new RouteValueDictionary();
        AddParameterValuesFromExpressionToDictionary(rvd, body);
        return rvd;
    }

    private static void AddParameterValuesFromExpressionToDictionary(
        RouteValueDictionary rvd,
        MethodCallExpression call)
    {
        ParameterInfo[] parameters = call.Method.GetParameters();
        if (parameters.Length <= 0)
            return;
        for (int index = 0; index < parameters.Length; ++index)
        {
            Expression expression = call.Arguments[index];
            object obj = !(expression is ConstantExpression constantExpression)
                ? Expression.Lambda<Func<object, object>>((Expression)Expression.Convert(expression, typeof(object)), Expression.Parameter(typeof(object), "_unused")).Compile().Invoke((object)null)
                : constantExpression.Value;
            rvd.Add(parameters[index].Name, obj);
        }
    }

    private static string GetTargetActionName(MethodInfo methodInfo)
    {
        string name = methodInfo.Name;
        ActionNameAttribute actionNameAttribute = !methodInfo.IsDefined(typeof(NonActionAttribute), true)
            ? methodInfo.GetCustomAttributes(typeof(ActionNameAttribute), true).OfType<ActionNameAttribute>().FirstOrDefault<ActionNameAttribute>()
            : throw new InvalidOperationException("CannotCallNonAction");
        if (actionNameAttribute != null)
            return actionNameAttribute.Name;
        return name;
    }
}

Using this helper class we may again generate actions links from C# expressions in ASP.Net Core.


Wednesday, May 3, 2023

Custom logger for .Net Core for writing logs to Azure BLOB storage

If you create .Net Core app you may use standard console logger (from Microsoft.Extensions.Logging.Console nuget package). For development it works quite well but if app goes to production you probably want to store logs in some persistent storage in order to be able to check them when needed. In this post I will show how to create custom logger which will write logs to Azure BLOB storage.

At first need to mention that there is BlobLoggerProvider from MS (from Microsoft.Extensions.Logging.AzureAppServices nuget package) which creates BatchingLogger which in turn stores logs to BLOB storage as well. However it has dependency on Azure App Services infrastructure so on practice you may use it only if your code is running inside Azure App Service. If this limitation is important we may go with custom logger implementation.

First of all we need to implement logger itself which inherits Microsoft.Extensions.Logging.ILogger interface and writes logs to the BLOB:

public class BlobLogger : ILogger
{
	private const string CONTAINER_NAME = "custom-logs";
	private string connStr;
	private string categoryName;

	public BlobLogger(string categoryName, string connStr)
	{
		this.connStr = connStr;
		this.categoryName = categoryName;
	}

	public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
		Func<TState, Exception?, string> formatter)
	{
		if (!IsEnabled(logLevel))
		{
			return;
		}

		using (var ms = new MemoryStream(Encoding.UTF8.GetBytes($"[{this.categoryName}: {logLevel,-12}] {formatter(state, exception)}{Environment.NewLine}")))
		{
			var container = this.ensureContainer();
			var now = DateTime.UtcNow;
			var blob = container.GetAppendBlobClient($"{now:yyyyMMdd}/log.txt");
			blob.CreateIfNotExists();
			blob.AppendBlock(ms);
		}
	}

	private BlobContainerClient ensureContainer()
	{
		var container = new BlobContainerClient(this.connStr, CONTAINER_NAME);
		container.CreateIfNotExists();
		return container;
	}

	public bool IsEnabled(LogLevel logLevel) => true;

	public IDisposable BeginScope<TState>(TState state) => default!;
}

BlobLogger creates new container "custom-logs" (if it doesn't exist yet) in specified BLOB storage. Inside this container it also creates folders using current date as folder name yyyyMMdd (own folder for each day) and writes messages to the log.txt file inside this folder. Note that for working with Azure BLOB storage we used BlobContainerClient class from Azure.Storage.Blobs nuget package since. It will allow us to use instance of our logger as singleton (see below) because instance methods of this client class are guaranteed to be thread safe:

Thread safety
We guarantee that all client instance methods are thread-safe and independent of each other (guideline). This ensures that the recommendation of reusing client instances is always safe, even across threads.

In order to create BlobLogger we need to pass connection string to Azure storage and logging category name. It will be done in logger provider which will be responsible for creating BlobLogger:

public class BlobLoggerProvider : ILoggerProvider
{
	private string connStr;

	public BlobLoggerProvider(string connStr)
	{
		this.connStr = connStr;
	}

	public ILogger CreateLogger(string categoryName)
	{
		return new BlobLogger(categoryName, this.connStr);
	}

	public void Dispose()
	{
	}
}

Now everything is ready for using our logger in .Net Core app. If we want to use our BLOB logger together with console logger (which is quite convenient since logging is done both into console and into BLOB storage) we may use LoggingFactory and pass both standard ConsoleLoggerProvider and our BlobLoggerProvider:

var configuration = builder.GetContext().Configuration;
var azureStorageConnectionString = configuration.GetValue<string>("AzureWebJobsStorage");
var logger = new LoggerFactory(new ILoggerProvider[]
{
	new ConsoleLoggerProvider(new OptionsMonitor<ConsoleLoggerOptions>(new ConsoleLoggerOptions())),
	new BlobLoggerProvider(azureStorageConnectionString)
}).CreateLogger("CustomLogger");
builder.Services.AddSingleton(logger);

For creating instance of ConsoleLoggerProvider I used OptionsMonitor<T> helper class from How to create a ConsoleLoggerProvider without the full hosting framework (don't know why MS didn't provide this option OTB and made it so complex there):

public class OptionsMonitor<T> : IOptionsMonitor<T>
{
	private readonly T options;

	public OptionsMonitor(T options)
	{
		this.options = options;
	}

	public T CurrentValue => options;

	public T Get(string name) => options;

	public IDisposable OnChange(Action<T, string> listener) => new NullDisposable();

	private class NullDisposable : IDisposable
	{
		public void Dispose() { }
	}
}

After that you will have logs both in console and in Azure BLOB storage.

Update 2023-06-22: wrote another post how to add support of logging scopes which work in task-based asynchronous pattern (TAP) and multithread code: Custom logger for .Net Core for writing logs to Azure BLOB storage.

Monday, April 17, 2023

Add basic authentication to ASP.Net Core Web API

Basic authentication is probably simplest authentication type for Web API when HTTP requests are authenticated using username and passwords provided in HTTP request headers. In this post I will describe how to add basic authentication to ASP.Net Core Web API.

At first we need to add reference to idunno.Authentication.Basic nuget package. It contains infrastructure for basic authentication ready to be used in ASP.Net Core/.NET Core projects. Also we will need simple validation service which will check provided username/password and based on that will authenticate/reject requests:

public interface IBasicAuthValidationService
{
    bool AreCredentialsValid(string username, string password);
}

public class BasicAuthValidationService : IBasicAuthValidationService
{
    private string username;
    private string password;

    public BasicAuthValidationService(string username, string password)
    {
        this.username = username;
        this.password = password;
    }

    public bool AreCredentialsValid(string username, string password)
    {
        return string.Compare(this.username, username, true) == 0 && this.password == password;
    }
}

In our example BasicAuthValidationService simply compares credentials coming from HTTP request with predefined allowed username/password.

Then in Web API's Program.cs we need to configure basic authentication itself. Since username and password are sent in HTTP request headers it is important to force using HTTPS for securing communication with our API. We will do that by adding RequireHttpsAttribute filter and UseHttpsRedirection middleware:

builder.Services.AddScoped<IBasicAuthValidationService>(c =>
{
    // get allowed username and password to authenticate requests
    string username = ...;
    string password = ...;
    return new BasicAuthValidationService(uername, password);
});

// add basic auth
builder.Services.AddAuthentication(BasicAuthenticationDefaults.AuthenticationScheme)
    .AddBasic(options =>
    {
        options.Realm = "Test.Api";
        options.Events = new BasicAuthenticationEvents
        {
            OnValidateCredentials = context =>
            {
                var validationService = context.HttpContext.RequestServices.GetService<IBasicAuthValidationService>();
                if (validationService.AreCredentialsValid(context.Username, context.Password))
                {
                    var claims = new[]
                    {
                        new Claim(ClaimTypes.NameIdentifier, context.Username, ClaimValueTypes.String, context.Options.ClaimsIssuer),
                        new Claim(ClaimTypes.Name, context.Username, ClaimValueTypes.String, context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();
                }

                return Task.CompletedTask;
            }
        };
    });

builder.Services.Configure<MvcOptions>(options =>
{
    options.Filters.Add(new RequireHttpsAttribute());
});

var app = builder.Build();
... app.UseHttpsRedirection();

Our Web API is now ready to be used with basic authentication. If you use Swagger for generating client for Web API (see Generate strongly-typed C# client for ASP.Net Core Web API with OpenAPI (swagger) support running on localhost) then you may configure your Web API client for basic authentication like this:

services.AddHttpClient<HttpClient>();
services.AddSingleton<IApiClient>(c =>
{
    string baseUrl = ...;
    string username = ...;
    string password = ...;
    var httpClientFactory = c.GetRequiredService<IHttpClientFactory>();
    var httpClient = httpClientFactory.CreateClient();
    httpClient.DefaultRequestHeaders.Authorization = new BasicAuthenticationHeaderValue(username, password);
    return new ApiClient(baseUrl, httpClient);
});

Now both server and client sides support basic authentication.

Wednesday, March 29, 2023

Generate strongly-typed C# client for ASP.Net Core Web API with OpenAPI (swagger) support running on localhost

Some time ago I wrote how to generate C# client for web api with OpenAPI support using online service https://generator3.swagger.io/api/generate. Limitation of this approach is that web api should be available online as well. But what to do if you develop web api and it is available only on localhost? One of the way is to use https://github.com/swagger-api/swagger-codegen but it is not very easy to use it on localhost.

Simpler solution is to use tool called NSwagStudio. At first we need to add support of OpenAPI to our web api - in my previous post I wrote how to do that: Add OpenAPI (Swagger) support to ASP.Net Core Web API. We will use the same code example here.

When we add OpenAPI support to our web api in addition to online documentation (which is available by url http://localhost:{port}/swagger/index.html) we also get endpoint with OpenAPI (swagger) JSON:

http://localhost:{port}/swagger/v1/swagger.json

which looks like this:


Having this endpoint it is very easy to generate strongly-typed C# client for our web api running on localhost using mentioned NSwagStudio. All we have to do is to choose runtime (we will use .NET6), paste url of swagger.json endpoint to the studio, check CSharp Client checkbox and click Generate Outputs:

Here is example of C# client generated for web api from our example with default settings:

//----------------------
// <auto-generated>
//     Generated using the NSwag toolchain v13.18.2.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------

#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended."
#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword."
#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?'
#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ...
#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..."
#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'"
#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant"
#pragma warning disable 8603 // Disable "CS8603 Possible null reference return"

namespace MyNamespace
{
    using System = global::System;

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.18.2.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v13.0.0.0))")]
    public partial class Client 
    {
        private string _baseUrl = "";
        private System.Net.Http.HttpClient _httpClient;
        private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;

        public Client(string baseUrl, System.Net.Http.HttpClient httpClient)
        {
            BaseUrl = baseUrl;
            _httpClient = httpClient;
            _settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings);
        }

        private Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings()
        {
            var settings = new Newtonsoft.Json.JsonSerializerSettings();
            UpdateJsonSerializerSettings(settings);
            return settings;
        }

        public string BaseUrl
        {
            get { return _baseUrl; }
            set { _baseUrl = value; }
        }

        protected Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get { return _settings.Value; } }

        partial void UpdateJsonSerializerSettings(Newtonsoft.Json.JsonSerializerSettings settings);

        partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url);
        partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder);
        partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);

        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public virtual System.Threading.Tasks.Task<System.Collections.Generic.ICollection<User>> ListAsync()
        {
            return ListAsync(System.Threading.CancellationToken.None);
        }

        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public virtual async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<User>> ListAsync(System.Threading.CancellationToken cancellationToken)
        {
            var urlBuilder_ = new System.Text.StringBuilder();
            urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/User/List");

            var client_ = _httpClient;
            var disposeClient_ = false;
            try
            {
                using (var request_ = new System.Net.Http.HttpRequestMessage())
                {
                    request_.Method = new System.Net.Http.HttpMethod("GET");
                    request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));

                    PrepareRequest(client_, request_, urlBuilder_);

                    var url_ = urlBuilder_.ToString();
                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);

                    PrepareRequest(client_, request_, url_);

                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
                    var disposeResponse_ = true;
                    try
                    {
                        var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
                        if (response_.Content != null && response_.Content.Headers != null)
                        {
                            foreach (var item_ in response_.Content.Headers)
                                headers_[item_.Key] = item_.Value;
                        }

                        ProcessResponse(client_, response_);

                        var status_ = (int)response_.StatusCode;
                        if (status_ == 200)
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<System.Collections.Generic.ICollection<User>>(response_, headers_, cancellationToken).ConfigureAwait(false);
                            if (objectResponse_.Object == null)
                            {
                                throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
                            }
                            return objectResponse_.Object;
                        }
                        else
                        if (status_ == 500)
                        {
                            string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
                            throw new ApiException("Server Error", status_, responseText_, headers_, null);
                        }
                        else
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_, cancellationToken).ConfigureAwait(false);
                            if (objectResponse_.Object == null)
                            {
                                throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
                            }
                            throw new ApiException<ProblemDetails>("Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
                        }
                    }
                    finally
                    {
                        if (disposeResponse_)
                            response_.Dispose();
                    }
                }
            }
            finally
            {
                if (disposeClient_)
                    client_.Dispose();
            }
        }

        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public virtual System.Threading.Tasks.Task<User> DetailsAsync(int? id)
        {
            return DetailsAsync(id, System.Threading.CancellationToken.None);
        }

        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public virtual async System.Threading.Tasks.Task<User> DetailsAsync(int? id, System.Threading.CancellationToken cancellationToken)
        {
            var urlBuilder_ = new System.Text.StringBuilder();
            urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/User/Details?");
            if (id != null)
            {
                urlBuilder_.Append(System.Uri.EscapeDataString("id") + "=").Append(System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture))).Append("&");
            }
            urlBuilder_.Length--;

            var client_ = _httpClient;
            var disposeClient_ = false;
            try
            {
                using (var request_ = new System.Net.Http.HttpRequestMessage())
                {
                    request_.Method = new System.Net.Http.HttpMethod("GET");
                    request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));

                    PrepareRequest(client_, request_, urlBuilder_);

                    var url_ = urlBuilder_.ToString();
                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);

                    PrepareRequest(client_, request_, url_);

                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
                    var disposeResponse_ = true;
                    try
                    {
                        var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
                        if (response_.Content != null && response_.Content.Headers != null)
                        {
                            foreach (var item_ in response_.Content.Headers)
                                headers_[item_.Key] = item_.Value;
                        }

                        ProcessResponse(client_, response_);

                        var status_ = (int)response_.StatusCode;
                        if (status_ == 200)
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<User>(response_, headers_, cancellationToken).ConfigureAwait(false);
                            if (objectResponse_.Object == null)
                            {
                                throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
                            }
                            return objectResponse_.Object;
                        }
                        else
                        if (status_ == 404)
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_, cancellationToken).ConfigureAwait(false);
                            if (objectResponse_.Object == null)
                            {
                                throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
                            }
                            throw new ApiException<ProblemDetails>("Not Found", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
                        }
                        else
                        if (status_ == 500)
                        {
                            string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
                            throw new ApiException("Server Error", status_, responseText_, headers_, null);
                        }
                        else
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_, cancellationToken).ConfigureAwait(false);
                            if (objectResponse_.Object == null)
                            {
                                throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
                            }
                            throw new ApiException<ProblemDetails>("Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
                        }
                    }
                    finally
                    {
                        if (disposeResponse_)
                            response_.Dispose();
                    }
                }
            }
            finally
            {
                if (disposeClient_)
                    client_.Dispose();
            }
        }

        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public virtual System.Threading.Tasks.Task SaveAsync(User body)
        {
            return SaveAsync(body, System.Threading.CancellationToken.None);
        }

        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public virtual async System.Threading.Tasks.Task SaveAsync(User body, System.Threading.CancellationToken cancellationToken)
        {
            var urlBuilder_ = new System.Text.StringBuilder();
            urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/User/Save");

            var client_ = _httpClient;
            var disposeClient_ = false;
            try
            {
                using (var request_ = new System.Net.Http.HttpRequestMessage())
                {
                    var json_ = Newtonsoft.Json.JsonConvert.SerializeObject(body, _settings.Value);
                    var content_ = new System.Net.Http.StringContent(json_);
                    content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");
                    request_.Content = content_;
                    request_.Method = new System.Net.Http.HttpMethod("POST");

                    PrepareRequest(client_, request_, urlBuilder_);

                    var url_ = urlBuilder_.ToString();
                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);

                    PrepareRequest(client_, request_, url_);

                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
                    var disposeResponse_ = true;
                    try
                    {
                        var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
                        if (response_.Content != null && response_.Content.Headers != null)
                        {
                            foreach (var item_ in response_.Content.Headers)
                                headers_[item_.Key] = item_.Value;
                        }

                        ProcessResponse(client_, response_);

                        var status_ = (int)response_.StatusCode;
                        if (status_ == 200)
                        {
                            return;
                        }
                        else
                        if (status_ == 500)
                        {
                            string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
                            throw new ApiException("Server Error", status_, responseText_, headers_, null);
                        }
                        else
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_, cancellationToken).ConfigureAwait(false);
                            if (objectResponse_.Object == null)
                            {
                                throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
                            }
                            throw new ApiException<ProblemDetails>("Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
                        }
                    }
                    finally
                    {
                        if (disposeResponse_)
                            response_.Dispose();
                    }
                }
            }
            finally
            {
                if (disposeClient_)
                    client_.Dispose();
            }
        }

        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public virtual System.Threading.Tasks.Task DeleteAsync(int? id)
        {
            return DeleteAsync(id, System.Threading.CancellationToken.None);
        }

        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public virtual async System.Threading.Tasks.Task DeleteAsync(int? id, System.Threading.CancellationToken cancellationToken)
        {
            var urlBuilder_ = new System.Text.StringBuilder();
            urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/User/Delete?");
            if (id != null)
            {
                urlBuilder_.Append(System.Uri.EscapeDataString("id") + "=").Append(System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture))).Append("&");
            }
            urlBuilder_.Length--;

            var client_ = _httpClient;
            var disposeClient_ = false;
            try
            {
                using (var request_ = new System.Net.Http.HttpRequestMessage())
                {
                    request_.Method = new System.Net.Http.HttpMethod("DELETE");

                    PrepareRequest(client_, request_, urlBuilder_);

                    var url_ = urlBuilder_.ToString();
                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);

                    PrepareRequest(client_, request_, url_);

                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
                    var disposeResponse_ = true;
                    try
                    {
                        var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
                        if (response_.Content != null && response_.Content.Headers != null)
                        {
                            foreach (var item_ in response_.Content.Headers)
                                headers_[item_.Key] = item_.Value;
                        }

                        ProcessResponse(client_, response_);

                        var status_ = (int)response_.StatusCode;
                        if (status_ == 200)
                        {
                            return;
                        }
                        else
                        if (status_ == 404)
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_, cancellationToken).ConfigureAwait(false);
                            if (objectResponse_.Object == null)
                            {
                                throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
                            }
                            throw new ApiException<ProblemDetails>("Not Found", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
                        }
                        else
                        if (status_ == 500)
                        {
                            string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
                            throw new ApiException("Server Error", status_, responseText_, headers_, null);
                        }
                        else
                        {
                            var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_, cancellationToken).ConfigureAwait(false);
                            if (objectResponse_.Object == null)
                            {
                                throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null);
                            }
                            throw new ApiException<ProblemDetails>("Error", status_, objectResponse_.Text, headers_, objectResponse_.Object, null);
                        }
                    }
                    finally
                    {
                        if (disposeResponse_)
                            response_.Dispose();
                    }
                }
            }
            finally
            {
                if (disposeClient_)
                    client_.Dispose();
            }
        }

        protected struct ObjectResponseResult<T>
        {
            public ObjectResponseResult(T responseObject, string responseText)
            {
                this.Object = responseObject;
                this.Text = responseText;
            }

            public T Object { get; }

            public string Text { get; }
        }

        public bool ReadResponseAsString { get; set; }

        protected virtual async System.Threading.Tasks.Task<ObjectResponseResult<T>> ReadObjectResponseAsync<T>(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Threading.CancellationToken cancellationToken)
        {
            if (response == null || response.Content == null)
            {
                return new ObjectResponseResult<T>(default(T), string.Empty);
            }

            if (ReadResponseAsString)
            {
                var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                try
                {
                    var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(responseText, JsonSerializerSettings);
                    return new ObjectResponseResult<T>(typedBody, responseText);
                }
                catch (Newtonsoft.Json.JsonException exception)
                {
                    var message = "Could not deserialize the response body string as " + typeof(T).FullName + ".";
                    throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception);
                }
            }
            else
            {
                try
                {
                    using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
                    using (var streamReader = new System.IO.StreamReader(responseStream))
                    using (var jsonTextReader = new Newtonsoft.Json.JsonTextReader(streamReader))
                    {
                        var serializer = Newtonsoft.Json.JsonSerializer.Create(JsonSerializerSettings);
                        var typedBody = serializer.Deserialize<T>(jsonTextReader);
                        return new ObjectResponseResult<T>(typedBody, string.Empty);
                    }
                }
                catch (Newtonsoft.Json.JsonException exception)
                {
                    var message = "Could not deserialize the response body stream as " + typeof(T).FullName + ".";
                    throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception);
                }
            }
        }

        private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo)
        {
            if (value == null)
            {
                return "";
            }

            if (value is System.Enum)
            {
                var name = System.Enum.GetName(value.GetType(), value);
                if (name != null)
                {
                    var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name);
                    if (field != null)
                    {
                        var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute)) 
                            as System.Runtime.Serialization.EnumMemberAttribute;
                        if (attribute != null)
                        {
                            return attribute.Value != null ? attribute.Value : name;
                        }
                    }

                    var converted = System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo));
                    return converted == null ? string.Empty : converted;
                }
            }
            else if (value is bool) 
            {
                return System.Convert.ToString((bool)value, cultureInfo).ToLowerInvariant();
            }
            else if (value is byte[])
            {
                return System.Convert.ToBase64String((byte[]) value);
            }
            else if (value.GetType().IsArray)
            {
                var array = System.Linq.Enumerable.OfType<object>((System.Array) value);
                return string.Join(",", System.Linq.Enumerable.Select(array, o => ConvertToString(o, cultureInfo)));
            }

            var result = System.Convert.ToString(value, cultureInfo);
            return result == null ? "" : result;
        }
    }

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.18.2.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v13.0.0.0))")]
    public partial class ProblemDetails
    {
        [Newtonsoft.Json.JsonProperty("type", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Type { get; set; }

        [Newtonsoft.Json.JsonProperty("title", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Title { get; set; }

        [Newtonsoft.Json.JsonProperty("status", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public int? Status { get; set; }

        [Newtonsoft.Json.JsonProperty("detail", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Detail { get; set; }

        [Newtonsoft.Json.JsonProperty("instance", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Instance { get; set; }

        private System.Collections.Generic.IDictionary<string, object> _additionalProperties;

        [Newtonsoft.Json.JsonExtensionData]
        public System.Collections.Generic.IDictionary<string, object> AdditionalProperties
        {
            get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary<string, object>()); }
            set { _additionalProperties = value; }
        }

    }

    [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.18.2.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v13.0.0.0))")]
    public partial class User
    {
        [Newtonsoft.Json.JsonProperty("id", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public int Id { get; set; }

        [Newtonsoft.Json.JsonProperty("firstName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string FirstName { get; set; }

        [Newtonsoft.Json.JsonProperty("lastName", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string LastName { get; set; }

        [Newtonsoft.Json.JsonProperty("email", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string Email { get; set; }

    }



    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.18.2.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v13.0.0.0))")]
    public partial class ApiException : System.Exception
    {
        public int StatusCode { get; private set; }

        public string Response { get; private set; }

        public System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> Headers { get; private set; }

        public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Exception innerException)
            : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + ((response == null) ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length)), innerException)
        {
            StatusCode = statusCode;
            Response = response;
            Headers = headers;
        }

        public override string ToString()
        {
            return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString());
        }
    }

    [System.CodeDom.Compiler.GeneratedCode("NSwag", "13.18.2.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v13.0.0.0))")]
    public partial class ApiException<TResult> : ApiException
    {
        public TResult Result { get; private set; }

        public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, TResult result, System.Exception innerException)
            : base(message, statusCode, response, headers, innerException)
        {
            Result = result;
        }
    }

}

#pragma warning restore 1591
#pragma warning restore 1573
#pragma warning restore  472
#pragma warning restore  114
#pragma warning restore  108
#pragma warning restore 3016
#pragma warning restore 8603

Having this client makes it much easier to use our web api from client apps (comparing with developing api calls manually). In addition to that NSwagStudio allows to set many different settings which change generated C# code (e.g. include synchronous method in addition to asynchronous which are generated by default, generate interfaces for client classes for simplifying unit testing, default namespace, etc):

You may check other settings when will play with NSwagStudio which for sure worth to spend some time on it. Hope that this knowledge will help to reduce development time and simplify consumption of your web APIs.