.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.
.NET Remoting Order Service Overview
- 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 fromServiceBase
and provides the fundamental service infrastructure to initialize and manage the service. Within the OnStart method, a .NET Remoting server type known asOrderService
is exposed. ThisOrderService
class, derived fromMarshalByRefObject
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;
}
}
}
- Contract and implementation. There is an
IOrderService
contract and implementation usingMarshalByRefObject
:
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.
}
- 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);
}
- .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:
- 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.
- 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.
- 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.
- Configuration Management Enhancement: Substitute the use of
ConfigurationManager
with the modern IConfiguration interface available in .NET Core for reading application configuration data. - Dependency Injection Implementation: Use the dependency injection pattern in .NET Core to inject the dependencies for better code organization and maintainability.
- Database Connectivity Upgrade: Replace references to
System.Data.SqlClient
withMicrosoft.Data.SqlClient
to align with modern database connectivity practices. - Removal of MarshalByRefObject: Eliminate the dependency on
MarshalByRefObject
from theOrderService
. - 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:
- 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);
}
}
- 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).
- 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)
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
andip
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.
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.
Cleanup
To save costs, delete the resources you created as part of the blog post:
Delete Application Load Balancer (ALB)
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.