In this article, we’re going to cover how to set up an ASP.NET Core gRPC service with MongoDB as our database. MongoDB is an increasingly popular database and is often integrated with a gRPC service. Integration with MongoDB has been covered in a previous article so our focus today will be on setting up a simple gRPC service with ASP.NET Core.

To download the source code, you can visit our gRPC with ASP.NET Core repository.

This post is divided into the following parts:

Let’s jump right in.

Introducing gRPC

Before we start coding, let’s get a good sense of what gRPC is. Firstly, gRPC is a communications protocol that enables a client and a server to exchange messages. gRPC is unique in that it uses protocol buffers to define methods and messages and to generate interface code between different programming languages. It can be used between microservices and across HTTP clients.

gRPC is becoming increasingly popular as an alternative to REST APIs in low-power systems. Often REST APIs can be demanding of resources by giving you more data than you actually need. Additionally, gRPC allows for bidirectional streaming.

Let’s look at a common use case. We could have a low-power mobile system full of sensors that collect data. Using gRPC, we can stream that data to a remote server to perform heavy computation and send those results back to the mobile client. We could do all this in real-time (or near real-time). With REST, we would have to periodically send POST requests of our data. Then we would periodically send a GET request until our computed result is ready. In this case, gRPC is a better alternative.

Now, let’s see how we can implement gRPC in our ASP.NET Core server. There are 4 types of gRPC: unary, server streaming, unary streaming, and bidirectional streaming. For this example, we are going to look at the unary type as it is the most similar to REST.

Creating an ASP.NET Core Project with MongoDB

We are going to continue to use the MongoDB database we created in the previous article. We highly recommend checking it out to be able to follow along. In that article, we get into all the details on how to set up our MongoDB backend.

Let’s begin by clearing the Courses and Students collections.

db.Courses.remove({})

db.Students.remove({})

We are going to continue by creating two documents in the Courses collection:

db.Courses.insertMany([{'Name': 'Power Systems, 'Code': 'ECEN 485'}, {'Name': 'Networks, 'Code': 'ECEN 474'}])

Let’s take note of the ObjectIds that were created and use them to create a document in the Students collection:

MongoDb ObjectIds - ASP.NET Core with gRPC

db.Students.insert({"FirstName" : "Linus", "LastName" : "Torvald", "Major" : "Electrical Engineering", "Courses" : [ ObjectId("5eeffb00a28671a6e62dbda0"), ObjectId("5eeffb00a28671a6e62dbda1")]})

This command creates a student record with a reference to two courses.

That’s it for our sample data!

Writing the Models

Let’s go ahead and create a new WebAPI project and configure it with MongoDB. We won’t get into the details on how to do this in this article, but step-by-step instructions can be found in our previous MongoDB article.

We are going to begin by creating the Course.cs class under the Models folder:

public class Course
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string Id { get; set; }

    public string Name { get; set; }

    public string Code { get; set; }
}

Now, we are going to create the Student.cs class under the Models folder:
public class Student
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string Id { get; set; }

    [Required(ErrorMessage = "First name is required")]
    public string FirstName { get; set; }

    [Required(ErrorMessage = "Last name is required")]
    public string LastName { get; set; }

    public string Major { get; set; }

    [BsonRepresentation(BsonType.ObjectId)]
    public List<string> Courses {get; set;}

    [BsonIgnore]
    public List<Course> CourseList {get; set;}
}

We have defined a Courses and a CourseList property in the Student class. The Courses property is a collection of string representations of the MongoDB-generated ObjectIds. CourseList is a collection of Course objects which are not represented in the database.

Writing the Data Access Layer

Let’s continue by creating the StudentDataAccess.cs class under the DataAccess folder:

public class StudentDataAccess
{
    private readonly IMongoCollection<Student> _students;

    private readonly IMongoCollection<Course> _courses;

    public StudentDataAccess(ISchoolDatabaseSettings settings)
    {
        var client = new MongoClient(settings.ConnectionString);
        var database = client.GetDatabase(settings.DatabaseName);
        _students = database.GetCollection<Student>(settings.StudentsCollectionName);
        _courses = database.GetCollection<Course>(settings.CoursesCollectionName);
    }

    public async Task<List<Student>> GetAllAsync()
    {
        return await _students.Find(s => true).ToListAsync();
    }

    public async Task<Student> GetByIdAsync(string id)
    {
        return await _students.Find<Student>(s => s.Id == id).FirstOrDefaultAsync();
    }

    public async Task<Student> GetByIdWithCoursesAsync(string id)
    {
        var student = await GetByIdAsync(id);
        if (student.Courses != null && student.Courses.Count > 0)
        {
           student.CourseList = await _courses.Find<Course>(c => student.Courses.Contains(c.Id)).ToListAsync();
        }
        return student;
    }

    public async Task<Student> CreateAsync(Student student)
    {
        await _students.InsertOneAsync(student);
        return student;
    }

    public async Task UpdateAsync(string id, Student student)
    {
        await _students.ReplaceOneAsync(s => s.Id == id, student);
    }

    public async Task DeleteAsync(string id)
    {
        await _students.DeleteOneAsync(s => s.Id == id);
    }
}

Let’s take a close look at the GetByIdWithCoursesAsync method. This method returns a Student object populated with the data from the Student document with the given id. Additionally, this method also populates the CourseList property with Course objects if there are references to Course documents.

For step-by-step instructions and an in-depth overview of what the other methods do visit our article on Getting Started with ASP.NET Core and MongoDB.

That’s it for the MongoDB part of the project!

Defining Protobuf Messages

We can continue with the most important part of any gRPC project. Let’s go ahead and create student-service.proto under the Protos folder:

syntax = "proto3";

option csharp_namespace = "StudentGrpcService";

message Course {
  string id = 1;
  string name = 2;
  string code = 3;
}

message Student {
  string id = 1;
  string firstName = 2;
  string lastName = 3;
  string major = 4;
  repeated Course courses = 5;
}

service StudentService {
  rpc GetStudent(GetStudentRequest) returns (GetStudentResponse);
  rpc GetStudents(GetStudentsResponse) returns (GetStudentsResponse);
  rpc CreateOrUpdateStudent (CreateOrUpdateStudentRequest) returns (CreateOrUpdateStudentResponse);
}

message GetStudentsRequest {}

message GetStudentsResponse {
  repeated Student students = 1;
  string error = 2;
}

message GetStudentRequest {
  string id = 1;
}

message GetStudentResponse {
  Student student = 1;
  string error = 2;
}

message CreateOrUpdateStudentRequest {
  Student student = 1;
}

message CreateOrUpdateStudentResponse {
  bool success = 1;
  string error = 2;
}

Let’s dive into this file!

First, let’s look at the first two declarative statements. The syntax statement tells the protobuf compiler what syntax we are going to use. As the protobuf mechanism develops, it’s important to specify which protobuf version we are using. The second statement is optional and it tells the protobuf compiler to generate C# classes within the specified namespace: StudentGrpcService. Although it’s optional, this statement is highly recommended to avoid name clashes.

Let’s continue with the message type definitions.

Firstly, we are defining the Student and Course message types. If we take a close look, these message types correspond to the model definitions in our project. Therefore, we can treat these messages like data transfer objects (DTOs). This is where protobuf really shines. We can define DTOs in the protobuf syntax and generate code for any language that is supported by protobuf.

Before we move on to services, we are going to notice other message types with a name ending with Request or Response. These message types help us customize the format of our request and response data for each of our RPC services.

Next, let’s go to the service type definitions.

We are going to call our service type StudentService, which is a collection of three RPC services. In this article, we are going to look at the GetStudent RPC service. Every RPC service is simply a method that may take a request message and return a response message. This RPC service takes in a GetStudentRequest message and returns a GetStudentResponse message.

Now, we can move on to generating our C# classes.

Generating C# Classes with Protobuf

The first thing we are going to do is install the Grpc.AspNetCoreNuGet package:

dotnet add package Grpc.AspNetCore

Then we can add a protobuf element to the project file:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.29.0" />
    <PackageReference Include="MongoDb.Driver" Version="2.10.4" />
  </ItemGroup>

  <ItemGroup>
    <Protobuf Include="Protos\student-service.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>

In some configurations of the VS Code editor, the Protobuf tag may cause syntax errors. If we’re using VS Code with the Omnisharp extension, we may need to use the following option in the settings.json for VS Code:

"omnisharp.useGlobalMono": "never"

Now, we can generate the C# classes that our gRPC server is going to use:

dotnet build

Furthermore, we can verify that the classes were generated by navigating to obj, then Debug, then netcoreapp3.1. We should be able to see the Student-serviceGrpc.cs and the StudentService.cs classes.

We have successfully used protobuf!

Writing the gRPC Service

First, we are going to need to install the Automapper and its dependency injection NuGet package.

dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

Let’s create the mapping profile StudentProfile.cs under the Profiles folder:

public class StudentProfile : Profile
{
    public StudentProfile()
    {
        CreateMap<Models.Course, StudentGrpcService.Course>();

        CreateMap<Models.Student, StudentGrpcService.Student>()
            .ForMember(dest => dest.Courses, opt => opt.MapFrom(src => src.CourseList));
    }
}

The mapping profile maps our MongoDB models to our protobuf generated models. These profiles let us easily transfer data from the data access layer to the presentation layer. If you are not familiar with AutoMapper check out our Getting Started with AutoMapper in ASP.NET Core article.

Let’s continue by creating the StudentGrpcController.cs class under the Controllers folder:

public class StudentGrpcController : StudentService.StudentServiceBase
{
    private readonly StudentDataAccess _students;

    private readonly IMapper _mapper;

    public StudentGrpcController( StudentDataAccess students, IMapper mapper)
    {
        _students = students;
        _mapper = mapper;
    }

    public override async Task<GetStudentResponse> GetStudent(GetStudentRequest request, ServerCallContext context)
    {
        try
        {
            if (request.Id != null)
            {
                var student = await _students.GetByIdWithCoursesAsync(request.Id);
                return new GetStudentResponse
                {
                    Student = _mapper.Map<Student>(student)
                };
            }
            else
            {
                return new GetStudentResponse
                {
                    Error = "ID is null or empty"
                };
            }
        }
        catch (Exception ex)
        {
            return new GetStudentResponse
            {
                Error = $"{ex.Message}"
            };
        }
    }
}

Technically, this is not a controller class, because there is no ASP.NET controller base. However, this naming convention helps us remember that it functions in place of a traditional API controller and avoids potential name clashes with generic terms like “service”. This class inherits from the generated StudentServiceBase which makes our RPC service available as an overridable C# method. For now, we are only overriding the GetStudent method.

In this class, we are using the DI(dependency injection) to get an instance of the StudentDataAccess class and an instance of the AutoMapper. Now, let’s take a look at the asynchronous GetStudent method.

In this method, we need two arguments: the request and the context. We know that the request object only has one property: id. So, we are going to use this id to query the MongoDB database and get the corresponding Studentobject from the GrpcExamplenamespace. Then we can convert that object using AutoMapper to the protobuf generated Student type available within the StudentGrpcServicenamespace.

The last part of this method is the return statements. This method must return a GetStudentResponseobject. We defined this object to have two properties: Student and Error. If a valid id is given, then the method returns a GetStudentResponse object with the queried student data. If the id is invalid or there is some other error, the method returns a GetStudentResponseobject with some details of what went wrong.

Okay, now that we covered this class, let’s go ahead and configure our project with gRPC.

Configuring an ASP.NET Core Project with gRPC

We are going to begin by modifying the Startup class:

public class Startup
{

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

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<SchoolDatabaseSettings>(
        Configuration.GetSection(nameof(SchoolDatabaseSettings)));

        services.AddSingleton<ISchoolDatabaseSettings>(provider =>
              provider.GetRequiredService<IOptions<SchoolDatabaseSettings>>().Value);

        services.AddSingleton<StudentDataAccess>();
        services.AddGrpc();
        services.AddAutoMapper(typeof(Startup));
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGrpcService<StudentGrpcController>();
        });
    }
}

In the Startup class, we are adding a few services. The most relevant one for this project is the AddGrpc method. This tells the server that we need gRPC support. We also need to map the gRPC service controller to route endpoints. Without this configuration, the server won’t know what to do with any gRPC requests.

MacOS Workaround

Lastly, we’re going to need a workaround if we are developing on macOS and with the .NET Core SDK 3.1. Let’s modify the Program.cs class:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.ConfigureKestrel(options =>
                {
                    // Setup a HTTP/2 endpoint without TLS.
                    options.ListenLocalhost(5000, o => o.Protocols =
                        HttpProtocols.Http2);
                });
                webBuilder.UseStartup<Startup>();
            });
}

This workaround enables the server to host an unsecured HTTP2 protocol.

Creating a gRPC Console Client

The next part is the client code. We are going to write a console client application in C#. It’s possible to write a client application in any other language, but for this example, we are going to stick to C#.

Let’s begin by creating a console project:

dotnet new console GrpcClient

Then we are going to add some NuGet packages:

dotnet add package Grpc.Net.Client

dotnet add package Google.Protobuf

dotnet add package Grpc.Tools

After that, we are going to copy the student-service.proto file from the server-side project into the client-side project under the Protos folder.

Then we are going to modify GrpcClient.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.12.3" />
    <PackageReference Include="Grpc.Net.Client" Version="2.29.0" />
    <PackageReference Include="Grpc.Tools" Version="2.29.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="Protos\student-service.proto" GrpcServices="Client" />
  </ItemGroup>

</Project>

Like in the server-side project file, this modification tells the project to also run the protobuf compiler on the included file path. Let’s build the project:

dotnet build

The build command runs the protobuf compiler and generates the necessary C# files we are going to need to interface with our gRPC server. This is by far one of the best features of gRPC.

Writing the gRPC Client

Lastly, we can write the actual console application in the Program class:

class Program
{
    static async Task Main(string[] args)
    {
        AppContext.SetSwitch(
          "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

        using var channel = GrpcChannel.ForAddress("http://localhost:5000");
    
        var client = new StudentService.StudentServiceClient(channel);
    
        while(true)
        {
            Console.WriteLine("Enter an argument");
      
            string line = Console.ReadLine();
      
            if (line == "exit")
            {
                break;
            }
      
            var reply = await client.GetStudentAsync(
                new GetStudentRequest (){Id = line}
            );
      
            if(reply.Student != null)
            {
                Console.WriteLine("Reply: " + reply.Student);
            }
      
            if(reply.Error != null)
            {
                Console.WriteLine("Reply: " + reply.Error);
            }
        }
        Console.WriteLine("Exiting...");
    }
}

Firstly, we have to configure the project to support unsecured HTTP2 requests. We only have to do this, if we use the workaround for a macOS development environment. 

Next, we are configuring a gRPC channel for the address of the gRPC server: http://localhost:5000.  Then, we are using a variable of type StudentServiceClientto store the client. StudentServiceClientis a class generated by protobuf. We defined this class when we defined the StudentService service in the proto file.

Now, for the most important part of the console application: the while loop.

The application expects us to type in “exit” or the id of the student we are interested in. If we type in “exit”, the application will end. Otherwise, it will instantiate a GetStudentRequest object with the value we typed in. Then the program will pass that object as an argument into the GetStudentAsync method. This method will trigger a gRPC request to the corresponding RPC service on the server. If the id is valid, then it will return a reply of type GetStudentResponse with the corresponding student data. If the id is not valid, it will return a reply of type GetStudentResponse with an error message.

Let’s go ahead and try it:

gRPC Client Console - ASP.NET Core with gRPC and MongoDB

And just like that, we have integrated a gRPC service in ASP.NET Core with a MongoDB backend!

Conclusion

Instead of using a traditional REST service, we successfully implemented a gRPC service with ASP.NET Core. This example only implements a unary service. However, this example can be extended to implement other gRPC services: client streaming, server streaming, and bidirectional streaming.

In summary, we have covered:

  • A basic understanding of gRPC
  • Defining protobuf messages
  • Configuring an ASP.NET Core project with gRPC
  • How to use server-side and client-side gRPC services in C#