.NET on AWS Blog

Modernize an existing Windows service to a .NET 8 gRPC service with Amazon ECS

Introduction

gRPC is a high-performance, cross-platform Remote Procedure Call (RPC) framework that allows for efficient communication between microservices. It was developed by Google and is now open-source. gRPC efficiently connects services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. For more information on gRPC, read the gRPC documentation.

In this post, we will demonstrate how a legacy Windows service running on the .NET Framework may be upgraded to an ASP.NET Core gRPC service. We will use a sample order Windows service that allows connections via .NET Remoting to show how it can be modernized to become an ASP.NET Core gRPC service. The rationale behind modernizing a .NET remoting service stems from its lack of support in .NET Core, necessitating an adaptation to technologies such as gRPC services or Web APIs.

We will guide you through the step-by-step process of modernizing a .NET remoting-based Windows service into a robust gRPC service. We’ll also provide steps on how to run the gRPC service on Amazon Elastic Container Service (Amazon ECS), accessible through an Application Load Balancer (ALB).

gRPC use cases

gRPC has several use cases for .NET-based applications, offering various benefits over traditional communication protocols. Here are some common use cases of gRPC in .NET applications:

  • High-Performance APIs: gRPC is a service known for high performance and efficiency. It uses protocol buffers (protobuf) as its default data format, which is a language-agnostic binary serialization format. It uses HTTP/2 for communication, which results in compact message payloads and efficient data transfer. This makes gRPC ideal for building high-performance APIs where low latency and high throughput are critical, such as real-time streaming, IoT applications, or financial systems.
  • Cross-Platform Communication: gRPC supports multiple programming languages and platforms, making it an excellent choice for building cross-platform applications. You can have gRPC clients and servers implemented in different languages (such as .NET, Java, Go, etc.) and they can communicate seamlessly using the same protobuf defined service contracts.
  • Service-to-Service Communication: gRPC is commonly used for service-to-service communication within distributed systems. It enables efficient and reliable communication between different services, allowing them to exchange data and collaborate seamlessly. With its support for bidirectional streaming and advanced features like flow control and error handling, gRPC facilitates robust service-to-service communication.
  • Mobile and IoT Applications: gRPC is well-suited for mobile and IoT applications where bandwidth and resources are limited. Its compact binary format (protobuf) reduces the payload size, resulting in reduced network usage and improved performance. Additionally, gRPC supports client streaming and server streaming, enabling efficient communication patterns for mobile and IoT devices.
  • Cloud-Native Applications: gRPC integrates well with cloud-native application architectures. It aligns with containerization technologies (such as Docker and Kubernetes) and can be easily deployed in cloud environments. With support for load balancing, service discovery, and automatic code generation, gRPC simplifies the development and deployment of cloud-native applications.

Modernization use case overview

We have a sample order service which is a Windows service application built on the .NET Framework using .NET Remoting. This application manages CRUD operations for order management This service relies on MS SQL Server as its backend data repository. Subsequently, we will deep dive into the steps of transforming this legacy Windows application into a modernized gRPC service.

To modernize a .NET Remoting based Windows service to .NET 8, there are several options which encompass the adoption of a gRPC Service, facilitating client connectivity to gRPC APIs, or choosing a pathway towards modernizing it to ASP.NET Core Web APIs.

Architecture overview

Upon the completion of the modernization process, the legacy application leverages a modernized stack, seamlessly integrating Amazon ECS for containerization and ALB for load balancing. Figure 1 shows the modernized version of the application.

Figure 1: Architecture of modernized application

Figure 1: Architecture of modernized application

.NET Remoting Order Service Overview

  1. Windows Service Implementation. The following code listing is a Windows service implementation that serves as the entry point for an order processing service. The MyOrderService class inherits from ServiceBase and provides the fundamental service infrastructure to initialize and manage the service. Within the OnStart method, a .NET Remoting server type known as OrderService is exposed. This OrderService class, derived from MarshalByRefObject is designed to be accessible to clients, allowing them to invoke the service functionality.
public partial class MyOrderService : ServiceBase
{
	public ServiceHost serviceHost;
	public MyOrderService()
	{
		// code removed for brevity		
	}

	protected override void OnStart(string[] args)
	{
		try
		{                
			// Create remoting channel
			TcpChannel channel = new TcpChannel(8000);
			ChannelServices.RegisterChannel(channel, ensureSecurity: true);

			// Expose remote object
			RemotingConfiguration.RegisterWellKnownServiceType(
			typeof(OrderService),
			"OrderService",
			WellKnownObjectMode.Singleton);

			// Start remoting service host
			serviceHost = new ServiceHost(typeof(OrderService));
			serviceHost.Open();
		}
		catch (Exception ex)
		{
			eventLog.WriteEntry(ex.Message);
			throw;
		}
	}
}
  1. Contract and implementation. There is an IOrderService contract and implementation using MarshalByRefObject:
public interface IOrderService
{
  OrderResponse CreateOrderAsync(OrderRequest order);
  // Remaining code removed for brevity.
}
public class OrderService: MarshalByRefObject, IOrderService
{
        OrderRepository _orderRepository;
        public OrderService()
        {
            _orderRepository = new OrderRepository();
        }

        public OrderResponse CreateOrderAsync(OrderRequest order)
        {
            return _orderRepository.InsertOrder(order).Result;
        }

        // Remaining code removed for brevity.
}
  1. Windows service startup. The following code is how the Windows service is signalled to start from the main entry point.
static void Main()
{
    ServiceBase[] ServicesToRun;
    ServicesToRun = new ServiceBase[]
    {
        new MyOrderService()
    };
    ServiceBase.Run(ServicesToRun);
}
  1. .NET Remoting client. This code snippet creates the .NET Remoting client to invoke server object to make a call to the order service.
TcpChannel channel = new TcpChannel();
ChannelServices.RegisterChannel(channel, ensureSecurity: true);
OrderService server = (OrderService)Activator.GetObject(
                  typeof(OrderService),                              $"tcp://localhost:8000/{nameof(OrderService)}");

OrderResponse orderResponse = server.CreateOrderAsync(
               new OrderRequest
               {
                   OrderStage = OrderStage.Placed,
                   Customer = new CustomerDto()
                   {
                       Address = "Test Address",
                       Email = "test-email@test.com",
                       Name = "test-name",
                       Phone = "123-456-7890"
                   },
                   Products = new List<ProductDto> 
                   {
                       new ProductDto() {
                            Price = 10.00M,
                            Category = "Clothing",
                            Quantity = 1,
                            Title = "T-Shirt -XL"
                    }
               }
            });

Modernization steps

Here are the steps to modernize the sample order service from .NET Remoting to a gRPC service:

  1. Creation of ASP.NET Core gRPC Service Project: Create a new ASP.NET Core gRPC Service project using dotnet cli command dotnet new grpc -o DemoGrpcServer.
  2. Migration of Dependent Classes: Move all required classes in order service to the new project. If the dependent classes are available in a separate library, you can refer to the same project or NuGet package.
  3. NuGet Package Management: Include and update compatible NuGet packages to ensure seamless compatibility with the new framework. You may need to upgrade NuGet packages to .NET Standard/.NET 8 or later version.
  4. Configuration Management Enhancement: Substitute the use of ConfigurationManager with the modern IConfiguration interface available in .NET Core for reading application configuration data.
  5. Dependency Injection Implementation: Use the dependency injection pattern in .NET Core to inject the dependencies for better code organization and maintainability.
  6. Database Connectivity Upgrade: Replace references to System.Data.SqlClient with Microsoft.Data.SqlClient to align with modern database connectivity practices.
  7. Removal of MarshalByRefObject: Eliminate the dependency on MarshalByRefObject from the OrderService.
  8. Protocol Buffers Integration: Create a proto definition file into the project and include it using an item group. For this use case, we have created an order.proto file, which contains definitions for the gRPC service and message types, facilitating client-server communication. For more details on how to define message types in a proto file, please read the protocol buffers documentation.

Note that the GrpcServices contain only the Server keyword. This adds the .proto file to the C# application. Refer to gRPC services with C# for the available options.

<Project Sdk="Microsoft.NET.Sdk.Web">
  // code removed for brevity.  
  <ItemGroup>
    <Protobuf Include="Protos\order.proto" GrpcServices="Server" />
  </ItemGroup>
 // code removed for brevity.  
</Project>

Following is the snippet order.proto definition file created for order gRPC service:

syntax = "proto3";

import "google/protobuf/wrappers.proto";

option csharp_namespace = "DemoGrpcServer";

package order;

service ProcessOrder {
    rpc CreateOrder (CreateUpdateOrderRequest) returns (CreateOrderResponse);
    rpc UpdateOrder (CreateUpdateOrderRequest) returns (UpdateOrderResponse);
    rpc GetOrder (GetOrderRequest) returns (GetOrderResponse);
    rpc DeleteOrder (DeleteOrderRequest) returns (DeleteOrderResponse);
}

message CreateUpdateOrderRequest {    
    Customer customer = 1;
    repeated Product products = 2;
    OrderStatus order_status = 3;
}
message GetOrderRequest {    
    int32 order_id = 1;
}
message DeleteOrderRequest {    
    int32 order_id = 1;
}
message Customer {
    string name = 1;
    string email = 2;
    string phone = 3;
    string address = 4;
}
message Product {
    string title = 1;
    int32 quantity = 2;
    string category = 3;
    double price = 4;
}
enum OrderStatus {
  ORDER_STATUS_PLACED = 0;
  ORDER_STATUS_UNSHIPPED = 1;  
  ORDER_STATUS_Shipped = 2;
  ORDER_STATUS_Complete = 3;
}
message CreateOrderResponse {
    int32 order_id = 1;
    google.protobuf.StringValue error = 2;
}
message GetOrderResponse {    
    int32 order_id = 1;
    OrderStatus order_status = 2; 
}
message UpdateOrderResponse {    
    bool is_order_updated = 1;
}
message DeleteOrderResponse {    
    bool is_order_deleted = 1;
}
  1. Service Implementation: Develop a concrete service implementation of the gRPC service defined in the proto file named ProcessOrder.

We named our class as ProcessOrderService which is the concrete implementation of the ProcessOrder service in the proto file, this extends the base type ProcessOrder.ProcessOrderBase.

C# types are created from the order.proto file with the help of C# tooling support for protobuf files. The ProcessOrderService effectively utilizes AutoMapper for mapping gRPC message types to legacy request and response formats while invoking the necessary actions within the order service.

We recommend you build the project after creating the proto file to generate the C# types by the C# tooling support for proto files.

Following is the gRPC service code to create an order:

public class ProcessOrderService : ProcessOrder.ProcessOrderBase
{
	private readonly ILogger<ProcessOrderService> _logger;
	private readonly IMapper _mapper;
	private readonly IOrderService _orderService;
	public ProcessOrderService(ILogger<ProcessOrderService> logger, IMapper mapper, IOrderService orderService)
	{
		_logger = logger;
		_mapper = mapper;
		_orderService = orderService;
	}
	public override Task<CreateOrderResponse> CreateOrder(CreateUpdateOrderRequest request, ServerCallContext context)
	{            
		OrderRequest orderRequest = _mapper.Map<OrderRequest>(request);
		OrderResponse orderResponse = _orderService.CreateOrderAsync(orderRequest);
		CreateOrderResponse placeOrderResponse = _mapper.Map<CreateOrderResponse>(orderResponse);
		return Task.FromResult(placeOrderResponse); 
	}	
}
  1. Deployment Preparation: With the legacy application successfully modernized into a gRPC service, it is ready for deployment within an Amazon ECS container. Add a docker file to the project, followed by building the project and pushing the Docker image to the Amazon Elastic Container Registry (ECR).
  2. gRPC Health Check: The gRPC health checking protocol serves as a standardized method for evaluating the operational health of gRPC server applications. These health checks are frequently used along with external monitoring services to assess an application’s health.ASP.NET Core includes native support for gRPC health checks, facilitated through the Grpc.AspNetCore.HealthChecks package. For more details on configuring gRPC health checks, please refer to gRPC health checks in ASP.NET Core. Once a health check is configured, the health check path is generally available as either /grpc.health.v1.Health/Check or /grpc.health.v1.Health/. When a request is sent to the health check endpoint, gRPC responds with a status code, indicating the application’s health status. You can find the list of gRPC status code at GRPC Core status codes.

gRPC Client Overview

You will need to change the .NET Remoting client application to make it compatible with the modernized gRPC service. For simplicity, you can create a new .NET application.

To call the order gRPC service, we create a .NET console gRPC client. Add the following listed NuGet packages. Copy .proto files from gRPC service, optionally you can provide a different value for option csharp_namespace in your .proto file to represent the client.

In our use case, we have copied order.proto from the gRPC service and updated option csharp_namespace to DemoGrpcClient.

Finally, edit the console or client application project file and add an item group with a <Protobuf> element which points to order.proto file. Refer to the following code snippet for how to add an item group with a <Protobuf> element in a project file.

<Project Sdk="Microsoft.NET.Sdk.Web">
  // code removed for brevity.  
  <ItemGroup>
    <Protobuf Include="Protos\order.proto" GrpcServices="Client" />
  </ItemGroup>
 // code removed for brevity.  
</Project>

These are the required NuGet packages for the client:

These are optional NuGet packages for the client:

This code snippet creates a gRPC client for the ProcessOrderService.

// code removed for brevity.  
Console.WriteLine($"Creating Order...");
string endpoint = "https://localhost:53906";
using var channel = GrpcChannel.ForAddress(endpoint);
var client = new ProcessOrder.ProcessOrderClient(channel);
var reply = await client.CreateOrderAsync(createOrderRequest);
Console.WriteLine($"Created Order: {reply.OrderId}");

Optionally, if you want to call health check service, you can create a health check client as shown  in the next listing. Please reference the required Grpc.HealthCheck NuGet package which contains a client for gRPC health checks.

Console.WriteLine($"Health check..");
var healthChannel = GrpcChannel.ForAddress("https://<replace with ALB DNS name>");
var healthCheckClient = new Health.HealthClient(healthChannel);
var response = await healthCheckClient.CheckAsync(new HealthCheckRequest());
var status = response.Status;
Console.WriteLine($"Health Status:{response.Status}");

Note that in these code snippets, we have hard-coded the ALB endpoint for this sample code. Ideally, the value should come from configuration.

Deployment to Amazon Elastic Container Service (ECS)

Deploying a gRPC service on an Amazon ECS container which is accessible by an Application Load Balancer is a process similar to deploying any REST API. This involves steps such as:

Creating a Docker image

Pushing the docker image to Amazon Elastic Container Registry (ECR)

Creating a task definition

Setting up an ECS cluster

Configuring an ECS service

Configuring load balancer along with target groups.

The key differentiator lies in the load balancer configuration, which is detailed in the following section.

Load Balancer Configuration

Before setting up a gRPC service, it’s crucial to think about a few important points, as some of the ALB configuration differs for a gRPC service.

  • Supported listener protocol is limited to HTTPS.
  • Supported action type for listener rules is forward only.
  • Supported target types must adhere to instance and ip only.
  • It is essential to define a health check method following the format /package.service/method.
  • Be sure to specify the gRPC status codes to use when validating a successful response from a target.

The following screenshots show the configuration settings for the load balancer, target group, and target group health check.

Figure 2: load balancer listener configuration

Figure 2: load balancer listener configuration

Figure 3: target group configuration

Figure 3: target group configuration

Figure 4: Target group health check configuration

Figure 4: Target group health check configuration

Once ALB configuration is complete and health check is passed, you are ready to test the gRPC service. It can be tested using gRPC client code or gRPC CLI.

Figure 5: Call to CreateOrder service by using gRPC client code in C#

Figure 5: Call to CreateOrder service by using gRPC client code in C#

Figure 5: Call to CreateOrder service by using gRPCurl CLI

Figure 6: Call to CreateOrder service by using gRPCurl CLI

Cleanup

To save costs, delete the resources you created as part of the blog post:

Delete Application Load Balancer (ALB)

Delete ECS Service

Delete ECS Cluster

Delete ECS Task Definition

Delete ECR Repository

Conclusion

In this post, we modernized a Windows service using .NET Remoting, transitioning it into a gRPC service, and subsequently showcasing the configuration required to run it on Amazon Elastic Container Service (ECS) through an Application Load Balancer (ALB).

While the provided guide covers a fundamental overview of how to undertake the modernization of a straightforward .NET Remoting service, complex Windows services may require a substantial investment of time and effort for successful modernization.