GRPC For WCF Developers
GRPC For WCF Developers
PUBLISHED BY
All rights reserved. No part of the contents of this book may be reproduced or transmitted in any
form or by any means without the written permission of the publisher.
This book is provided “as-is” and expresses the author’s views and opinions. The views, opinions and
information expressed in this book, including URL and other Internet website references, may change
without notice.
Some examples depicted herein are provided for illustration only and are fictitious. No real association
or connection is intended or should be inferred.
Microsoft and the trademarks listed at https://www.microsoft.com on the “Trademarks” webpage are
trademarks of the Microsoft group of companies.
The Docker whale logo is a registered trademark of Docker, Inc. Used by permission.
All other marks and logos are property of their respective owners.
Authors:
Editor:
Feel free to forward this guide to your team to help ensure a common understanding of these
considerations and opportunities. Having everybody working from a common set of terms and
underlying principles helps ensure consistent application of architectural patterns and practices.
References
• gRPC website https://grpc.io
• Choosing between .NET 5 and .NET Framework for server apps
https://docs.microsoft.com/dotnet/standard/choosing-core-framework-server
Contents
Introduction ............................................................................................................................. 1
History .......................................................................................................................................................................................... 1
Microservices ............................................................................................................................................................................. 2
Protocol buffers....................................................................................................................... 9
How Protobuf works ............................................................................................................................................................... 9
Protobuf messages.................................................................................................................................................................. 9
Types ..................................................................................................................................................................................... 11
Decimals............................................................................................................................................................................... 14
i Contents
Any ......................................................................................................................................................................................... 17
Oneof .................................................................................................................................................................................... 18
NetTCP .................................................................................................................................................................................. 24
HTTP ...................................................................................................................................................................................... 25
MSMQ ................................................................................................................................................................................... 25
WebHttpBinding ............................................................................................................................................................... 25
Request/reply..................................................................................................................................................................... 26
Metadata .................................................................................................................................................................................. 31
Error handling......................................................................................................................................................................... 32
WS-* protocols....................................................................................................................................................................... 33
WS-ReliableMessaging .................................................................................................................................................. 34
ii Contents
Create a new ASP.NET Core gRPC project .................................................................................................................. 35
Summary .............................................................................................................................................................................. 65
WS-Federation .................................................................................................................................................................. 67
iii Contents
Provide channel credentials in the client application ........................................................................................ 71
Kubernetes............................................................................................................................................................................... 83
Service meshes....................................................................................................................................................................... 91
iv Contents
CHAPTER 1
Introduction
Helping machines communicate with each other has been one of the primary preoccupations of the
digital age. In particular, there’s an ongoing effort to determine the optimal remote communication
mechanism that will suit the interoperability demands of the current infrastructure. As you can
imagine, that mechanism changes as either the demands or the infrastructure evolves.
The release of .NET Core 3.0 marks a shift in the way that Microsoft delivers remote communication
solutions to developers who want to deliver services across a range of platforms. .NET Core and later
doesn’t offer Windows Communication Foundation (WCF) out of the box but, with the release of
ASP.NET Core 3.0, it does provide built-in gRPC functionality.
gRPC is a popular framework in the wider software community. It’s used by developers across many
programming languages for modern RPC scenarios. The community and the ecosystem are vibrant
and active. Support for the gRPC protocol is being added to infrastructure components like
Kubernetes, service meshes, load balancers, and more. These factors, together with its performance,
efficiency, and cross-platform compatibility, make gRPC a natural choice for new apps and WCF apps
moving to .NET.
History
The fundamental principle of a computer network as nothing more than a group of computers
exchanging data with each other to achieve a set of interrelated tasks hasn’t changed since its
inception. But the complexity, scale, and expectations have grown exponentially.
During the 1990s, the emphasis was mainly on improving internal networks that used the same
language and platforms. TCP/IP became the gold standard for this type of communication.
The focus soon shifted to how best to optimize communication across multiple platforms by
promoting a language-agnostic approach. Service-oriented architecture (SOA) provided a structure
for loosely coupling a broad collection of services that could be provided to an application.
The development of web services occurred when all major platforms could access the internet, but
they still couldn’t interact with each other. Web services have open standards and protocols,
including:
Windows Communication Foundation is a framework for building services. It was designed in the early
2000s to help developers using early SOA to manage the complexities of working with SOAP.
Although it removes the requirement for the developers to write their own SOAP protocols, WCF still
uses SOAP to enable interoperability with other systems. WCF was also designed to deliver solutions
across multiple protocols (HTTP/1.1, Net.TCP, and so on).
Microservices
In microservice architectures, large applications are built as a collection of smaller modular services.
Each component does a specific task or process, and components are designed to work interoperably
but can be isolated as necessary.
It was into this environment that gRPC was launched, 10 years after Microsoft first released WCF.
Evolved directly from Google’s internal infrastructure RPC (Stubby), gRPC was never based on the
same standards and protocols that had informed the parameters of many earlier RPCs. And gRPC was
only ever based on HTTP/2. That’s why it could draw on the new capabilities that advanced transport
protocol provided. In particular, bidirectional streaming, binary messaging, and multiplexing.
Using a set of sample WCF applications, Chapter 5 is a deep-dive look at converting the main types of
WCF service (simple request-reply, one-way, and streaming) to their equivalents in gRPC.
The final section of the book looks at how to get the best from gRPC in practice. This section includes
information on using additional tools, like Docker containers or Kubernetes, to take advantage of the
efficiency of gRPC. It also includes a detailed look at monitoring with logging, metrics, and distributed
tracing.
ASP.NET Core 3.0 is the first release of ASP.NET that natively supports gRPC as a first-class citizen,
with Microsoft teams contributing to the official .NET implementation of gRPC. It’s recommended for
building distributed applications with .NET that can interoperate with all other major programming
languages and frameworks.
Key principles
As discussed in chapter 1, Google wanted to use the introduction of HTTP/2 to replace Stubby, its
internal, general purpose RPC infrastructure. gRPC, based on Stubby, now can take advantage of
standardization and would extend its applicability to mobile computing, the cloud, and the Internet of
Things.
To achieve this standardization, the Cloud Native Computing Foundation (CNCF) established a set of
principles that would govern gRPC. The following list shows the most relevant ones, which are
primarily concerned with maximizing accessibility and usability:
• Free and open – All artifacts should be open source, with licensing that doesn’t constrain
developers from adopting gRPC.
• Coverage and simplicity – gRPC should be available across every popular platform, and simple
enough to build on any platform.
• Interoperability and reach – It should be possible to use gRPC on any network, regardless of
bandwidth or latency, by using widely available network standards.
• General purpose and performant – The framework should be usable by as broad a range of
use-cases as possible, without compromising performance.
• Streaming – The protocol should provide streaming semantics for large datasets or
asynchronous messaging.
• Metadata exchange – The protocol allows non-business data, such as authentication tokens, to
be handled separately from actual business data.
• Standardized status codes – The variability of error codes should be reduced to make error
handling decisions clearer. Where additional, richer error handling is required, a mechanism
should be provided for managing behavior within the metadata exchange.
The following table sets out how the key features of WCF relate to gRPC, and where you can find
more detailed explanations.
One of the advantages of the Protobuf IDL is that as a custom language, it enables gRPC to be
completely language and platform agnostic, not favoring any technology over another.
The Protobuf IDL is also designed for humans to both read and write, whereas WSDL is intended as a
machine-readable/writable format. Changing the WSDL of a WCF service typically requires changing
the service, running the service, and regenerating the WSDL file from the server. By contrast, with a
.proto file, changes are simple to apply with a text editor, and automatically flow through the
generated code. Visual Studio 2022 builds .proto files in the background when they are saved. With
other editors, such as VS Code, the changes are applied when the project is built.
When compared with XML, and particularly SOAP, messages encoded by using Protobuf have many
advantages. Protobuf messages tend to be smaller than the same data serialized as SOAP XML, and
encoding, decoding, and transmitting them over a network can be faster.
The potential disadvantage of Protobuf compared to SOAP is that, because the messages aren’t
readable by humans, additional tooling is required to debug message content.
Tip
gRPC does support server reflection for dynamically accessing services without pre-compiled stubs,
although it’s intended more for general-purpose tools than application-specific clients. For more
information, see GRPC Server Reflection Protocol on GitHub.
Note
WCF’s binary format, used with the NetTCP binding, is much closer to Protobuf in terms of
compactness and performance. But NetTCP is only usable between .NET clients and servers, whereas
Protobuf is a cross-platform solution.
Network protocols
Unlike Windows Communication Foundation (WCF), gRPC uses HTTP/2 as a base for its networking.
This protocol offers significant advantages over WCF and SOAP, which operate only on HTTP/1.1. For
developers wanting to use gRPC, given that there’s no alternative to HTTP/2, it would seem to be the
ideal moment to explore HTTP/2 in more detail and identify additional benefits of using gRPC.
HTTP/2, released by Internet Engineering Task Force in 2015, was derived from the experimental SPDY
protocol, which was already being used by Google. It was specifically designed to be more efficient,
faster, and more secure than HTTP/1.1.
Binary protocol
Request/response cycles no longer need text commands. This activity simplifies and speeds up the
implementation of commands. Specifically, parsing data is faster and uses less memory, network
latency is reduced with obvious related improvements to speed, and there’s an overall better use of
network resources.
Streams
Streams allow you to create long-lived connections between sender and receiver, over which multiple
messages or frames can be sent asynchronously. Multiple streams can operate independently over a
single HTTP/2 connection.
Similarity to WCF
Although the implementation and approach are different for gRPC, the experience of developing and
consuming services with gRPC should be intuitive for WCF developers. The underlying goal is the
same: make it possible to code as though the client and server are on the same platform, without
needing to worry about networking.
Both platforms share the principle of declaring and then implementing an interface, even though the
process for declaring that interface is different. And as you’ll see in chapter 5, the different types of
RPC calls that gRPC supports map well to the bindings available to WCF services.
Performance
Using HTTP/2 rather than HTTP/1.1 removes the requirement for human-readable messages and
instead uses the smaller, faster binary protocol. This is more efficient for computers to parse. HTTP/2
also supports multiplexing requests over a single connection. This support enables responses to be
sent as soon as they’re ready without the need to wait in a queue. (In HTTP/1.1, this issue is known as
“head-of-line (HOL) blocking.”) You need fewer resources when using gRPC, which makes it a good
solution to use for mobile devices and over slower networks.
Interoperability
There are gRPC tools and libraries for all major programming languages and platforms, including
.NET, Java, Python, Go, C++, Node.js, Swift, Dart, Ruby, and PHP. Thanks to the Protocol Buffers binary
wire format and the efficient code generation for each platform, developers can build performant
apps while still enjoying full cross-platform support.
Streaming
gRPC has full bidirectional streaming, which provides similar functionality to WCF’s full-duplex
services. gRPC streaming can operate over regular internet connections, load balancers, and service
meshes.
Security
gRPC is implicitly secure when it’s using HTTP/2 over a TLS end-to-end encrypted connection. Support
for client certificate authentication (see chapter 6) further increases security and trust between client
and server.
This chapter covers how Protobuf works, and how to define your own Protobuf messages.
The Protobuf compiler, protoc, is maintained by Google, although alternative implementations are
available. The generated code is efficient and optimized for fast serialization and deserialization of
data.
The Protobuf wire format is a binary encoding. It uses some clever tricks to minimize the number of
bytes used to represent messages. Knowledge of the binary encoding format isn’t necessary to use
Protobuf. But if you’re interested, you can learn more about it on the Protocol Buffers website.
Protobuf messages
This section covers how to declare Protocol Buffer (Protobuf) messages in .proto files. It explains the
fundamental concepts of field numbers and types, and it looks at the C# code that the protoc
compiler generates.
The rest of the chapter will look in more detail at how different types of data are represented in
Protobuf.
Declaring a message
In Windows Communication Foundation (WCF), a Stock class for a stock market trading application
might be defined like the following example:
[DataContract]
public class Stock
{
[DataMember]
public int Id { get; set; }
[DataMember]
public string Symbol { get; set; }
[DataMember]
public string DisplayName { get; set; }
[DataMember]
public int MarketId { get; set; }
}
To implement the equivalent class in Protobuf, you must declare it in the .proto file. The protoc
compiler will then generate the .NET class as part of the build process.
syntax = "proto3";
message Stock {
int32 id = 1;
string symbol = 2;
string display_name = 3;
int32 market_id = 4;
The first line declares the syntax version being used. Version 3 of the language was released in 2016.
It’s the version that we recommend for gRPC services.
The option csharp_namespace line specifies the namespace to be used for the generated C# types.
This option will be ignored when the .proto file is compiled for other languages. Protobuf files often
contain language-specific options for several languages.
The Stock message definition specifies four fields. Each has a type, a name, and a field number.
Field numbers
Field numbers are an important part of Protobuf. They’re used to identify fields in the binary encoded
data, which means they can’t change from version to version of your service. The advantage is that
backward compatibility and forward compatibility are possible. Clients and services will ignore field
numbers that they don’t know about, as long as the possibility of missing values is handled.
In the binary format, the field number is combined with a type identifier. Field numbers from 1 to 15
can be encoded with their type as a single byte. Numbers from 16 to 2,047 take 2 bytes. You can go
higher if you need more than 2,047 fields on a message for any reason. The single-byte identifiers for
field numbers 1 to 15 offer better performance, so you should use them for the most basic, frequently
used fields.
Note
Protobuf doesn’t natively support a decimal type, so double is used instead. For applications that
require full decimal precision, refer to the section on decimals in the next part of this chapter.
The actual code that’s generated is far more complicated than this. The reason is that each class
contains all the code necessary to serialize and deserialize itself to the binary wire format.
Property names
Note that the Protobuf compiler applied PascalCase to the property names, although they were
snake_case in the .proto file. The Protobuf style guide recommends using snake_case in your
message definitions so that the code generation for other platforms produces the expected case for
their conventions.
float float
int32 int 1
int64 long 1
uint32 uint
uint64 ulong
string string 3
bytes ByteString 4
Notes:
1. The standard encoding for int32 and int64 is inefficient when you’re working with signed
values. If your field is likely to contain negative numbers, use sint32 or sint64 instead. These
types map to the C# int and long types, respectively.
2. The fixed fields always use the same number of bytes no matter what the value is. This behavior
makes serialization and deserialization faster for larger values.
3. Protobuf strings are UTF-8 (or 7-bit ASCII) encoded. The encoded length can’t be greater than
232.
4. The Protobuf runtime provides a ByteString type that maps easily to and from C# byte[]
arrays.
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
message Meeting {
string subject = 1;
google.protobuf.Timestamp time = 2;
google.protobuf.Duration duration = 3;
The generated properties in the C# class aren’t the .NET date and time types. The properties use the
Timestamp and Duration classes in the Google.Protobuf.WellKnownTypes namespace. These classes
provide methods for converting to and from DateTimeOffset, DateTime, and TimeSpan.
Note
The Timestamp type works with UTC times. DateTimeOffset values always have an offset of zero, and
the DateTime.Kind property is always DateTimeKind.Utc.
System.Guid
Protobuf doesn’t directly support the Guid type, known as UUID on other platforms. There’s no well-
known type for it.
The best approach is to handle Guid values as a string field, by using the standard 8-4-4-4-12
hexadecimal format (for example, 45a9fda3-bd01-47a9-8460-c1cd7484b0b3). All languages and
platforms can parse that format.
Don’t use a bytes field for Guid values. Problems with endianness (Wikipedia definition) can result in
erratic behavior when Protobuf is interacting with other platforms, such as Java.
Nullable types
The Protobuf code generation for C# uses the native types, such as int for int32. So the values are
always included and can’t be null.
For values that require explicit null, such as using int? in your C# code, Protobuf’s “Well Known
Types” include wrappers that are compiled to nullable C# types. To use them, import wrappers.proto
into your .proto file, like this:
import "google/protobuf/wrappers.proto"
message Person {
...
google.protobuf.Int32Value age = 5;
Protobuf will use the simple T? (for example, int?) for the generated message property.
The following table shows the complete list of wrapper types with their equivalent C# type:
The well-known types Timestamp and Duration are represented in .NET as classes. In C# 8 and
beyond, you can use nullable reference types. But it’s important to check for null on properties of
those types when you’re converting to DateTimeOffset or TimeSpan.
Decimals
Protobuf doesn’t natively support the .NET decimal type, just double and float. There’s an ongoing
discussion in the Protobuf project about the possibility of adding a standard Decimal type to the well-
known types, with platform support for languages and frameworks that support it. Nothing has been
implemented yet.
It’s possible to create a message definition to represent the decimal type that would work for safe
serialization between .NET clients and servers. But developers on other platforms would have to
understand the format being used and implement their own handling for it.
The nanos field represents values from 0.999_999_999 to -0.999_999_999. For example, the decimal
value 1.5m would be represented as { units = 1, nanos = 500_000_000 }. This is why the nanos
field in this example uses the sfixed32 type, which encodes more efficiently than int32 for larger
values. If the units field is negative, the nanos field should also be negative.
Note
There are multiple other algorithms for encoding decimal values as byte strings, but this message is
easier to understand than any of them. The values are not affected by endianness on different
platforms.
Conversion between this type and the BCL decimal type might be implemented in C# like this:
namespace CustomTypes;
public partial class DecimalValue
{
private const decimal NanoFactor = 1_000_000_000;
public DecimalValue(long units, int nanos)
{
Units = units;
Nanos = nanos;
}
Whenever you use custom message types like this, you must document them with comments in
.proto. Other developers can then implement conversion to and from the equivalent type in their
own language or framework.
message Outer {
message Inner {
string text = 1;
}
Inner inner = 1;
}
In the generated C# code, the Inner type will be declared in a nested static Types class within the
HelloRequest class:
message Person {
// Other fields elided
repeated string aliases = 8;
}
In the generated code, repeated fields are represented by read-only properties of the
Google.Protobuf.Collections.RepeatedField<T> type rather than any of the built-in .NET collection
types. This type implements all the standard .NET collection interfaces, such as IList and IEnumerable.
So you can use LINQ queries or convert it to an array or a list easily.
The RepeatedField<T> type includes the code required to serialize and deserialize the list to the
binary wire format.
syntax "proto3";
message Stock {
reserved 3, 4;
int32 id = 1;
string symbol = 2;
You can also use the reserved keyword as a placeholder for fields that might be added in the future.
You can express contiguous field numbers as a range by using the to keyword.
syntax "proto3";
message Info {
Protocol Buffer (Protobuf) provides two simpler options for dealing with values that might be of more
than one type. The Any type can represent any known Protobuf message type. And you can use the
oneof keyword to specify that only one of a range of fields can be set in any message.
Any
Any is one of Protobuf’s “well-known types”: a collection of useful, reusable message types with
implementations in all supported languages. To use the Any type, you must import the
google/protobuf/any.proto definition.
syntax "proto3"
import "google/protobuf/any.proto"
message Stock {
// Stock-specific data
}
message Currency {
// Currency-specific data
}
message ChangeNotification {
int32 id = 1;
In the C# code, the Any class provides methods for setting the field, extracting the message, and
checking the type.
Protobuf’s internal reflection code uses the Descriptor static field on each generated type to resolve
Any field types. There’s also a TryUnpack<T> method, but that creates an uninitialized instance of T
even when it fails. It’s better to use the Is method as shown earlier.
Oneof
Oneof fields are a language feature: the compiler handles the oneof keyword when it generates the
message class. Using oneof to specify the ChangeNotification message might look like this:
message Stock {
// Stock-specific data
}
message Currency {
// Currency-specific data
}
message ChangeNotification {
int32 id = 1;
oneof instrument {
Stock stock = 2;
Currency currency = 3;
}
}
Fields within the oneof set must have unique field numbers in the overall message declaration.
When you use oneof, the generated C# code includes an enum that specifies which of the fields has
been set. You can test the enum to find which field is set. Fields that aren’t set return null or the
default value, rather than throwing an exception.
Setting any field that’s part of a oneof set will automatically clear any other fields in the set. You can’t
use repeated with oneof. Instead, you can create a nested message with either the repeated field or
the oneof set to work around this limitation.
Protobuf enumerations
Protobuf supports enumeration types. You saw this support in the previous section, where an enum
was used to determine the type of a Oneof field. You can define your own enumeration types, and
Protobuf will compile them to C# enum types.
Because you can use Protobuf with various languages, the naming conventions for enumerations are
different from the C# conventions. However, the code generator converts the names to the traditional
C# case. If the Pascal-case equivalent of the field name starts with the enumeration name, then it’s
removed.
For example, in the following Protobuf enumeration, the fields are prefixed with ACCOUNT_STATUS. This
prefix is equivalent to the Pascal-case enum name, AccountStatus.
enum AccountStatus {
ACCOUNT_STATUS_UNKNOWN = 0;
ACCOUNT_STATUS_PENDING = 1;
ACCOUNT_STATUS_ACTIVE = 2;
ACCOUNT_STATUS_SUSPENDED = 3;
ACCOUNT_STATUS_CLOSED = 4;
}
enum AccountStatus {
option allow_alias = true;
ACCOUNT_STATUS_UNKNOWN = 0;
ACCOUNT_STATUS_PENDING = 1;
ACCOUNT_STATUS_ACTIVE = 2;
ACCOUNT_STATUS_SUSPENDED = 3;
ACCOUNT_STATUS_CLOSED = 4;
ACCOUNT_STATUS_CANCELLED = 4;
}
You can declare enumerations at the top level in a .proto file, or nested within a message definition.
Nested enumerations—like nested messages—will be declared within the .Types static class in the
generated message class.
There’s no way to apply the [Flags] attribute to a Protobuf-generated enum, and Protobuf doesn’t
understand bitwise enum combinations. Look at the following example:
enum Region {
REGION_NONE = 0;
REGION_NORTH_AMERICA = 1;
REGION_SOUTH_AMERICA = 2;
REGION_EMEA = 4;
REGION_APAC = 8;
}
message Product {
Region available_in = 1;
}
The best way to work with multiple enum values in Protobuf is to use a repeated field of the enum
type.
message StockPrices {
map<string, double> prices = 1;
}
Map fields can’t be directly repeated in a message definition. But you can create a nested message
that contains a map and use repeated on the message type, as in the following example:
message Order {
message Attributes {
map<string, string> values = 1;
}
repeated Attributes attributes = 1;
}
Further reading
For more information about Protobuf, see the official Protobuf documentation.
gRPC example
When you create a new ASP.NET Core 6.0 gRPC project from Visual Studio 2022 or the command line,
the gRPC equivalent of “Hello World” is generated for you. It consists of a greeter.proto file that
defines the service and its messages, and a GreeterService.cs file with an implementation of the
service.
syntax = "proto3";
package Greet;
This chapter will refer to this example code when explaining different concepts and features of gRPC.
• You write the application code in a class and decorate methods with the OperationContract
attribute.
• You declare an interface for the service and add OperationContract attributes to the interface.
For example, the WCF equivalent of the greet.proto Greeter service might be written as follows:
[ServiceContract]
public interface IGreeterService
{
[OperationContract]
string SayHello(string name);
}
Chapter 3 showed that Protobuf message definitions are used to generate data classes. Service and
method declarations are used to generate base classes that you inherit from to implement the service.
You just declare the methods to be implemented in the .proto file, and the compiler generates a
base class with virtual methods that you must override.
OperationContract properties
The OperationContract attribute has properties to control or refine how it works. gRPC methods don’t
offer this type of control. The following table lists those OperationContract properties and describes
how the functionality that they specify is (or isn’t) dealt with in gRPC:
The IsInitiating property lets you indicate that a method within ServiceContract can’t be the first
method called as part of a session. The IsTerminating property causes the server to close the session
after an operation is called (or the client, if the property is used on a callback client). In gRPC, streams
are created by single methods and closed explicitly. See gRPC streaming.
What follows is a short discussion about the most relevant WCF bindings and how they compare to
their equivalents in gRPC.
NetTCP
WCF’s NetTCP binding allows for persistent connections, small messages, and two-way messaging.
But it works only between .NET clients and servers. gRPC allows the same functionality but is
supported across multiple programming languages and platforms.
gRPC has many features of WCF’s NetTCP binding, but they’re not always implemented in the same
way. For example, in WCF, encryption is controlled through configuration and handled in the
framework. In gRPC, encryption is achieved at the connection level through HTTP/2 over TLS.
The equivalent in gRPC uses HTTP/2 as the underlying transport layer with the binary Protobuf wire
format for messages. So it can offer performance at the NetTCP service level and full cross-platform
interoperability with all modern programming languages and frameworks.
Named pipes
WCF provided a named pipes binding for communication between processes on the same physical
machine. ASP.NET Core gRPC doesn’t support named pipes. For inter-process communication (IPC)
using gRPC instead supports Unix domain sockets. Unix domain sockets are supported on Linux and
modern versions of Windows.
MSMQ
MSMQ is a proprietary Windows message queue. WCF’s binding to MSMQ enables “fire and forget”
requests from clients that might be processed at any time in the future. gRPC doesn’t natively provide
any message queue functionality.
The best alternative is to directly use a messaging system like Azure Service Bus, RabbitMQ, or Kafka.
You can implement this functionality with the client placing messages directly onto the queue, or a
gRPC client streaming service that enqueues the messages.
WebHttpBinding
WebHttpBinding (also known as WCF REST), with the WebGet and WebInvoke attributes, enabled you
to develop RESTful APIs that could speak JSON at a time when this behavior was less common. If you
have a RESTful API built with WCF REST, consider migrating it to a regular ASP.NET Core MVC Web
API application. This migration would provide the same functionality as a conversion to gRPC.
Types of RPC
As a Windows Communication Foundation (WCF) developer, you’re probably used to dealing with the
following types of remote procedure call (RPC):
• Request/reply
• Duplex:
• One-way duplex with session
• Full duplex with session
• One-way
WCF gRPC
Regular request/reply Unary
Duplex service with session using a client callback interface Server streaming
Full duplex service with session Bidirectional streaming
One-way operations Client streaming
Request/reply
For simple request/reply methods that take and return small amounts of data, use the simplest gRPC
pattern, the unary RPC.
service Things {
rpc Get(GetThingRequest) returns (GetThingResponse);
}
As you can see, implementing a gRPC unary RPC service method is similar to implementing a WCF
operation. The difference is that with gRPC, you override a base class method instead of
implementing an interface. On the server, gRPC base methods always return Task, although the client
provides both async and blocking methods to call the service.
gRPC services provide similar functionality with message streams. Streams don’t map exactly to WCF
duplex services in terms of implementation, but you can achieve the same results.
You can use streams for arbitrary, asynchronous messaging over time. Or you can use them for
passing large datasets that are too big to generate and send in a single request or response.
service ClockStreamer {
rpc Subscribe(ClockSubscribeRequest) returns (stream ClockMessage);
}
This server stream can be consumed from a client application, as shown in the following code:
Note
Server-streaming RPCs are useful for subscription-style services. They’re also useful for sending large
datasets when it would be inefficient or impossible to build the entire dataset in memory. However,
streaming responses isn’t as fast as sending repeated fields in a single message. As a rule, streaming
shouldn’t be used for small datasets.
In WCF, the ServiceContract class with the session is kept alive until the connection is closed. Multiple
methods can be called within the session. In gRPC, the Task that the implementation method returns
shouldn’t finish until the connection is closed.
thing_log.proto
service ThingLog {
rpc OpenConnection(stream Thing) returns (ConnectionClosedResponse);
}
ThingLogService.cs
public class ThingLogService : Protos.ThingLog.ThingLogBase
{
private static readonly ConnectionClosedResponse EmptyResponse = new
ConnectionClosedResponse();
private readonly ILogger<ThingLogService> _logger;
public ThingLogService(ILogger<ThingLogService> logger)
{
_logger = logger;
}
You can use client-streaming RPCs for fire-and-forget messaging, as shown in the previous example.
You can also use them for sending very large datasets to the server. The same warning about
performance applies: for smaller datasets, use repeated fields in regular messages.
chat.proto
service Chatter {
rpc Connect(stream IncomingMessage) returns (stream OutgoingMessage);
}
In the previous example, you can see that the implementation method receives both a request stream
(IAsyncStreamReader<MessageRequest>) and a response stream
(IServerStreamWriter<MessageResponse> ). The method can read and write messages until the
connection is closed.
Chatter client
public class Chat : IAsyncDisposable
{
private readonly Chatter.ChatterClient _client;
private readonly AsyncDuplexStreamingCall<MessageRequest, MessageResponse> _stream;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly Task _readTask;
Metadata
Metadata refers to additional data that might be useful during the processing of requests and
responses but that’s not part of the actual application data. Metadata might include authentication
tokens, request identifiers and tags for monitoring purposes, and information about the data, like the
number of records in a dataset.
It’s possible to add generic key/value headers to Windows Communication Foundation (WCF)
messages by using an OperationContextScope and the OperationContext.OutgoingMessageHeaders
property and handle them by using MessageProperties.
gRPC calls and responses can also include metadata that’s similar to HTTP headers. This metadata is
mostly invisible to gRPC itself and is passed through to be processed by your application code or
middleware. Metadata is represented as key/value pairs, where the key is a string and the value is
either a string or binary data. You don’t need to specify metadata in the .proto file.
Metadata is handled by the Metadata class of the Grpc.Core.Api NuGet package. This class can be
used with collection initializer syntax.
gRPC services can access metadata from the ServerCallContext argument’s RequestHeaders
property:
Error handling
Windows Communication Foundation (WCF) uses FaultException and FaultContract to provide
detailed error information, including supporting the SOAP Fault standard.
Unfortunately, the current version of gRPC lacks the sophistication found with WCF, and only has
limited built-in error handling based on simple status codes and metadata. The following table is a
quick guide to the most commonly used status codes:
try
{
var portfolio = await client.GetPortfolioAsync(new GetPortfolioRequest { Id = id });
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.PermissionDenied)
{
var userEntry = ex.Trailers.FirstOrDefault(e => e.Key == "User");
Console.WriteLine($"User '{userEntry.Value}' does not have permission to view this
portfolio.");
}
catch (RpcException)
{
// Handle any other error type ...
}
Important
When you provide additional metadata for errors, be sure to document the relevant keys and values
in your API documentation, or in comments in your .proto file.
WS-* protocols
One of the real benefits of working with Windows Communication Foundation (WCF) was that it
supported many of the existing WS-* standard protocols. This section will briefly cover how gRPC
manages the same WS-* protocols and discuss what options are available when there’s no alternative.
gRPC works best when servers and clients are generated from the same .proto files, but a Server
Reflection optional extension does provide a way to expose dynamic information from a running
The WS-Discovery protocol is used to locate services on a local network. gRPC services are located
through DNS or a service registry such as Consul or ZooKeeper.
WS-ReliableMessaging
gRPC does not provide an equivalent to WS-ReliableMessaging. Retry semantics should be handled in
code, possibly with a library like Polly. When you’re running in Kubernetes or similar orchestration
environments, service meshes can also help to provide reliable messaging between services.
WS-Transaction, WS-Coordination
WCF’s implementation of distributed transactions uses Microsoft Distributed Transaction Coordinator
(MSDTC). It works with resource managers that specifically support it, like SQL Server, MSMQ, or
Windows file systems. There’s no equivalent yet in the modern microservices world, in part due to the
wider range of technologies in use. For a discussion of transactions, see Appendix A.
The sample WCF application is a minimal stub of a set of stock trading services. It uses the open-
source Inversion of Control (IoC) container library called Autofac for dependency injection. It includes
three services, one for each WCF service type. The services will be discussed in more detail in the
following sections. You can download the solutions from dotnet-architecture/grpc-for-wcf-developers
on GitHub. The services use fake data to minimize external dependencies.
The samples include the WCF and gRPC implementations of each service.
To develop any ASP.NET Core 6.0 app, you need Visual Studio 2022, with the ASP.NET and web
development workload installed.
Select Next to continue to the Configure your new project dialog box. Name the project
TraderSys.Portfolios and add an src subdirectory to the Location.
Create the solution as shown in the following command. The -o (or --output) flag specifies the
output directory, which is created in the current directory if it doesn’t already exist. The solution has
the same name as the directory: TraderSys.sln. You can provide a different name by using the -n (or
--name) flag.
ASP.NET Core 6.0 comes with a CLI template for gRPC services. Create the new project by using this
template, putting it into an src subdirectory as is conventional for ASP.NET Core projects. The project
is named after the directory (TraderSys.Portfolios.csproj), unless you specify a different name
with the -n flag.
Finally, add the project to the solution by using the dotnet sln command:
Because the particular directory only contains a single .csproj file, you can specify just the directory,
to save typing.
You can now open this solution in Visual Studio 2022, Visual Studio Code, or whatever editor you
prefer.
syntax = "proto3";
package PortfolioServer;
service Portfolios {
// RPCs will go here
}
Tip
The template doesn’t add the Protos namespace part by default, but adding it makes it easier to keep
gRPC-generated classes and your own classes clearly separated in your code.
If you rename the greet.proto file in an integrated development environment (IDE) like Visual Studio,
a reference to this file is automatically updated in the .csproj file. But in some other editor, such as
Visual Studio Code, this reference isn’t updated automatically, so you need to edit the project file
manually.
In the gRPC build targets, there’s a Protobuf item element that lets you specify which .proto files
should be compiled, and which form of code generation is required (that is, “Server” or “Client”).
<ItemGroup>
<Protobuf Include="Protos\portfolios.proto" GrpcServices="Server" />
</ItemGroup>
There was a reference to the GreeterService class in the Program.cs. If you used refactoring to
rename the class, this reference should have been updated automatically. However, if you didn’t, you
need to edit it manually.
using TraderSys.Portfolios.Services;
builder.Services.AddGrpc();
app.Run();
[OperationContract]
Task<List<Portfolio>> GetAll(Guid traderId);
}
The Portfolio model is a simple C# class marked with DataContract and including a list of
PortfolioItem objects. These models are defined in the TraderSys.PortfolioData project along
with a repository class that represents a data access abstraction.
[DataContract]
public class Portfolio
{
[DataMember]
public int Id { get; set; }
[DataMember]
public Guid TraderId { get; set; }
[DataMember]
public List<PortfolioItem> Items { get; set; }
}
[DataContract]
public class PortfolioItem
{
[DataMember]
public int Id { get; set; }
[DataMember]
public int ShareId { get; set; }
[DataMember]
public int Holding { get; set; }
[DataMember]
public decimal Cost { get; set; }
}
The ServiceContract implementation uses a repository class provided via dependency injection that
returns instances of the DataContract types:
syntax = "proto3";
package PortfolioServer;
service Portfolios {
// RPCs will go here
}
The first step is to migrate the DataContract classes to their Protobuf equivalents.
Note
Remember to use snake_case for field names in your .proto file. The C# code generator will convert
them to PascalCase for you, and users of other languages will thank you for respecting their different
coding standards.
message PortfolioItem {
int32 id = 1;
int32 share_id = 2;
int32 holding = 3;
int32 cost_cents = 4;
}
The Portfolio class is a little more complicated. In the WCF code, the developer used a Guid for the
TraderId property, and contains a List<PortfolioItem>. In Protobuf, which doesn’t have a first-class
UUID type, you should use a string for the traderId field and parse it in your own code. For the list
of items, use the repeated keyword on the field.
Now that you have the data messages, you can declare the service RPC endpoints.
The service could just return a Portfolio message directly, but again, this could affect backward
compatibility in the future. It’s a good practice to define separate Request and Response messages for
every method in a service, even if many of them are the same right now. So declare a GetResponse
message with a single Portfolio field.
This example shows the declaration of the gRPC service method with the GetRequest message:
message GetRequest {
string trader_id = 1;
int32 portfolio_id = 2;
}
message GetResponse {
Portfolio portfolio = 1;
}
service Portfolios {
rpc Get(GetRequest) returns (GetResponse);
}
The WCF GetAll method takes only a single parameter, traderId, so it might seem that you could
specify string as the parameter type. But gRPC requires a defined message type. This requirement
helps to enforce the practice of using custom messages for all inputs and outputs, for future backward
compatibility.
The WCF method also returns a List<Portfolio>, but for the same reason it doesn’t allow simple
parameter types, gRPC won’t allow repeated Portfolio as a return type. Instead, create a
GetAllResponse type to wrap the list.
Warning
You might be tempted to create a PortfolioList message or something similar and use it across
multiple service methods, but you should resist this temptation. It’s impossible to know how the
various methods on a service will evolve, so keep their messages specific and cleanly separated.
message GetAllResponse {
repeated Portfolio portfolios = 1;
}
service Portfolios {
rpc Get(GetRequest) returns (Portfolio);
rpc GetAll(GetAllRequest) returns (GetAllResponse);
}
If you save your project with these changes, the gRPC build target will run in the background and
generate all the Protobuf message types and a base class that you can inherit to implement the
service.
Open the Services/GreeterService.cs class and delete the example code. Now you can add the
Portfolio service implementation. The generated base class will be in the Protos namespace and is
generated as a nested class. gRPC creates a static class with the same name as the service in the
.proto file and a base class with the suffix Base inside that static class, so the full identifier for the
base type is TraderSys.Portfolios.Protos.Portfolios.PortfoliosBase .
namespace TraderSys.Portfolios.Services;
The base class declares virtual methods for Get and GetAll that can be overridden to implement
the service. The methods are virtual rather than abstract so that if you don’t implement them, the
service can return an explicit gRPC Unimplemented status code, much like you might throw a
NotImplementedException in regular C# code.
The signature for all gRPC unary service methods in ASP.NET Core is consistent. There are two
parameters: the first is the message type declared in the .proto file, and the second is a
ServerCallContext that works similarly to the HttpContext from ASP.NET Core. In fact, there’s an
extension method called GetHttpContext on the ServerCallContext class that you can use to get
the underlying HttpContext, although you shouldn’t need to use it often. We’ll take a look at
ServerCallContext later in this chapter, and also in the chapter that discusses authentication.
The method’s return type is a Task<T>, where T is the response message type. All gRPC service
methods are asynchronous.
After you’ve created the library and added it to the solution, delete the generated Class1.cs file and
copy the files from the WCF solution’s library into the new class library’s folder, keeping the folder
structure:
Models
Portfolio.cs
PortfolioItem.cs
IPortfolioRepository.cs
PortfolioRepository.cs
SDK-style .NET projects automatically include any .cs files in or under their own directory, so you
don’t need to explicitly add them to the project. The only step remaining is to remove the
DataContract and DataMember attributes from the Portfolio and PortfolioItem classes so they’re
plain old C# classes:
using TraderSys.Portfolios.Services;
builder.Services.AddGrpc();
Start by implementing the Get method. The default override looks like this example:
The first problem is that request.TraderId is a string, and the service requires a Guid. Even though
the expected format for the string is UUID, the code has to deal with the possibility that a caller has
sent an invalid value and respond appropriately. The service can respond with errors by throwing an
RpcException and use the standard InvalidArgument status code to express the problem:
After there’s a proper Guid value for traderId, you can use the repository to retrieve the Portfolio and
return it to the client:
namespace TraderSys.Portfolios.Protos;
target.Items.AddRange(source.Items.Select(PortfolioItem.FromRepositoryModel));
return target;
}
}
Note
You could use a library like AutoMapper to handle this conversion from internal model classes to
Protobuf types, as long as you configure the lower-level type conversions like string/Guid or
decimal/double and the list mapping.
Now that you have the conversion code in place, you can complete the Get method implementation:
The implementation of the GetAll method is similar. Note that the repeated fields on Protobuf
messages are generated as readonly properties of type RepeatedField<T>, so you have to add items
to them by using the AddRange method, like in this example:
return response;
}
Having successfully migrated the WCF request-reply service to gRPC, let’s look at creating a client for
it from the .proto file.
Caution
The Grpc.Net.Client NuGet package requires .NET Core 3.0 or later (or another .NET Standard 2.1-
compliant runtime). Earlier versions of .NET Framework and .NET Core are supported by the Grpc.Core
NuGet package.
In Visual Studio 2022, you can add references to gRPC services in a way that’s similar to how you’d
add service references to WCF projects in earlier versions of Visual Studio. Service references and
connected services are all managed under the same UI now. You can access the UI by right-clicking
the Dependencies node in the TraderSys.Portfolios.Client project in Solution Explorer and
selecting Manage Connected Service. In the tool window that appears, select the Connected
48 CHAPTER 5 | Migrate a WCF solution to gRPC
Services section, then select Add a service reference in Service References section, select gRPC and
click Next:
Browse to the portfolios.proto file in the TraderSys.Portfolios project, leave Client under Select
the type of class to be generated, and then select OK:
Notice that this dialog box also provides a URL field. If your organization maintains a web-accessible
directory of .proto files, you can create clients just by setting this URL address.
When you use the Visual Studio Add Connected Service feature, the portfolios.proto file is added
to the class library project as a linked file rather than copied, so changes to the file in the service
project will automatically be applied in the client project. The <Protobuf> element in the csproj file
looks like this:
Tip
If you’re not using Visual Studio or prefer to work from the command line, you can use the dotnet-
grpc global tool to manage Protobuf references in a .NET gRPC project. For more information, see the
dotnet-grpc documentation.
You’ve now migrated a basic WCF application to an ASP.NET Core gRPC service and created a client to
consume the service from a .NET application. The next section will cover the more involved duplex
services.
There are multiple ways to use duplex services in Windows Communication Foundation (WCF). Some
services are initiated by the client and then they stream data from the server. Other full-duplex
services might involve more ongoing two-way communication, like the classic Calculator example in
the WCF documentation. This chapter will take two possible WCF stock ticker implementations and
migrate them to gRPC: one that uses a server streaming RPC and another one that uses a bidirectional
streaming RPC.
The service has a single method with no return type because it uses the callback interface
ISimpleStockTickerCallback to send data to the client in real time.
You can find the implementations of these interfaces in the solution, along with faked external
dependencies to provide test data.
gRPC streaming
The gRPC process for handling real-time data is different from the WCF process. A call from client to
server can create a persistent stream, which can be monitored for messages that arrive
asynchronously. Despite the difference, streams can be a more intuitive way of dealing with this data
and are more relevant in modern programming, which emphasizes LINQ, Reactive Streams, functional
programming, and so on.
The service definition needs two messages: one for the request and one for the stream. The service
returns a stream of the StockTickerUpdate message with the stream keyword in its return
declaration. We recommend that you add a Timestamp to the update to show the exact time of the
price change.
simple_stock_ticker.proto
syntax = "proto3";
import "google/protobuf/timestamp.proto";
package SimpleStockTickerServer;
service SimpleStockTicker {
rpc Subscribe (SubscribeRequest) returns (stream StockTickerUpdate);
}
message SubscribeRequest {
repeated string symbols = 1;
}
Implement SimpleStockTicker
Reuse the fake StockPriceSubscriber from the WCF project by copying the three classes from the
TraderSys.StockMarket class library into a new .NET Standard class library in the target solution. To
better follow best practices, add a Factory type to create instances of it, and register the
IStockPriceSubscriberFactory with the ASP.NET Core dependency injection services.
builder.Services.AddGrpc();
app.Run();
await AwaitCancellation(context.CancellationToken);
}
As you can see, although the declaration in the .proto file says the method returns a stream of
StockTickerUpdate messages, it actually returns a Task. The job of creating the stream is handled by
the generated code and the gRPC runtime libraries, which provide the
IServerStreamWriter<StockTickerUpdate> response stream, ready to use.
Unlike a WCF duplex service, where the instance of the service class is kept alive while the connection
is open, the gRPC service uses the returned task to keep the service alive. The task shouldn’t complete
until the connection is closed.
In the Subscribe method, then, get a StockPriceSubscriber and add an event handler that writes to
the response stream. Then wait for the connection to be closed before immediately disposing the
subscriber to prevent it from trying to write data to the closed stream.
The WriteUpdateAsync method has a try/catch block to handle any errors that might happen when
a message is written to the stream. This consideration is important in persistent connections over
networks, which could be broken at any millisecond, whether intentionally or because of a failure
somewhere.
Example Program.cs
class Program
{
static async Task Main(string[] args)
{
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new SimpleStockTicker.SimpleStockTickerClient(channel);
WaitForExitKey();
tokenSource.Cancel();
await task;
}
}
In this case, the Subscribe method on the generated client isn’t asynchronous. The stream is created
and usable right away because its MoveNext method is asynchronous and the first time it’s called it
won’t complete until the connection is alive.
The stream is passed to an asynchronous DisplayAsync method. The application then waits for the
user to press a key, and then cancels the DisplayAsync method and waits for the task to complete
before exiting.
This code uses the new C# 8 using declaration syntax to dispose of the stream and the channel when
the Main method exits. It’s a small change, but a nice one that reduces indentations and empty lines.
The IAsyncStreamReader<T> type works much like an IEnumerator<T>. There’s a MoveNext method
that returns true as long as there’s more data, and a Current property that returns the latest value.
The only difference is that the MoveNext method returns a Task<bool> instead of just a bool. The
ReadAllAsync extension method wraps the stream in a standard C# 8 IAsyncEnumerable that can be
used with the new await foreach syntax.
Tip
For developers using reactive programming patterns, the section on client libraries at the end of this
chapter shows how to add an extension method and classes to wrap IAsyncStreamReader<T> in an
IObservable<T>.
Again, be sure to catch exceptions here because of the possibility of network failure, and because of
the OperationCanceledException that will inevitably be thrown because the code is using a
CancellationToken to break the loop. The RpcException type has a lot of useful information about
gRPC runtime errors, including the StatusCode. For more information, see Error handling in Chapter 4.
Bidirectional streaming
A WCF full-duplex service allows for asynchronous, real-time messaging in both directions. In the
server streaming example, the client starts a request and then receives a stream of updates. A better
[OperationContract(IsOneWay = true)]
void AddSymbol(string symbol);
[OperationContract(IsOneWay = true)]
void RemoveSymbol(string symbol);
}
Implementing this pattern in gRPC is less straightforward because there are now two streams of data
with messages being passed: one from client to server and another from server to client. It isn’t
possible to use multiple methods to implement the add and remove operations, but you can pass
more than one type of message on a single stream by using either the Any type or the oneof keyword,
which were covered in Chapter 3.
In a case where there’s a specific set of types that are acceptable, oneof is a better way to go. Use an
ActionMessage that can hold either an AddSymbolRequest or a RemoveSymbolRequest:
message ActionMessage {
oneof action {
AddSymbolRequest add = 1;
RemoveSymbolRequest remove = 2;
}
}
message AddSymbolRequest {
string symbol = 1;
}
message RemoveSymbolRequest {
string symbol = 1;
}
service FullStockTicker {
rpc Subscribe (stream ActionMessage) returns (stream StockTickerUpdate);
}
_logger.LogInformation("Subscription started.");
await AwaitCancellation(context.CancellationToken);
_logger.LogInformation("Subscription finished.");
}
The ActionMessage class that gRPC has generated guarantees that only one of the Add and Remove
properties can be set. Finding which one isn’t null is a valid way to determine which type of message
is used, but there’s a better way. The code generation also created an enum ActionOneOfCase in the
ActionMessage class, which looks like this:
Tip
The switch statement has a default case that logs a warning if it encounters an unknown
ActionOneOfCase value. This could be useful to indicate that a client is using a later version of the
.proto file that has added more actions. This is one reason why using a switch is better than testing
for null on known fields.
The client is used in the MainWindowViewModel class, which gets an instance of the
FullStockTicker.FullStockTickerClient type from dependency injection:
The object returned by the client.Subscribe() method is now an instance of the gRPC library type
AsyncDuplexStreamingCall<TRequest, TResponse>, which provides a RequestStream for sending
requests to the server and a ResponseStream for handling responses.
The request stream is used from some WPF ICommand methods to add and remove symbols. For each
operation, set the relevant field on an ActionMessage object:
Important
Setting a oneof field’s value on a message automatically clears any fields that have been set
previously.
The stream of responses is handled in an async method. The Task it returns is held to be disposed
when the window is closed:
try
{
await foreach (var update in stream.ReadAllAsync(token))
{
var price = Prices.FirstOrDefault(p => p.Symbol.Equals(update.Symbol));
if (price == null)
{
price = new PriceViewModel(this) {Symbol = update.Symbol, Price =
update.PriceCents / 100m};
Prices.Add(price);
}
else
{
price.Price = update.PriceCents / 100m;
}
}
}
Client cleanup
When the window is closed and the MainWindowViewModel is disposed (from the Closed event of
MainWindow), we recommend that you properly dispose the AsyncDuplexStreamingCall object. In
particular, the CompleteAsync method on the RequestStream should be called to gracefully close the
stream on the server. This example shows the DisposeAsync method from the sample view-model:
Closing request streams enables the server to dispose of its own resources in a timely way. This
improves the efficiency and scalability of services and prevents exceptions.
You can also use the stream feature for long-running temporal data such as notifications or log
messages. But this chapter will consider its use for returning a single dataset.
Similarly, you should send datasets of unconstrained size over streams to avoid running out of
memory while constructing them.
For datasets where the consumer can separately process each item, you should consider using a
stream if it means that progress can be indicated to the user. Using a stream can improve the
responsiveness of an application, but you should balance it against the overall performance of the
application.
Another scenario where streams can be useful is where a message is being processed across multiple
services. If each service in a chain returns a stream, then the terminal service (that is, the last one in
the chain) can start returning messages. These messages can be processed and passed back along the
chain to the original requestor. The requestor can either return a stream or aggregate the results into
a single response message. This approach lends itself well to patterns like MapReduce.
One advantage of distributing a client library is that you can enhance the generated gRPC and
Protobuf classes with helpful “convenience” methods and properties. In the client code, as in the
server, all the classes are declared as partial, so you can extend them without editing the generated
code. This behavior means it’s easy to add constructors, methods, and calculated properties to the
basic types.
Caution
You shouldn’t use custom code to provide essential functionality. You don’t want to restrict that
essential functionality to .NET teams that use the shared library, and not provide it to teams that use
other languages or platforms, such as Python or Java.
Ensure that as many teams as possible can access your gRPC service. The best way to do this
functionality is to share .proto files so developers can generate their own clients. This approach is
Useful extensions
There are two commonly used interfaces in .NET for dealing with streams of objects: IEnumerable and
IObservable. Starting with .NET Core 3.0 and C# 8.0, there’s an IAsyncEnumerable interface for
processing streams asynchronously, and an await foreach syntax for using the interface. This section
presents reusable code for applying these interfaces to gRPC streams.
With the .NET gRPC client libraries, there’s a ReadAllAsync extension method for
IAsyncStreamReader<T> that creates an IAsyncEnumerable<T> interface. For developers using
reactive programming, an equivalent extension method to create an IObservable<T> interface might
look like the example in the following section.
IObservable
The IObservable<T> interface is the “reactive” inverse of IEnumerable<T>. Rather than pulling items
from a stream, the reactive approach lets the stream push items to a subscriber. This behavior is very
similar to gRPC streams, and it’s easy to wrap an IObservable<T> interface around an
IAsyncStreamReader<T> interface.
This code is longer than the IAsyncEnumerable<T> code, because C# doesn’t have built-in support for
working with observables. You have to create the implementation class manually. It’s a generic class,
though, so a single implementation works across all types.
namespace Grpc.Core;
This observable implementation allows the Subscribe method to be called only once, because having
multiple subscribers trying to read from the stream would result in chaos. There are operators, such as
Replay in the System.Reactive.Linq, that enable buffering and repeatable sharing of observables,
which can be used with this implementation.
_task = Run(_tokenSource.Token);
}
_observer.OnNext(_reader.Current);
}
_completed = true;
_tokenSource.Dispose();
_task.Dispose();
}
All that is required now is a simple extension method to create the observable from the stream reader.
namespace Grpc.Core;
public static class AsyncStreamReaderObservableExtensions
{
public static IObservable<T> AsObservable<T>(
this IAsyncStreamReader<T> reader,
CancellationToken cancellationToken = default) =>
new GrpcStreamObservable<T>(reader, cancellationToken);
}
Summary
The IAsyncEnumerable and IObservable models are both well-supported and well-documented ways
of dealing with asynchronous streams of data in .NET. gRPC streams map well to both paradigms,
offering close integration with .NET, and reactive and asynchronous programming styles.
Note
This chapter will cover the facilities for authentication and authorization in gRPC for ASP.NET Core. It
will also discuss network security through TLS encrypted connections.
• Call authentication
• Azure Active Directory
• IdentityServer
• JWT Bearer Token
• OAuth 2.0
• OpenID Connect
• WS-Federation
• Channel authentication
• Client certificate
The call authentication methods are all based on tokens. The only real difference is how the tokens are
generated and the libraries that are used to validate the tokens in the ASP.NET Core service.
Note
When you’re using gRPC over a TLS-encrypted HTTP/2 connection, all traffic between clients and
servers is encrypted, even if you don’t use channel-level authentication.
This chapter will show how to apply call credentials and channel credentials to a gRPC service. It will
also show how to use credentials from a .NET gRPC client to authenticate with the service.
Call credentials
Call credentials are all based on a token passed in metadata with each request.
WS-Federation
ASP.NET Core supports WS-Federation using the WsFederation NuGet package. WS-Federation is the
closest available alternative to Windows Authentication, which isn’t supported over HTTP/2. Users are
authenticated by using Active Directory Federation Services (AD FS), which provides a token that can
be used to authenticate with ASP.NET Core.
For more information on how to get started with this authentication method, see Authenticate users
with WS-Federation in ASP.NET Core.
Add the Authentication service in the Program.cs class, and configure the JWT Bearer handler:
//
//
builder.Services.AddGrpc();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = signingKey
};
});
//
//
Next, add the Authorization service, which controls access to the system:
services.AddAuthorization(options =>
{
options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim(ClaimTypes.Name);
});
});
Tip
Authentication and authorization are two separate steps. You use authentication to determine the
user’s identity. You use authorization to decide whether that user is allowed to access various parts of
the system.
//
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<PortfolioService>();
});
}
Finally, apply the [Authorize] attribute to any services or methods to be secured, and use the User
property from the underlying HttpContext to verify permissions.
[Authorize]
public override async Task<GetResponse> Get(GetRequest request, ServerCallContext context)
{
if (!TryValidateUser(request.TraderId, context.GetHttpContext().User))
{
throw new RpcException(new Status(StatusCode.PermissionDenied, "Denied."));
}
// Display portfolio
}
Now you’ve secured your gRPC service by using JWT bearer tokens as call credentials. A version of the
portfolios sample gRPC application with authentication and authorization added is on GitHub.
You can combine channel credentials with call credentials to provide comprehensive security for a
gRPC service. The channel credentials prove that the client application is allowed to access the service,
and the call credentials provide information about the person who is using the client application.
Client certificate authentication works for gRPC the same way it works for ASP.NET Core. For more
information, see Configure certificate authentication in ASP.NET Core.
For development purposes you can use a self-signed certificate, but for production you should use a
proper HTTPS certificate signed by a trusted authority.
});
Add the certificate authentication service in the Program.cs, and add authentication and authorization
to the ASP.NET Core pipeline.
//
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.Chained;
options.RevocationMode = X509RevocationMode.NoCheck;
app.UseRouting();
app.UseAuthentication();
app.UseEndpoints(endpoints => { endpoints.MapGrpcService<GreeterService>(); });
//
class Program
{
static async Task Main(string[] args)
{
// Assume path to a client .pfx file and password are passed from command line
// On Windows this would probably be a reference to the Certificate Store
var cert = new X509Certificate2(args[0], args[1]);
Load a client certificate from certificate and private key .PEM files
A certificate can be loaded from a certificate and private key .pem file.
class Program
{
static async Task Main(string[] args)
{
// Assume path to a certificate and private key .pem files are passed from command
line
string certificatePem = File.ReadAllText(args[0]);
string privateKeyPem = File.ReadAllText(args[1]);
var cert = X509Certificate2.CreateFromPem(certificatePem, privateKeyPem);
Note
Due to an internal Windows bug as documented here, you’ll need to apply the following workaround
if the certificate is created from a certificate and private key PEM data.
Tip
You can use the ChannelCredentials.Create method for a client without certificate authentication.
This is a useful way to pass token credentials with every call made on the channel.
A version of the FullStockTicker sample gRPC application with certificate authentication added is on
GitHub.
gRPC leaves secure networking to the underlying HTTP/2 protocol, which you can secure by using TLS
certificates.
Web browsers insist on using TLS connections for HTTP/2, but most programmatic clients, including
.NET’s HttpClient, can use HTTP/2 over unencrypted connections.
For public APIs, you should always use TLS connections, and provide valid certificates for your services
from a proper SSL authority. LetsEncrypt provides free, automated SSL certificates, and most hosting
infrastructure today supports the LetsEncrypt standard with common plug-ins or extensions.
If you need to use explicit TLS between services running in Kubernetes, consider using an in-cluster
certificate authority and a certificate manager controller like cert-manager. You can then automatically
assign certificates to services at deployment time.
You can run your application as a Windows service. Or you can run it as a Linux service controlled by
systemd, because of new features in the .NET 6 hosting extensions.
Host.CreateDefaultBuilder(args)
.UseWindowsService()
...
Note
If the application isn’t running as a Windows service, the UseWindowsService method doesn’t do
anything.
• From Visual Studio by right-clicking the project and selecting Publish on the shortcut menu.
• From the .NET CLI.
When you publish a .NET application, you can choose to create a framework-dependent deployment
or a self-contained deployment. Framework-dependent deployments require the .NET Shared Runtime
to be installed on the host where they are run. Self-contained deployments are published with a
complete copy of the .NET runtime and framework and can be run on any host. For more information,
To publish a self-contained build of the application that does not require the .NET 5 runtime to be
installed on the host, specify the runtime to be included with the application. Use the -r (or --
runtime) flag.
Copy the complete contents of the publish directory to an installation folder. Then, use the sc tool to
create a Windows service for the executable file.
You can override the source name used in the event log by setting a SourceName property in these
settings. If you don’t specify a name, the default application name (normally the executable assembly
name) will be used.
Note
If the application isn’t running as a Linux service, the UseSystemd method doesn’t do anything.
Now publish your application. The application can be either framework dependent or self-contained
for the relevant Linux runtime (for example, linux-x64). You can publish by using one of these
methods:
Copy the complete contents of the publish directory to an installation folder on the Linux host.
Registering the service requires a special file, called a unit file, to be added to the
/etc/systemd/system directory. You’ll need root permission to create a file in this folder. Name the
file with the identifier that you want systemd to use and the .service extension. For example, use
/etc/systemd/system/myapp.service.
[Unit]
Description=My gRPC Application
[Service]
Type=notify
ExecStart=/usr/sbin/myapp
[Install]
WantedBy=multi-user.target
The Type=notify property tells systemd that the application will notify it on startup and shutdown.
The WantedBy=multi-user.target setting will cause the service to start when the Linux system
reaches “runlevel 2,” which means a non-graphical, multi-user shell is active.
Before systemd will recognize the service, it needs to reload its configuration. You control systemd by
using the systemctl command. After reloading, use the status subcommand to confirm that the
application has registered successfully.
If you’ve configured the service correctly, you’ll get the following output:
Tip
To tell systemd to start the service automatically on system startup, use the enable command.
Because journald is the standard for Linux logs, a variety of tools integrate with it. You can easily
route logs from journald to an external logging system. Working locally on the host, you can use the
journalctl command to view logs from the command line.
Tip
If you have a GUI environment available on your host, a few graphical log viewers are available for
Linux, such as QJournalctl and gnome-logs.
To learn more about querying the systemd journal from the command line by using journalctl, see
the manpages.
On Windows hosts, you can load the certificate from a secure certificate store by using the X509Store
class. You can also use the X509Store class with the OpenSSL key store on some Linux hosts.
You can also create certificates by using one of the X509Certificate2 constructors, from either:
{
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pfx",
"Password": "DO NOT STORE PLAINTEXT PASSWORDS IN APPSETTINGS FILES"
}
}
}
}
Important
Again, be sure to store the password for the .pfx file in, and retrieve it from, a secure configuration
source.
For each image, there are four variants based on different Linux distributions, distinguished by tags.
The Alpine base image is around 100 MB, compared to 200 MB for the Debian and Ubuntu images.
Some software packages or libraries might not be available in Alpine’s package management. If you’re
not sure which image to use, you should probably choose the default Debian.
Important
Make sure you use the same variant of Linux for the build and the runtime. Applications built and
published on one variant might not work on another.
WORKDIR /src
COPY ./StockKube.sln .
COPY ./src/StockData/StockData.csproj ./src/StockData/
COPY ./src/StockWeb/StockWeb.csproj ./src/StockWeb/
COPY . .
WORKDIR /app
The Dockerfile has two parts: the first uses the sdk base image to build and publish the application;
the second creates a runtime image from the aspnet base. This is because the sdk image is around
HTTPS in Docker
Microsoft base images for Docker set the ASPNETCORE_URLS environment variable to http://+:80,
meaning that Kestrel runs without HTTPS on that port. If you’re using HTTPS with a custom certificate
(as described in Self-hosted gRPC applications), you should override this configuration. Set the
environment variable in the runtime image creation part of your Dockerfile.
ENV ASPNETCORE_URLS=https://+:443
The confusingly named --tag flag (which can be shortened to -t) specifies the whole name of the
image, including the actual tag if specified. The . at the end specifies the context in which the build
will be run; the current working directory for the COPY commands in the Dockerfile.
If you have multiple applications within a single solution, you can keep the Dockerfile for each
application in its own folder, beside the .csproj file. You should still run the docker build command
from the base directory to ensure that the solution and all the projects are copied into the image. You
can specify a Dockerfile below the current directory by using the --file (or -f) flag.
The -ti flag connects your current terminal to the container’s terminal, and runs in interactive mode.
The -p 5000:80 publishes (links) port 80 on the container to port 5000 on the localhost network
interface.
To push to Docker Hub, prefix the image name with your user or organization name.
To push to a private registry, prefix the image name with the registry host name and the organization
name.
Kubernetes
Although it’s possible to run containers manually on Docker hosts, for reliable production systems it’s
better to use a container orchestration engine to manage multiple instances running across several
servers in a cluster. There are various container orchestration engines available, including Kubernetes,
Docker Swarm, and Apache Mesos. But of these engines, Kubernetes is far and away the most widely
used, so it will be the focus of this chapter.
• Scheduling runs containers on multiple nodes within a cluster, ensuring balanced usage of the
available resource, keeping containers running if there are outages, and handling rolling updates
to new versions of images or new configurations.
• Health checks monitor containers to ensure continued service.
• DNS & service discovery handles routing between services within a cluster.
• Ingress exposes selected services externally and generally provides load-balancing across
instances of those services.
• Resource management attaches external resources like storage to containers.
This chapter will detail how to deploy an ASP.NET Core gRPC service and a website that consumes the
service into a Kubernetes cluster. The sample application used is available in the dotnet-
architecture/grpc-for-wcf-developers repository on GitHub.
Kubernetes terminology
Kubernetes uses desired state configuration: the API is used to describe objects like Pods, Deployments,
and Services, and the Control Plane takes care of implementing the desired state across all the nodes
in a cluster. A Kubernetes cluster has a Master node that runs the Kubernetes API, which you can
communicate with programmatically or by using the kubectl command-line tool. kubectl can create
and manage objects through command-line arguments, but it works best with YAML files that contain
declaration data for Kubernetes objects.
apiVersion: v1
kind: Namespace
metadata:
# Object properties
The apiVersion property is used to specify which version (and which API) the file is intended for. The
kind property specifies the kind of object the YAML represents. The metadata property contains
object properties like name, namespace, and labels.
Pods
Pods are the basic units of execution in Kubernetes. They can run multiple containers, but they’re also
used to run single containers. The pod also includes any storage resources required by the containers,
and the network IP address.
Services
Services are meta-objects that describe Pods (or sets of Pods) and provide a way to access them
within the cluster, such as mapping a service name to a set of pod IP addresses by using the cluster
DNS service.
Deployments
Deployments are the desired state objects for Pods. If you create a pod manually, it won’t be restarted
when it terminates. Deployments are used to tell the cluster which Pods, and how many replicas of
those Pods, should be running at the present time.
Other objects
Pods, Services, and Deployments are just three of the most basic object types. There are dozens of
other object types that are managed by Kubernetes clusters. For more information, see the
Kubernetes Concepts documentation.
Namespaces
Kubernetes clusters are designed to scale to hundreds or thousands of nodes and to run similar
numbers of services. To avoid clashes between object names, namespaces are used to group objects
together as part of larger applications. Kubernetes’s own services run in a default namespace. All
user objects should be created in their own namespaces to avoid potential clashes with default
objects or other tenants in the cluster.
To confirm that your cluster is running and accessible, run the kubectl version command:
kubectl version
Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.3",
GitCommit:"1e11e4a2108024935ecfcb2912226cedeafd99df", GitTreeState:"clean",
BuildDate:"2020-10-14T12:50:19Z", GoVersion:"go1.15.2", Compiler:"gc",
Platform:"windows/amd64"}
Server Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.3",
GitCommit:"1e11e4a2108024935ecfcb2912226cedeafd99df", GitTreeState:"clean",
BuildDate:"2020-10-14T12:41:49Z", GoVersion:"go1.15.2", Compiler:"gc",
Platform:"linux/amd64"}
In this example, both the kubectl CLI and the Kubernetes server are running version 1.14.6. Each
version of kubectl is supposed to support the previous and next version of the server, so kubectl
1.14 should work with server versions 1.13 and 1.15 as well.
The apply command will check the validity of the YAML file and display any errors received from the
API, but doesn’t wait until all the objects declared in the file have been created because this step can
apiVersion: v1
kind: Namespace
metadata:
name: stocks
Use kubectl to apply the namespace.yml file and to confirm the namespace is created successfully:
apiVersion: apps/v1
kind: Deployment
metadata:
name: stockdata
namespace: stocks
spec:
selector:
matchLabels:
run: stockdata
replicas: 1
template:
metadata:
labels:
run: stockdata
spec:
containers:
- name: stockdata
image: stockdata:1.0.0
imagePullPolicy: Never
resources:
limits:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 80
The template.spec section declares the container to be run. When you’re working with a local
Kubernetes cluster, such as the one provided by Docker Desktop, you can specify images that were
built locally as long as they have a version tag.
Important
By default, Kubernetes will always check for and try to pull a new image. If it can’t find the image in
any of its known repositories, the Pod creation will fail. To work with local images, set the
imagePullPolicy to Never.
The ports property specifies which container ports should be published on the Pod. The
stockservice image runs the service on the standard HTTP port, so port 80 is published.
The resources section applies resource limits to the container running within the Pod. This is a good
practice because it prevents an individual Pod from consuming all the available CPU or memory on a
node.
Note
ASP.NET Core 6.0 has been optimized and tuned to run in resource-limited containers. The
dotnet/core/aspnet Docker image sets an environment variable to tell the dotnet runtime that it’s in
a container.
apiVersion: v1
kind: Service
metadata:
name: stockdata
namespace: stocks
spec:
ports:
- port: 80
selector:
run: stockdata
The Service spec uses the selector property to match running Pods, in this case looking for Pods that
have a label run: stockdata. The specified port on matching Pods is published by the named
service. Other Pods running in the stocks namespace can access HTTP on this service by using
http://stockdata as the address. Pods running in other namespaces can use the
http://stockdata.stocks host name. You can control cross-namespace service access by using
Network Policies.
apiVersion: apps/v1
kind: Deployment
metadata:
name: stockweb
namespace: stocks
spec:
selector:
matchLabels:
run: stockweb
replicas: 1
template:
metadata:
labels:
run: stockweb
spec:
containers:
- name: stockweb
image: stockweb:1.0.0
imagePullPolicy: Never
resources:
limits:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 80
env:
- name: StockData__Address
value: "http://stockdata"
- name: DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2UNENCRYPTEDSUPPORT
value: "true"
---
apiVersion: v1
kind: Service
metadata:
name: stockweb
namespace: stocks
Environment variables
The env section of the Deployment object specifies environment variables to be set in the container
that’s running the stockweb:1.0.0 images.
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
If you use an environment variable for the switch, you can easily change the context depending on the
context in which the application is running.
Service types
The type: NodePort property is used to make the web application accessible from outside the
cluster. This property type causes Kubernetes to publish port 80 on the Service to an arbitrary port on
the cluster’s external network sockets. You can find the assigned port by using the kubectl get
service command.
The stockdata Service shouldn’t be accessible from outside the cluster, so it uses the default type,
ClusterIP.
Production systems will most likely use an integrated load balancer to expose public applications to
external consumers. Services exposed in this way should use the LoadBalancer type.
For more information on Service types, see the Kubernetes Publishing Services documentation.
The output of the get service command shows that the HTTP port has been published to port 32564
on the external network. For Docker Desktop, this IP address will be localhost. You can access the
application by browsing to http://localhost:32564.
If the number of replicas of the stockdata Service were increased, you might expect the Server value
to change from line to line, but in fact all 100 records are always returned from the same instance. If
you refresh the page every few seconds, the server ID remains the same. Why does this happen?
There are two factors at play here.
First, the Kubernetes Service discovery system uses round-robin load balancing by default. The first
time the DNS server is queried, it will return the first matching IP address for the Service. The next
Second, the HttpClient used for the StockWeb application’s gRPC client is created and managed by
the ASP.NET Core HttpClientFactory, and a single instance of this client is used for every call to the
page. The client only does one DNS lookup, so all requests are routed to the same IP address. And
because the HttpClientHandler is cached for performance reasons, multiple requests in quick
succession will all use the same IP address, until the cached DNS entry expires or the handler instance
is disposed for some reason.
The result is that by default requests to a gRPC Service aren’t balanced across all instances of that
Service in the cluster. Different consumers will use different instances, but that doesn’t guarantee a
good distribution of requests or a balanced use of resources.
Service meshes
A service mesh is an infrastructure component that takes control of routing service requests within a
network. Service meshes can handle all kinds of network-level concerns within a Kubernetes cluster,
including:
• Service discovery
• Load balancing
• Fault tolerance
• Encryption
• Monitoring
Kubernetes service meshes work by adding an extra container, called a sidecar proxy, to each pod
included in the mesh. The proxy takes over handling all inbound and outbound network requests. You
can then keep the configuration and management of networking matters separate from the
application containers. In many cases, this separation doesn’t require any changes to the application
code.
In the previous chapter’s example, the gRPC requests from the web application were all routed to a
single instance of the gRPC service. This happens because the service’s host name is resolved to an IP
address, and that IP address is cached for the lifetime of the HttpClientHandler instance. It might be
possible to work around this behavior by handling DNS lookups manually or creating multiple clients.
But this workaround would complicate the application code without adding any business or customer
value.
When you use a service mesh, the requests from the application container are sent to the sidecar
proxy. The sidecar proxy can then distribute them intelligently across all instances of the other service.
The mesh can also:
You can see from the Server column that the requests from the StockWeb application have been
routed to both replicas of the StockData service, despite originating from a single HttpClient
instance in the application code. In fact, if you review the code, you’ll see that all 100 requests to the
StockData service are made simultaneously by using the same HttpClient instance. With the service
mesh, those requests will be balanced across however many service instances are available.
Service meshes apply only to traffic within a cluster. For external clients, see the next chapter, Load
Balancing.
• The organization’s specific requirements around costs, compliance, paid support plans, and so
on.
• The nature of the cluster, its size, the number of services deployed, and the volume of traffic
within the cluster network.
• Ease of deploying and managing the mesh and using it with services.
With the Linkerd CLI installed, follow the Getting Started instructions to install the Linkerd
components on your Kubernetes cluster. The instructions are straightforward, and the installation
should take only a couple of minutes on a local Kubernetes instance.
You can inspect the new files to see what changes have been made. For deployment objects, a
metadata annotation is added to tell Linkerd to inject a sidecar proxy container into the pod when it’s
created.
It’s also possible to pipe the output of the linkerd inject command to kubectl directly. The
following commands will work in PowerShell or any Linux shell.
linkerd dashboard
The dashboard provides detailed information about all services that are connected to the mesh.
apiVersion: apps/v1
kind: Deployment
metadata:
name: stockdata
namespace: stocks
spec:
selector:
matchLabels:
run: stockdata
replicas: 2 # Increase the target number of instances
template:
metadata:
annotations:
linkerd.io/inject: enabled
creationTimestamp: null
labels:
run: stockdata
spec:
containers:
- name: stockdata
image: stockdata:1.0.0
imagePullPolicy: Never
resources:
limits:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 80
Load balancers are classified according to the layer they operate on. Layer 4 load balancers work on
the transport level, for example, with TCP sockets, connections, and packets. Layer 7 load balancers
work at the application level, specifically handling HTTP/2 requests for gRPC applications.
L4 load balancers
An L4 load balancer accepts a TCP connection request from a client, opens another connection to one
of the back-end instances, and copies data between the two connections with no real processing. L4
offers excellent performance and low latency, but with little control or intelligence. As long as the
client keeps the connection open, all requests will be directed to the same back-end instance.
L7 load balancers
An L7 load balancer parses incoming HTTP/2 requests and passes them on to back-end instances on a
request-by-request basis, no matter how long the connection is held by the client.
• NGINX
• HAProxy
• Traefik
As a rule of thumb, L7 load balancers are the best choice for gRPC and other HTTP/2 applications (and
for HTTP applications generally, in fact). L4 load balancers will work with gRPC applications, but
they’re primarily useful when low latency and low overhead are important.
Important
At the time of this writing, some L7 load balancers don’t support all the parts of the HTTP/2
specification that are required by gRPC services, such as trailing headers.
If you’re using TLS encryption, load balancers can terminate the TLS connection and pass unencrypted
requests to the back-end application, or they can pass the encrypted request along. Either way, the
load balancer will need to be configured with the server’s public and private key so it can decrypt
requests for processing.
See to the documentation for your preferred load balancer to find out how to configure it to handle
HTTP/2 requests with your back-end services.
Metrics refers to numeric data designed to be aggregated and presented by using charts and graphs
in a dashboard. The dashboard provides a view of the overall health and performance of an
application. Metrics data can also be used to trigger automated alerts when a threshold is exceeded.
Here are some examples of metrics data:
The ASP.NET Core gRPC framework writes detailed diagnostic logging messages to this logging
framework, so they can be processed and stored along with your application’s own messages.
Many log messages, such as requests and exceptions, are provided by the ASP.NET Core and gRPC
framework components. Add your own log messages to provide detail and context about application
logic, rather than lower-level concerns.
For more information about writing log messages and available logging sinks and targets, see
Logging in .NET Core and ASP.NET Core.
For more advanced metrics and for writing metric data to a wider range of data stores, you might try
an open-source project called App Metrics. This suite of libraries provides an extensive set of types to
instrument your code. It also offers packages to write metrics to different kinds of targets that include
time-series databases, such as Prometheus and InfluxDB, and Application Insights. The
App.Metrics.AspNetCore.Mvc NuGet package even adds a comprehensive set of basic metrics that are
automatically generated via integration with the ASP.NET Core framework. The project website
provides templates for displaying those metrics with the Grafana visualization platform.
Produce metrics
Most metrics platforms support the following types:
Metric
type Description
Counter Tracks how often something happens, such as requests and errors.
Gauge Records a single value that changes over time, such as active connections.
Histogram Measures a distribution of values across arbitrary limits. For example, a histogram can
track dataset size, counting how many contained <10 records, how many contained
11-100 records, how many contained 101-1000 records, and how many contained
>1000 records.
Meter Measures the rate at which an event occurs in various time spans.
Timer Tracks the duration of events and the rate at which it occurs, stored as a histogram.
By using App Metrics, an IMetrics interface can be obtained via dependency injection, and used to
record any of these metrics for a gRPC service. The following example shows how to count the
number of Get requests made over time:
// Serve request...
}
}
The current go-to solution for visualizing metrics data is Grafana, which works with a wide range of
storage providers. The following image shows an example Grafana dashboard that displays metrics
from the Linkerd service mesh running the StockData sample:
Distributed tracing
Distributed tracing is a relatively recent development in monitoring, which has arisen from the
increasing use of microservices and distributed architectures. A single request from a client browser,
application, or device can be broken down into many steps and sub-requests, and involve the use of
many services across a network. This activity makes it difficult to correlate log messages and metrics
with the specific request that triggered them. Distributed tracing applies identifiers to requests, and
allows logs and metrics to be correlated with a particular operation. This tracing is similar to WCF’s
end-to-end tracing, but it’s applied across multiple platforms.
Distributed tracing has grown quickly in popularity and is beginning to standardize. The Cloud Native
Computing Foundation created the Open Tracing standard, attempting to provide vendor-neutral
libraries for working with back ends like Jaeger and Elastic APM. At the same time, Google created the
OpenCensus project to address the same set of problems. These two projects are merging into a new
project, OpenTelemetry, which aims to be the industry standard of the future.
Because DiagnosticSource is a part of the core framework and later, it’s supported by several core
components. These include HttpClient, Entity Framework Core, and ASP.NET Core, including explicit
support in the gRPC framework. When ASP.NET Core receives a request, it checks for a pair of HTTP
headers matching the W3C Trace Context standard. If the headers are found, an activity is started by
using the identity values and context from the headers. If no headers are found, an activity is started
with generated identity values that match the standard format. Any diagnostics generated by the
framework or by application code during the lifetime of this activity can be tagged with the trace and
span identifiers. The HttpClient support extends this functionality further by checking for a current
activity on every request, and automatically adding the trace headers to the outgoing request.
The ASP.NET Core gRPC client and server libraries include explicit support for DiagnosticSource and
Activity, and create activities and apply and use header information automatically.
Note
All of this happens only if a listener is consuming the diagnostic information. If there’s no listener, no
diagnostics are written and no activities are created.
The OpenTracing API is described in the following section. If you want to use the OpenTelemetry API
in your application instead, refer to the OpenTelemetry .NET SDK repository on GitHub.
//
builder.Services.AddOpenTracing();
//
The OpenTracing package is an abstraction layer, and as such it requires implementation specific to
the back end. OpenTracing API implementations are available for the following open source back
ends.
For more information on the OpenTracing API for .NET, see the OpenTracing for C# and the
OpenTracing Contrib C#/.NET Core repositories on GitHub.
In the newer microservices landscape, this type of automated distributed transaction processing isn’t
possible. There are too many different technologies involved, including relational databases, NoSQL
data stores, and messaging systems. There might also be a mix of operating systems, programming
languages, and frameworks in use in a single environment.
WCF distributed transaction is an implementation of what is known as a two-phase commit (2PC). You
can implement 2PC transactions manually by coordinating messages across services, creating open
transactions within each service, and sending commit or rollback messages, depending upon success
or failure. However, the complexity involved in managing 2PC can increase exponentially as systems
evolve. Open transactions hold database locks that can negatively affect performance, or, worse, cause
cross-service deadlocks.
If possible, it’s best to avoid distributed transactions altogether. If two items of data are so linked as to
require atomic updates, consider handling them both with the same service. Apply those atomic
changes by using a single request or message to that service.
If that isn’t possible, then one alternative is to use the Saga pattern. In a saga, updates are processed
sequentially; as each update succeeds, the next one is triggered. These triggers can be propagated
from service to service, or managed by a saga coordinator or orchestrator. If an update fails at any
point during the process, the services that have already completed their updates apply specific logic
to reverse them.
Another option is to use Domain Driven Design (DDD) and Command/Query Responsibility
Segregation (CQRS), as described in the .NET Microservices e-book. In particular, using domain events
or event sourcing can help to ensure that updates are consistently, if not immediately, applied.