Skip to main content

Service Implementation

The DiscountService class implements all gRPC operations defined in the discount.proto file. It inherits from the auto-generated DiscountProtoService.DiscountProtoServiceBase class.

Complete Service Code

Location: src/Services/Discount/Discount.Grpc/Services/DiscountService.cs
using Discount.Grpc.Data;
using Discount.Grpc.Models;
using Grpc.Core;
using Mapster;
using Microsoft.EntityFrameworkCore;

namespace Discount.Grpc.Services;

public class DiscountService
    (DiscountContext dbContext, ILogger<DiscountService> logger)
    : DiscountProtoService.DiscountProtoServiceBase
{    
    public override async Task<CouponModel> GetDiscount(GetDiscountRequest request, ServerCallContext context)
    {
        var coupon = await dbContext
            .Coupons
            .FirstOrDefaultAsync(x => x.ProductName == request.ProductName);

        if (coupon is null)
            coupon = new Coupon { ProductName = "No Discount", Amount = 0, Description = "No Discount Desc" };

        logger.LogInformation("Discount is retrieved for ProductName : {productName}, Amount : {amount}", coupon.ProductName, coupon.Amount);

        var couponModel = coupon.Adapt<CouponModel>();
        return couponModel;
    }

    public override async Task<CouponModel> CreateDiscount(CreateDiscountRequest request, ServerCallContext context)
    {
        var coupon = request.Coupon.Adapt<Coupon>();
        if (coupon is null)
            throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid request object."));

        dbContext.Coupons.Add(coupon);
        await dbContext.SaveChangesAsync();

        logger.LogInformation("Discount is successfully created. ProductName : {ProductName}", coupon.ProductName);

        var couponModel = coupon.Adapt<CouponModel>();
        return couponModel;
    }

    public override async Task<CouponModel> UpdateDiscount(UpdateDiscountRequest request, ServerCallContext context)
    {
        var coupon = request.Coupon.Adapt<Coupon>();
        if (coupon is null)
            throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid request object."));

        dbContext.Coupons.Update(coupon);
        await dbContext.SaveChangesAsync();

        logger.LogInformation("Discount is successfully updated. ProductName : {ProductName}", coupon.ProductName);

        var couponModel = coupon.Adapt<CouponModel>();
        return couponModel;
    }

    public override async Task<DeleteDiscountResponse> DeleteDiscount(DeleteDiscountRequest request, ServerCallContext context)
    {
        var coupon = await dbContext
            .Coupons
            .FirstOrDefaultAsync(x => x.ProductName == request.ProductName);

        if (coupon is null)
            throw new RpcException(new Status(StatusCode.NotFound, $"Discount with ProductName={request.ProductName} is not found."));

        dbContext.Coupons.Remove(coupon);
        await dbContext.SaveChangesAsync();

        logger.LogInformation("Discount is successfully deleted. ProductName : {ProductName}", request.ProductName);

        return new DeleteDiscountResponse { Success = true };
    }
}

Service Methods

1. GetDiscount

Purpose: Retrieve discount information for a specific product. Method Signature:
public override async Task<CouponModel> GetDiscount(
    GetDiscountRequest request, 
    ServerCallContext context)
Request:
message GetDiscountRequest {
    string productName = 1;
}
Response:
message CouponModel {
    int32 id = 1;
    string productName = 2;
    string description = 3;
    int32 amount = 4;
}
Implementation Details:
  • Queries SQLite database using Entity Framework Core
  • Returns a default “No Discount” coupon if product not found
  • Logs the retrieved discount information
  • Uses Mapster to map entity to gRPC model
Graceful Handling: Instead of throwing an exception when no discount exists, it returns a zero-amount coupon, allowing calling services to handle it seamlessly.

2. CreateDiscount

Purpose: Create a new discount coupon for a product. Method Signature:
public override async Task<CouponModel> CreateDiscount(
    CreateDiscountRequest request, 
    ServerCallContext context)
Request:
message CreateDiscountRequest {
    CouponModel coupon = 1;
}
Response: Returns the created CouponModel Implementation Details:
  • Validates the request - throws RpcException with InvalidArgument status if null
  • Uses Mapster to convert gRPC model to entity
  • Adds to database and saves changes
  • Returns the created coupon
Error Handling:
if (coupon is null)
    throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid request object."));

3. UpdateDiscount

Purpose: Update an existing discount coupon. Method Signature:
public override async Task<CouponModel> UpdateDiscount(
    UpdateDiscountRequest request, 
    ServerCallContext context)
Request:
message UpdateDiscountRequest {
    CouponModel coupon = 1;
}
Response: Returns the updated CouponModel Implementation Details:
  • Validates the incoming coupon model
  • Maps gRPC model to entity
  • Updates the entity in the database
  • Returns the updated coupon
Note: The update uses EF Core’s Update method which tracks changes and updates all properties.

4. DeleteDiscount

Purpose: Delete a discount coupon by product name. Method Signature:
public override async Task<DeleteDiscountResponse> DeleteDiscount(
    DeleteDiscountRequest request, 
    ServerCallContext context)
Request:
message DeleteDiscountRequest {
    string productName = 1;
}
Response:
message DeleteDiscountResponse {
    bool success = 1;
}
Implementation Details:
  • Finds the coupon by product name
  • Throws RpcException with NotFound status if coupon doesn’t exist
  • Removes from database
  • Returns success response
Error Handling:
if (coupon is null)
    throw new RpcException(new Status(
        StatusCode.NotFound, 
        $"Discount with ProductName={request.ProductName} is not found."
    ));

Key Technologies

Mapster

Mapster is used for object-to-object mapping between entities and gRPC models:
// Entity to gRPC Model
var couponModel = coupon.Adapt<CouponModel>();

// gRPC Model to Entity
var coupon = request.Coupon.Adapt<Coupon>();
Benefits:
  • Zero configuration for simple mappings
  • High performance
  • Compile-time safety
  • Less boilerplate than manual mapping

ServerCallContext

The ServerCallContext parameter provides:
  • Cancellation tokens
  • Request headers and metadata
  • Response trailers
  • Peer information
  • Deadline information
// Example usage (if needed)
var cancellationToken = context.CancellationToken;
var userAgent = context.RequestHeaders.GetValue("user-agent");

RpcException

gRPC-specific exception for communicating errors to clients:
throw new RpcException(new Status(StatusCode.NotFound, "Resource not found"));
Common Status Codes:
  • OK: Success
  • Cancelled: Operation cancelled
  • InvalidArgument: Client specified invalid argument
  • NotFound: Requested entity not found
  • AlreadyExists: Entity already exists
  • PermissionDenied: Permission denied
  • Unauthenticated: Authentication required
  • Internal: Internal server error

Client Consumption

Adding gRPC Client (Basket Service Example)

1. Add Proto Reference (Basket.API.csproj):
<ItemGroup>
  <Protobuf Include="..\..\Discount\Discount.Grpc\Protos\discount.proto" GrpcServices="Client">
    <Link>Protos\discount.proto</Link>
  </Protobuf>
</ItemGroup>
2. Register gRPC Client (Program.cs):
builder.Services.AddGrpcClient<DiscountProtoService.DiscountProtoServiceClient>(options =>
{
    options.Address = new Uri(builder.Configuration["GrpcSettings:DiscountUrl"]!);
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler
    {
        // For development - accept any SSL certificate
        ServerCertificateCustomValidationCallback =
        HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
    };
    return handler;
});
3. Inject and Use:
public class StoreBasketCommandHandler
    (IBasketRepository repository, DiscountProtoService.DiscountProtoServiceClient discountProto)
    : ICommandHandler<StoreBasketCommand, StoreBasketResult>
{
    public async Task<StoreBasketResult> Handle(StoreBasketCommand command, CancellationToken cancellationToken)
    {
        await DeductDiscount(command.Cart, cancellationToken);
        await repository.StoreBasket(command.Cart, cancellationToken);
        return new StoreBasketResult(command.Cart.UserName);
    }

    private async Task DeductDiscount(ShoppingCart cart, CancellationToken cancellationToken)
    {
        foreach (var item in cart.Items)
        {
            var coupon = await discountProto.GetDiscountAsync(
                new GetDiscountRequest { ProductName = item.ProductName },
                cancellationToken: cancellationToken);
            
            item.Price -= coupon.Amount;
        }
    }
}

Service Registration

In Program.cs, the gRPC service is registered and mapped:
// Register gRPC infrastructure
builder.Services.AddGrpc();

// Register DbContext for dependency injection
builder.Services.AddDbContext<DiscountContext>(opts =>
    opts.UseSqlite(builder.Configuration.GetConnectionString("Database")));

var app = builder.Build();

// Map the gRPC service endpoint
app.MapGrpcService<DiscountService>();

Testing with gRPC Tools

Using grpcurl

List services:
grpcurl -plaintext localhost:5003 list
Call GetDiscount:
grpcurl -plaintext -d '{"productName": "IPhone X"}' \
  localhost:5003 discount.DiscountProtoService/GetDiscount
Call CreateDiscount:
grpcurl -plaintext -d '{
  "coupon": {
    "productName": "iPad Pro",
    "description": "iPad Discount",
    "amount": 200
  }
}' localhost:5003 discount.DiscountProtoService/CreateDiscount

Logging

The service logs all operations:
logger.LogInformation("Discount is retrieved for ProductName : {productName}, Amount : {amount}", 
    coupon.ProductName, coupon.Amount);

logger.LogInformation("Discount is successfully created. ProductName : {ProductName}", 
    coupon.ProductName);

logger.LogInformation("Discount is successfully updated. ProductName : {ProductName}", 
    coupon.ProductName);

logger.LogInformation("Discount is successfully deleted. ProductName : {ProductName}", 
    request.ProductName);

Best Practices Demonstrated

  1. Constructor Injection: Using primary constructor syntax (C# 12)
  2. Async/Await: All operations are asynchronous
  3. Graceful Degradation: GetDiscount returns default value instead of error
  4. Proper Error Handling: Using gRPC-specific exceptions
  5. Logging: Comprehensive logging of all operations
  6. Separation of Concerns: Entity vs gRPC model separation
  7. Validation: Input validation with proper error messages

Next Steps

Build docs developers (and LLMs) love