European ASP.NET 4.5 Hosting BLOG

BLOG about ASP.NET 4, ASP.NET 4.5 Hosting and Its Technology - Dedicated to European Windows Hosting Customer

European ASP.NET Core Hosting :: AutoWrapper Version 2.1.0 Released

clock November 12, 2019 10:42 by author Peter

AutoWrapper 2.1.0 has been released with newly features added based on community feedback. Here are the newly features added:

  • Added [AutoWrapIgnore] action filter attribute.
  • Added support to override non-success message when using IActionResult return types.
  • Added EnableResponseLogging and EnableExceptionLogging options to turn off auto logging.
  • Added UnAuthorize and BadRequest message for HandleNotSucessAsync() method response.

Using IActionResult Return Types
AutoWrapper now supports IActionResult return types that allows you to return non successful requests such as BadRequest, NotFound, UnAuthorize and etc.
For example:
[Route("{id:long}")] 
[HttpGet] 
public async Task<IActionResult> Get(long id)   

    var person = await _personManager.GetByIdAsync(id); 
    if (person != null) 
    { 
        return Ok(person); 
    } 
    else 
        return NotFound($"Record with id: { id } does not exist."); 


Another example such as:
return Unauthorized("Access token is invalid.");   
return BadRequest("SomeField is null.");   


AutoWrapIgnore Attribute
You can now use the [AutoWrapIgnore] filter attribute for enpoints that you don't want to be wrapped.
For example:
[HttpGet] 
[AutoWrapIgnore] 
public async Task<IActionResult> Get()   

    var data = await _personManager.GetAllAsync(); 
    return Ok(data); 


or
[HttpGet] 
[AutoWrapIgnore] 
public async Task<IEnumerable<Person>> Get()   

    return await _personManager.GetAllAsync(); 


Turn-off Default Logging
You can now turn off Logging by setting EnableResponseLogging and EnableExceptionLogging options to false in AutoWrapper options.
For example:
app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions {   
              EnableResponseLogging = false,  
              EnableExceptionLogging = false  
}); 


That's it. Feel free to request an issue on github if you find bugs or request a new feature. Your valuable feedback is much appreciated to better improve this project. If you find this useful, please give it a star to show your support for this project.



European ASP.NET Core Hosting :: All About Sessions In ASP.NET Core

clock November 5, 2019 10:53 by author Peter

HTTP is a stateless protocol, so we need some mechanism to maintain our App State. Server Side Session has been a way to maintain our state on the server side. In this article we'll see what differences ASP.NET Core has introduced regarding SESSION.

We'll quickly discuss how we used to use Sessions before ASP.NET Core and then we'll see how to access Sessions in ASP.NET Core.

Session In Pre-ASP.NET Core era
You get Session functionality by default (without adding any package)
Previously, you would have accessed Session by -

    Session variable in your Controllers/Forms
    System.Web.HttpContext.Current.Session in places where you don't have direct access to the Session variable.

Anything you store in session is stored as Object. You store values in Key/Value format.

    Session["mydata"] = 10;  

Or to access on those places where Session is not available (e.g. Non-Controller classes)

    System.Web.HttpContext.Current.Session["mydata"] = 10;  

Quite difficult to mock Session Object for Unit Testing

Session in ASP.NET Core 2.2
Now, Session is not available by default.
You need to add the following package. Meta package by default provides you this.

    <PackageReference Include="Microsoft.AspNetCore.Session" Version="2.2.0" /> 
In Startup.ConfigureServices, you need to add the following to register services with DI Container.
    services.AddDistributedMemoryCache();//To Store session in Memory, This is default implementation of IDistributedCache   
    services.AddSession(); 

In Startup.Configure, you need to add the following (before UseMVC) to add Session Middleware.

    app.UseCookiePolicy();     
    app.UseSession();     
    app.UseMvc(routes =>   

Make sure the following is also there (It is added by default when you use ASP.NET Core MVC Template).

    app.UseCookiePolicy();  

ASP.NET Core 2.2 onwards adds Cookie Consent (true) in the Startup file. When an application runs, the user needs to accept Cookie Consent on screen. When the user accepts the policy on the page, it creates a consent cookie. It is to follow GDPR and to give control to the user if the user wants to store cookies from a site or not. If the user doesn't accept that, Session does not work because Session requires a cookie to send/receive session Id. You may face this issue while working with ASP.NET Core MVC default template.

How to access Session in Controller?

You will notice that you don't have "Session" variable available now. Controller now has a property "HttpContext" which has "Session" variable. So, you can access session in controller by using the following code.

var a = this.HttpContext.Session.GetString("login");   
HttpContext.Session.SetString("login", dto.Login); 


How to access Session in Non-Controller class?

Now, you don't have System.Web.HttpContext.Current.Session in ASP.NET Core. To access session in non-controller class -

First, register the following service in Startup.ConfigureServices;

    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); 

Now, register a class (example - TestManager) where you want to access the Session in Startup.ConfigureServices;
    services.AddScoped<TestManager>(); 
Note
You may use AddTransient or AddSingleton according to your logic.

Now, in TestManager class, add the following code.
    private readonly IHttpContextAccessor _httpContextAccessor;   
    private readonly ISession _session;   
    public TestManager(IHttpContextAccessor httpContextAccessor)   
       {   
            _httpContextAccessor = httpContextAccessor;   
            _session = _httpContextAccessor.HttpContext.Session;   
        } 


The above code is receiving IHttpContextAccessor object through dependency injection and then, it is storing Sessions in a local variable.

How to access Session in View file?

Add the following at the top of your View file.
@using Microsoft.AspNetCore.Http   
@inject IHttpContextAccessor httpContextAccessor  


Then, you may access the Session variable in your View like following.
@httpContextAccessor.HttpContext.Session.GetString("login")  

What else is changed regarding Session?

Session is non-locking now.
A session is not created until you have at least one value in it
You need to use functions to get & set data. Array syntax is not supported now.
Now, ISession only provides Get & Set method which takes & returns data in Byte Array format.
If you want to store data in the String format, you may add the following in your file and use extension methods.
    using Microsoft.AspNetCore.Http;  
It exposes the new extension methods.
    SetInt32   
    GetInt32   
    SetString   
    GetString   
Under the hood, these covert the data into bytes.

You may also write your own extension methods. For example, the following Extension Methods help you store & retrieve any complex type.
    public static class SessionExtensions       
        {       
            public static void Set<T>(this ISession session, string key, T value)       
            {       
                session.Set<(key, JsonConvert.SerializeObject(value));       
            }       
           
            public static T GetObject<T>(this ISession session, string key)       
            {       
                var value = session.GetString(key);       
                return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value);       
            }       
        }  


In the next article, we'll learn about SESSION Wrapper Design Pattern.

Summary

Session concepts are similar to what we've seen in earlier .NET Frameworks. The real difference is that now, it is cleaner & more flexible to use.

 



European ASP.NET Core 3.1 Hosting - HostForLIFE.eu :: .NET Core Implementing .NET Core Health Checks

clock October 29, 2019 12:16 by author Peter

Generally, when we are using any uptime monitoring systems or load balancers, these systems will keep monitoring the health of the application and based on its health condition it will decide to send the request to serve it. For this earlier, we use to create a special endpoint where it will return any error message or code to indicate the health of the API/service.
 

Following is the sample, an endpoint /health where it verifying the database connection and returns the result accordingly.
    [Route("health")] 
    public ActionResult Health() 
    { 
        using (var connection = new SqlConnection(_connectionString)) 
        { 
            try 
            { 
                connection.Open(); 
            } 
            catch (SqlException) 
            { 
                return new HttpStatusCodeResult(503, "Database connection is unhealthy"); 
            } 
        } 
     
        return new EmptyResult(); 
    } 

When we ran the application with endpoint /health, it will display an empty message with 200 status code and 503 status code when there is any connectivity issue while connecting to the database.

Now, based on these resulted status codes, monitoring systems can take appropriate actions like removing this particular services instance from its list of healthy services so that no requests will be redirected to it until it becomes healthy (in our case, when database connectivity issue resolves).
 
We need to keep adding more external resource health checks accordingly.
 
Since .NET Core 2.2, we no need to add a special controller for health check endpoint, instead, the framework itself providing Health Check services as follows.
 
NuGet Package
You have to install following NuGet package
 
Install-Package Microsoft.Extensions.Diagnostics.HealthChecks
 
Once the package is installed, we need to add the following lines at ConfigureServices() and Configure() methods in Startup.cs file.
    public void ConfigureServices(IServiceCollection services) 
    { 
        services.AddHealthChecks(); 
    } 
      
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 
    { 
        app.UseHealthChecks("/Health"); 
    } 


As observed, we are providing the endpoint name in Configure() method. These lines of code will enable a dynamic endpoint "/Health" and display either Healthy/UnHealthy results based on its health state.
 
But, where can we write our custom logic to replicate the above? Yes, we have many features to customize our needs from logic to display results.
 
Adding Custom Logic
 
We can perform in two ways the following are those.
 
Option 1
 
In ConfigureServices method,
    public void ConfigureServices(IServiceCollection services) { 
     services.AddHealthChecks() 
      .AddCheck("sql", () => { 
     
       using(var connection = new SqlConnection(_connectionString)) { 
        try { 
         connection.Open(); 
        } catch (SqlException) { 
         return HealthCheckResult.Unhealthy(); 
        } 
       } 
     
       return HealthCheckResult.Healthy(); 
     
      }); 
    } 


Here, we can use an anonymous method to write the custom logic using AddCheck() method. This will expect a HealthCheckResult object as a result. This object will contain 3 options,
    Healthy
    Unhealthy
    Degraded

Based on the result we need to return appropriately so that runtime will return the status code accordingly. For example, in the above code, if database connection passes it will return a 200 status code (Healthy) and 503 status code (Unhealthy) if failed.
 
Option 2 - In a separate class
The class should implement an IHealthCheck interface and implement CheckHealthAsync() method as follows,
    public class DatabaseHealthCheck: IHealthCheck { 
     public Task < HealthCheckResult > CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = 
      default) { 
      using(var connection = new SqlConnection(_connectionString)) { 
       try { 
        connection.Open(); 
       } catch (SqlException) { 
        return HealthCheckResult.Healthy(); 
       } 
      } 
     
      return HealthCheckResult.Healthy(); 
     
     } 
    } 


Once we created the class, we need to mention this class in ConfigureServices() method using AddTask<T> method as follows by giving some valid unique names.
    public void ConfigureServices(IServiceCollection services) 
    { 
         services.AddControllers(); 
     
         services.AddHealthChecks() 
              .AddCheck<DatabaseHealthCheck>("sql"); 
    } 


Now, our code is clean and we can add any number of Health Tasks as above and it will be run in the order how we declared here.
 
Custom Status Code

As discussed above, by default it will send 200 status code if we return Healthy and 503 for Unhealthy. The Healthcheck service even provides scope for us to change this default behavior by providing custom status code using its options object as follows.
    var options = new HealthCheckOptions(); 
    options.ResultStatusCodes[HealthStatus.Unhealthy] = 420; 
    app.UseHealthChecks("/Health", options); 


In this example, I replace the status code for the Unhealthy state with 420.
 
Custom Response
The beauty of this tool is, we can even customize our output for more clear detailed information about each Health check task. This will be very useful in case we have multiple health check tasks to analyze which task made the complete service heath status to Unhealthy.
 
We can achieve this via HealthCheckOptions ResponseWriter property.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { 
     app.UseHealthChecks("/Health", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions() { 
      ResponseWriter = CustomResponseWriter 
     }); 
    } 
     
    private static Task CustomResponseWriter(HttpContext context, HealthReport healthReport) { 
     context.Response.ContentType = "application/json"; 
     
     var result = JsonConvert.SerializeObject(new { 
      status = healthReport.Status.ToString(), 
       errors = healthReport.Entries.Select(e => new { 
        key = e.Key, value = e.Value.Status.ToString() 
       }) 
     }); 
     return context.Response.WriteAsync(result); 
     
    } 


Now, the above code will display the result in JSON format will all the tasks information. Here, Key represents the Task name we have given (in our case "sql") and value is either Healthy/Unhealthy.
 
Health Check UI
We can even see the health check results on-screen visually by installing the following NuGet package
Install-Package AspNetCore.HealthChecks.UI
 
Once installed need to call respective service methods in ConfigureServices() and Configure() methods accordingly.
    public void ConfigureServices(IServiceCollection services) 
    { 
        services.AddHealthChecksUI(); 
    } 
     
    public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
    { 
        app.UseHealthChecksUI(); 
    } 

Once configured, you can run the application and point to /healthchecks-ui endpoint which display a UI as follows,



European ASP.NET Core 3 Hosting :: Simple Steps to Migrate Your ASP.NET Core 2 to ASP.NET Core 3

clock October 25, 2019 07:18 by author Scott

.NET Core always interesting. In the past few months, we have just launched ASP.NET Core 2.2 on our hosting environment, and now Microsoft has support latest ASP.NET Core 3. This post will be taking the Contacts project used in the ASP.NET Basics series and migrating it from .NET Core 2.2 to .NET Core 3.0.

Installation

If you are a Visual Studio user you can get .NET Core 3.0 by installing at least Visual Studio 16.3. For those not using Visual Studio, you can download and install .NET Core 3.0 SDK from here. As with previous versions, the SDK is available for Windows, Linux, and Mac.

After installation is complete you can runt the following command from a command prompt to see all the versions of the .NET Core SDK you have installed.

dotnet --list-sdks

You should see 3.0.100 listed. If you are like me you might also see a few preview versions of the SDK that can be uninstalled at this point.

Project File Changes

Right-click on the project and select Edit projectName.csproj.

Change the TargetFramework to netcoreapp3.0.

Before:
<TargetFramework>netcoreapp2.2</TargetFramework>

After:
<TargetFramework>netcoreapp3.0</TargetFramework>

The packages section has a lot of changes. Microsoft.AspNetCore.App is now gone and part of .NET Core without needing a specific reference. The other thing to note is that Entity Framework Core is no longer “in the box” so you will see a lot of references add to make Entity Framework Core usable.

Before:
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.0" PrivateAssets="All" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="4.0.1" />

After:
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.0.0" PrivateAssets="All" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />

The last thing to note is that Swashbuckle doesn’t have a final version ready for .NET Core 3 so you will have to make sure you are using version 5 rc2 at a minimum.

The following is my full project file for reference.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
    <UserSecretsId>aspnet-Contacts-cd2c7b27-e79c-43c7-b3ef-1ecb04374b70</UserSecretsId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.0.0" PrivateAssets="All" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />
  </ItemGroup>
</Project>

Program Changes

In Program.cs some changes to the way the host is constructed. The over version may or may not have worked, but I created a new app and pulled this out of it just to make sure I’m using the current set up.

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace Contacts
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                                          {
                                              webBuilder.UseStartup<Startup>();
                                          });
    }
}

Startup Changes

In Startup.cs we have quite a few changes to make. As long as you haven’t do any customization in the constructor you can replace it with the following.

public Startup(IConfiguration configuration)
{
        Configuration = configuration;
}

Next, they type on the configuration property changed from IConfigurationRoot to IConfiguration.

Before:
public IConfigurationRoot Configuration { get; }

After:
public IConfiguration Configuration { get; }

Moving on to the ConfigureServices function has a couple of changes to make. The first is a result of updating to the newer version of the Swagger package where the Info class has been replaced with OpenApiInfo.

Before:
services.AddSwaggerGen(c =>
{
        c.SwaggerDoc("v1", new Info { Title = "Contacts API", Version = "v1"});
});

After:
services.AddSwaggerGen(c =>
{
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "Contacts API", Version = "v1" })
});

Next, we are going to move from using UserMvc to the new AddControllersWithViews which is one of the new more targeted ways to add just the bits of the framework you need.

Before:
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

After:
services.AddControllersWithViews();

Now in the Configure function, the function signature needs to be updated and the logging factory bits removed. If you do need to configure logging that should be handled as part of the HostBuilder.

Before:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
      loggerFactory.AddDebug();

After:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{

For the next set of changes, I’m just going to show the result and not the before. The UseCors may or may not apply but the addition of UserRouting and the replacement of UseMvc with UserEndpoint will if you want to use the new endpoint routing features.

app.UseStaticFiles();
app.UseRouting();
app.UseCors(builder =>
            {
                builder.AllowAnyHeader();
                builder.AllowAnyMethod();
                builder.AllowAnyOrigin();
            }
           );
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
                 {
                     endpoints.MapControllerRoute(
                                                  name: "default",
                                                  pattern: "{controller=Home}/{action=Index}/{id?}");
                     endpoints.MapRazorPages();
                 });

Other Miscellaneous Changes

The only other change I had was the removal of @using Microsoft.AspNetCore.Http.Authentication in a few cshtml files related to login.

 



European ASP.NET Core 3 Hosting - HostForLIFE.eu :: ASP.NET Core Denial of Service Vulnerability

clock October 22, 2019 12:09 by author Peter

In this blog, we are going to discuss vulnerable versions of .Net Core. Microsoft releases the information about security breaches in ASP.Net Core. It informs developers which version they need to update to remove this vulnerability. Microsoft is aware of DOS Attack in the OData library. If you are using OData library in your application in the sense attacker can exploit.

We have two types of dependencies in .net core,

  • Direct dependencies
  • transitive dependencies

Direct dependencies are dependencies where you specifically add a package to your project, transitive dependencies occur when you add a package to your project that in turn relies on another package.

Mitigation policy
Open your application through visual studio and go to package manager console and run the below command.
command :-  dotnet --info 

By running the above command you will come to know which package we need to update as per Microsoft security guidelines.
 
Direct dependencies
By editing your Cs.proj file we can fix the issue or we can update Nuget Package manager.
 
Transitive dependencies
Transitive dependencies occur when any vulnerable package is referring or relies on another package. By examining the project.asset.json file you can fix the issue.

In this blog, we have discussed vulnerable versions of .Net Core. As per Microsoft security advice it is better to update packages which are in your application.



European ASP.NET Core 3 Hosting :: How to Enable GRPC Compression in ASP.NET Core 3

clock October 22, 2019 09:41 by author Scott

How Do I Enable Response Compression with GRPC?

There are two main approaches that I’ve found so far to enable the compression of gRPC responses. You can either configure this at the server level so that all gRPC services apply compression to responses, or at a per-service level.

Server Level Options

services.AddGrpc(o =>
{
    o.ResponseCompressionLevel = CompressionLevel.Optimal;
    o.ResponseCompressionAlgorithm = "gzip";
});

When registering the gRPC services into the dependency injection container with the AddGrpc method inside ConfigureServices, it’s possible to set properties on the GrpcServiceOptions. At this level, the options affect all gRPC services which the server implements.

Using the overload of the AddGrpc extension method, we can supply an Action<GrpcServiceOptions>. In the above snippet we’ve set the compression algorithm to “gzip”. We can also optionally control the CompressionLevel which trades the time needed to compress the data against the final size that is achieved by compression. If not specified the current implementation defaults to using CompressionLevel.Fastest. In the preceding snippet, we’ve chosen to allow more time for the compression to reduce the bytes to the smallest possible size.

Service Level Options

services.AddGrpc()
    .AddServiceOptions<WeatherService>(o =>
        {
            o.ResponseCompressionLevel = CompressionLevel.Optimal;
            o.ResponseCompressionAlgorithm = "gzip";
        });

After calling AddGrpc, an IGrpcServerBuilder is returned. We can call an extension method on that builder called AddServiceOptions to provide per service options. This method is generic and accepts the type of the gRPC service that the options should apply.

In the preceding example, we have decided to provide options specifically for calls that are handled by the WeatherService implementation. The same options are available at this level as we discussed for the server level configuration. In this scenario, if we mapped other gRPC services within this server, they would not receive the compression options.

Making Requests from A GRPC Client

Now that response compression is enabled, we need to ensure our requests state that our client accepts compressed content. In fact, this is enabled by default when using a GrpcChannel created using the ForAddress method so we have nothing to do in our client code.

var channel = GrpcChannel.ForAddress("https://localhost:5005");

Channels created in this way already send a “grpc-accept-encoding” header which includes the gzip compression type. The server reads this header and determines that the client allows gzipped responses to be returned.

One way to visualise the effect of compression is to enable trace level logging for our application while in development. We can achieve this by modifying the appsettings.Development.json file as follows:

{
  "Logging": {
    "LogLevel": {
        "Default": "Debug",
        "System": "Information",
        "Grpc": "Trace",
        "Microsoft": "Trace"
    }
  }
}

When running our server, we now get much more verbose console logging.

info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /WeatherForecast.WeatherForecasts/GetWeather'
dbug: Grpc.AspNetCore.Server.ServerCallHandler[1]
      Reading message.
dbug: Microsoft.AspNetCore.Server.Kestrel[25]
      Connection id "0HLQB6EMBPUIA", Request id "0HLQB6EMBPUIA:00000001": started reading request body.
dbug: Microsoft.AspNetCore.Server.Kestrel[26]
      Connection id "0HLQB6EMBPUIA", Request id "0HLQB6EMBPUIA:00000001": done reading request body.
trce: Grpc.AspNetCore.Server.ServerCallHandler[3]
      Deserializing 0 byte message to 'Google.Protobuf.WellKnownTypes.Empty'.
trce: Grpc.AspNetCore.Server.ServerCallHandler[4]
      Received message.
dbug: Grpc.AspNetCore.Server.ServerCallHandler[6]
      Sending message.
trce: Grpc.AspNetCore.Server.ServerCallHandler[9]
      Serialized 'WeatherForecast.WeatherReply' to 2851 byte message.
trce: Microsoft.AspNetCore.Server.Kestrel[37]
      Connection id "0HLQB6EMBPUIA" sending HEADERS frame for stream ID 1 with length 104 and flags END_HEADERS
trce: Grpc.AspNetCore.Server.ServerCallHandler[10]
      Compressing message with 'gzip' encoding.
trce: Grpc.AspNetCore.Server.ServerCallHandler[7]
      Message sent.
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /WeatherForecast.WeatherForecasts/GetWeather'
trce: Microsoft.AspNetCore.Server.Kestrel[37]
      Connection id "0HLQB6EMBPUIA" sending DATA frame for stream ID 1 with length 978 and flags NONE
trce: Microsoft.AspNetCore.Server.Kestrel[37]
      Connection id "0HLQB6EMBPUIA" sending HEADERS frame for stream ID 1 with length 15 and flags END_STREAM, END_HEADERS
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 2158.9035ms 200 application/grpc

On line 16 of this log, we can see that the WeatherReply, essentially an array of 100 WeatherData items in this sample, has been serialised to protocol buffers and has a size of 2851 bytes.

Later, in line 20, we can see that the message has been compressed with gzip encoding and on line 26, we can see the size of the data frame for this call which is 978 bytes. The data, in this case, has compressed quite well (66% reduction) because the repeated WeatherData items contain text and many of the values repeat within the message.

In this example, gzip compression has a good effect on the over the wire size of the data.

Disable Response Compression within A Service Method Implementation

It’s possible to control the response compression on a per-method basis. At this time, I’ve only found a way to do this on an opt-out approach. When compression is enabled for a service or server, we can opt-out of compression within the service method implementation.

Let’s look at the server log output when calling a service method which streams WeatherData messages from the server.

info: WeatherForecast.Grpc.Server.Services.WeatherService[0]
      Sending WeatherData response
dbug: Grpc.AspNetCore.Server.ServerCallHandler[6]
      Sending message.
trce: Grpc.AspNetCore.Server.ServerCallHandler[9]
      Serialized 'WeatherForecast.WeatherData' to 30 byte message.
trce: Grpc.AspNetCore.Server.ServerCallHandler[10]
      Compressing message with 'gzip' encoding.
trce: Microsoft.AspNetCore.Server.Kestrel[37]
      Connection id "0HLQBMRRH10JQ" sending DATA frame for stream ID 1 with length 50 and flags NONE
trce: Grpc.AspNetCore.Server.ServerCallHandler[7]
      Message sent.

On line 6, we can see that an individual WeatherData message is 30 bytes in size. On line 8, this gets compressed, and on line 10, we can see that the data length is now 50 bytes, larger than the original message. In this case, there is no gain from gzip compression, and we see an increase in the overall message size sent over the wire.

We can avoid compression for a particular message by setting the WriteOptions for the call within the service method.

public override async Task GetWeatherStream(Empty _, IServerStreamWriter<WeatherData> responseStream, ServerCallContext context)
{
    context.WriteOptions = new WriteOptions(WriteFlags.NoCompress);

    // implementation of the method which writes to the stream
}

At the top of our service method, we can set the WriteOptions on the ServerCallContext. We pass in a new WriteOptions instance which the WriteFlags value set to NoCompress. These write options are used for the next write.

With streaming responses, it’s also possible to set this value on the IServerStreamWriter.

public override async Task GetWeatherStream(Empty _, IServerStreamWriter<WeatherData> responseStream, ServerCallContext context)
{   
    responseStream.WriteOptions = new WriteOptions(WriteFlags.NoCompress);

    // implementation of the method which writes to the stream
}

When this option is applied, the logs now show that compression for calls to this service method is not applied.

info: WeatherForecast.Grpc.Server.Services.WeatherService[0]
      Sending WeatherData response
dbug: Grpc.AspNetCore.Server.ServerCallHandler[6]
      Sending message.
trce: Grpc.AspNetCore.Server.ServerCallHandler[9]
      Serialized 'WeatherForecast.WeatherData' to 30 byte message.
trce: Microsoft.AspNetCore.Server.Kestrel[37]
      Connection id "0HLQBMTL1HLM8" sending DATA frame for stream ID 1 with length 35 and flags NONE
trce: Grpc.AspNetCore.Server.ServerCallHandler[7]
      Message sent.

Now the 30 byte message has a length of 35 bytes in the DATA frame. There is a small overhead which accounts for the extra 5 bytes which we don’t need to concern ourselves with here.

Disable Response Compression from A GRPC Client

By default, the gRPC channel includes options that control which encodings it accepts. It is possible to configure these when creating the channel if you wish to disable compression of responses from your client. Generally, I would avoid this and let the server decide what to do, since it knows best what can and cannot be compressed. That said, you may sometimes need to control this from the client.

The only way I’ve found to do this in my exploration of the API to date is to configure the channel by passing in a GrpcChannelOptions instance. One of the properties on this options class is for the CompressionProviders, an IList<ICompressionProvider>. By default, when this is null, the client implementation adds the Gzip compression provider for you automatically. This means that the server can choose to gzip the response message(s) as we have already seen.

private static async Task Main()
{
    using var channel = GrpcChannel.ForAddress("https://localhost:5005", new GrpcChannelOptions { CompressionProviders = new List<ICompressionProvider>() });

    var client = new WeatherForecastsClient(channel);

    var reply = await client.GetWeatherAsync(new Empty());

    foreach (var forecast in reply.WeatherData)
    {
        Console.WriteLine($"{forecast.DateTimeStamp.ToDateTime():s} | {forecast.Summary} | {forecast.TemperatureC} C");
    }

    Console.WriteLine("Press a key to exit");
    Console.ReadKey();
}

In this sample client code, we establish the GrpcChannel and pass in a new instance of GrpcChannelOptions. We set the CompressionProviders property to an empty list. Since we now specify no providers in our channel, when the calls are created and sent via this channel, they won’t include any compression encodings in the “grpc-accept-encoding” header. The server acknowledges this and not apply gzip compression to the response.

Summary

In this post, we’ve explored the possibility of compressing response messages from a gRPC server. We’ve identified that in some cases, but crucially not all, this may result in smaller payloads. We’ve seen that by default clients calls include the gzip “grpc-accept-encoding” value in the headers. If the server is configured to apply compression, it only does so if a supported encoding type is matched from the request header.

We can configure the GrpcChannelOptions when creating a channel for the client, to disable the inclusion of the gzip compression encoding. On the server, we can configure the whole server, or a specific service to enable compression for responses. We can override and disable that on a per service-method level as well.



About HostForLIFE

HostForLIFE is European Windows Hosting Provider which focuses on Windows Platform only. We deliver on-demand hosting solutions including Shared hosting, Reseller Hosting, Cloud Hosting, Dedicated Servers, and IT as a Service for companies of all sizes.

We have offered the latest Windows 2019 Hosting, ASP.NET 5 Hosting, ASP.NET MVC 6 Hosting and SQL 2019 Hosting.


Month List

Tag cloud

Sign in