In modern software development, APIs (Application Programming Interfaces) are the backbone of communication between services and applications. Choosing the right API style can greatly influence performance, scalability, and maintainability. In this blog, we will explore four popular API technologies—REST, GraphQL, and gRPC—and how to implement them in C#.
Full C# Solution: REST, GraphQL, and gRPC
We will create a single solution with multiple projects, each demonstrating a different API style.
Step 1: Create a Solution
Open Visual Studio or CLI, then create a solution:
mkdir ApiDemoSolution
cd ApiDemoSolution
dotnet new sln -n ApiDemoSolution
This creates a solution called ApiDemoSolution.
Step 2: Create Shared Models Project
All APIs will use a shared project for models and services.
dotnet new classlib -n ApiDemo.Shared
![1]()
Step 3: Install EF Core for PostgreSQL
For all projects that use IProductRepository (Shared or REST API), install the necessary EF Core packages:
cd ApiDemo.Shared
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design
Step 2: Update Shared Project
Models/Product.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public class Product
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required(ErrorMessage = "Product name is required.")]
[MaxLength(100, ErrorMessage = "Product name cannot exceed 100 characters.")]
public string Name { get; set; } = string.Empty;
[MaxLength(500, ErrorMessage = "Description cannot exceed 500 characters.")]
public string Description { get; set; } = string.Empty;
[Range(0, double.MaxValue, ErrorMessage = "Price must be a non-negative value.")]
[Column(TypeName = "decimal(18,2)")] // Ensures proper precision in database
public decimal Price { get; set; }
[Range(0, int.MaxValue, ErrorMessage = "Stock cannot be negative.")]
public int Stock { get; set; }
// Domain Logic
public void ReduceStock(int quantity)
{
if (quantity > Stock)
throw new InvalidOperationException("Insufficient stock.");
Stock -= quantity;
}
}
Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace ApiDemo.Shared.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<Product> Products { get; set; }
}
Repositories/IProductRepository.cs
namespace ApiDemo.Shared.Repositories;
public interface IProductRepository
{
Task<Product?> GetByIdAsync(Guid id);
Task<IEnumerable<Product>> GetAllAsync();
Task AddAsync(Product product);
Task UpdateAsync(Product product);
Task DeleteAsync(Guid id);
}
Repositories/ProductRepository.cs
using ApiDemo.Shared.Data;
using Microsoft.EntityFrameworkCore;
namespace ApiDemo.Shared.Repositories;
public class ProductRepository : IProductRepository
{
private readonly AppDbContext _db;
public ProductRepository(AppDbContext db)
{
_db = db;
}
public async Task AddAsync(Product product)
{
product.Id = Guid.NewGuid();
await _db.Products.AddAsync(product);
await _db.SaveChangesAsync();
}
public async Task DeleteAsync(Guid id)
{
var product = await _db.Products.FindAsync(id);
if (product != null)
{
_db.Products.Remove(product);
await _db.SaveChangesAsync();
}
}
public async Task<IEnumerable<Product>> GetAllAsync() => await _db.Products.ToListAsync();
public async Task<Product?> GetByIdAsync(Guid id) => await _db.Products.FindAsync(id);
public async Task UpdateAsync(Product product)
{
_db.Products.Update(product);
await _db.SaveChangesAsync();
}
}
![2]()
Create REST API Project
dotnet new webapi -n ApiDemo.Rest
dotnet sln add ApiDemo.Rest/ApiDemo.Rest.csproj
![3]()
Install Swagger
dotnet add ECommerce.API package Swashbuckle.AspNetCore
![4]()
Update Program.cs
using ApiDemo.Shared.Data;
using ApiDemo.Shared.Repositories;
using Microsoft.EntityFrameworkCore;
using System;
var builder = WebApplication.CreateBuilder(args);
// Configure PostgreSQL
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly("ApiDemo.Rest")));
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
Upgrade All Packages for .net 10
dotnet add "ApiDemo.Shared/ApiDemo.Shared.csproj" package Microsoft.EntityFrameworkCore.Design --version 10.0.1
dotnet add "ApiDemo.Shared/ApiDemo.Shared.csproj" package Npgsql.EntityFrameworkCore.PostgreSQL --version 10.0.1
dotnet add "ApiDemo.Rest/ApiDemo.Rest.csproj" package Npgsql.EntityFrameworkCore.PostgreSQL --version 10.0.1
dotnet add "ApiDemo.Rest/ApiDemo.Rest.csproj" package Microsoft.EntityFrameworkCore.Design --version 10.0.1
Create Migrrations
dotnet ef migrations add InitialCreate -p ApiDemo.Rest -s ApiDemo.Rest
![5]()
Update Database
![6]()
Optional: Seed Database (Optional)
You can seed some data in Program.cs:
using var scope = app.Services.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<IProductRepository>();
if (!(await repo.GetAllAsync()).Any())
{
await repo.AddAsync(new Product { Name = "Test Product 1", Price = 10.5 });
await repo.AddAsync(new Product { Name = "Test Product 2", Price = 25.0 });
}
Create Controller for Rest
using ApiDemo.Shared.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ApiDemo.Rest.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
private readonly IProductRepository _repository;
public ProductController(IProductRepository repository)
{
_repository = repository;
}
// GET: api/product
[HttpGet]
public async Task<IActionResult> GetAll()
{
var products = await _repository.GetAllAsync();
return Ok(products);
}
// GET: api/product/{id}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id)
{
var product = await _repository.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
// POST: api/product
[HttpPost]
public async Task<IActionResult> Create([FromBody] Product product)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
await _repository.AddAsync(product);
return CreatedAtAction(nameof(GetById),
new { id = product.Id }, product);
}
// PUT: api/product/{id}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, [FromBody] Product product)
{
if (id != product.Id)
return BadRequest("ID mismatch.");
var existing = await _repository.GetByIdAsync(id);
if (existing == null)
return NotFound();
await _repository.UpdateAsync(product);
return NoContent();
}
// DELETE: api/product/{id}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id)
{
var existing = await _repository.GetByIdAsync(id);
if (existing == null)
return NotFound();
await _repository.DeleteAsync(id);
return NoContent();
}
}
}
Lets Test Rest API
![7]()
Create GraphQL API Project
dotnet new web -n ApiDemo.GraphQL
dotnet sln add ApiDemo.GraphQL/ApiDemo.GraphQL.csproj
dotnet add ApiDemo.GraphQL/ApiDemo.GraphQL.csproj reference ApiDemo.Shared/ApiDemo.Shared.csproj
![8]()
Add Package
dotnet add package HotChocolate.AspNetCore
![9]()
Create ProductQuery
Path : "/GraphQL/Products/ProductQuery.cs"
using ApiDemo.Shared.Repositories;
namespace ApiDemo.GraphQL.GraphQL.Products;
public class ProductQuery
{
public async Task<IEnumerable<Product>> GetProducts(
[Service] IProductRepository productRepository)
{
return await productRepository.GetAllAsync();
}
public async Task<Product?> GetProduct(
Guid id,
[Service] IProductRepository productRepository)
{
return await productRepository.GetByIdAsync(id);
}
}
Create ProductMutation
using ApiDemo.Shared.Repositories;
namespace ApiDemo.GraphQL.GraphQL.Products;
public class ProductMutation
{
public async Task<Product> CreateProduct(
Product input,
[Service] IProductRepository productRepository)
{
var product = new Product
{
Id = Guid.NewGuid(),
Name = input.Name,
Price = input.Price,
Description = input.Description
};
await productRepository.AddAsync(product);
return product;
}
public async Task<Product?> UpdateProduct(
Guid id,
Product input,
[Service] IProductRepository productRepository)
{
var existing = await productRepository.GetByIdAsync(id);
if (existing == null)
return null;
existing.Name = input.Name;
existing.Price = input.Price;
existing.Description = input.Description;
await productRepository.UpdateAsync(existing);
return existing;
}
public async Task<bool> DeleteProduct(
Guid id,
[Service] IProductRepository productRepository)
{
var existing = await productRepository.GetByIdAsync(id);
if (existing == null)
return false;
await productRepository.DeleteAsync(id);
return true;
}
}
Update Program File
using ApiDemo.GraphQL.GraphQL;
using ApiDemo.GraphQL.GraphQL.Products;
using ApiDemo.Shared.Data;
using ApiDemo.Shared.Repositories;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// Add Repository (register your concrete implementation)
builder.Services.AddScoped<IProductRepository, ProductRepository>();
// Add GraphQL
builder.Services
.AddGraphQLServer()
.AddQueryType<ProductQuery>()
.AddMutationType<ProductMutation>();
var app = builder.Build();
// Configure GraphQL endpoint
app.MapGraphQL("/graphql");
// Test endpoint
app.MapGet("/", () => "GraphQL API is running...");
app.Run();
Update Appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=ApiDemoDb;Username=postgres;Password=Admin123!"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Lets Test
![10]()
Access for Testing "https://localhost:7252/graphql/"
![11]()
Create gRPC API Project
dotnet new grpc -n ApiDemo.gRPC
dotnet sln add ApiDemo.gRPC/ApiDemo.gRPC.csproj
dotnet add ApiDemo.gRPC/ApiDemo.gRPC.csproj reference ApiDemo.Shared/ApiDemo.Shared.csproj
![12]()
Lets Create product.proto
product.proto is a Protocol Buffers (protobuf) schema file that defines the structure of data (messages) and the services or RPC methods your gRPC server provides.
syntax = "proto3";
option csharp_namespace = "GrpcProductService.Protos";
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
message ProductMessage {
string id = 1;
string name = 2;
string description = 3;
double price = 4;
int32 stock = 5;
}
message ProductRequest {
string id = 1;
}
message ProductList {
repeated ProductMessage products = 1;
}
message CreateProductRequest {
string name = 1;
string description = 2;
double price = 3;
int32 stock = 4;
}
message UpdateProductRequest {
string id = 1;
string name = 2;
string description = 3;
double price = 4;
int32 stock = 5;
}
service ProductService {
rpc GetProduct(ProductRequest) returns (ProductMessage);
rpc GetProducts(google.protobuf.Empty) returns (ProductList);
rpc CreateProduct(CreateProductRequest) returns (ProductMessage);
rpc UpdateProduct(UpdateProductRequest) returns (ProductMessage);
rpc DeleteProduct(ProductRequest) returns (google.protobuf.BoolValue);
}
Lets Create ProductService
A gRPC service is a collection of remote procedure calls (RPCs) defined in a .proto file that clients can invoke on the server, enabling strongly-typed and efficient communication between client and server.
using ApiDemo.Shared.Repositories;
using Grpc.Core;
using GrpcProductService.Protos;
namespace ApiDemo.gRPC.Services;
public class ProductServiceGrpc : ProductService.ProductServiceBase
{
private readonly IProductRepository _repository;
public ProductServiceGrpc(IProductRepository repository)
{
_repository = repository;
}
public override async Task<ProductMessage> GetProduct(ProductRequest request, ServerCallContext context)
{
var product = await _repository.GetByIdAsync(Guid.Parse(request.Id));
if (product == null) throw new RpcException(new Status(StatusCode.NotFound, "Product not found"));
return new ProductMessage
{
Id = product.Id.ToString(),
Name = product.Name,
Description = product.Description,
Price = (double)product.Price,
Stock = product.Stock
};
}
public override async Task<ProductList> GetProducts(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context)
{
var products = await _repository.GetAllAsync();
var response = new ProductList();
response.Products.AddRange(products.Select(p => new ProductMessage
{
Id = p.Id.ToString(),
Name = p.Name,
Description = p.Description,
Price = (double)p.Price,
Stock = p.Stock
}));
return response;
}
public override async Task<ProductMessage> CreateProduct(CreateProductRequest request, ServerCallContext context)
{
var product = new Product
{
Id = Guid.NewGuid(),
Name = request.Name,
Description = request.Description,
Price = (decimal)request.Price,
Stock = request.Stock
};
await _repository.AddAsync(product);
return new ProductMessage
{
Id = product.Id.ToString(),
Name = product.Name,
Description = product.Description,
Price = (double)product.Price,
Stock = product.Stock
};
}
public override async Task<ProductMessage> UpdateProduct(UpdateProductRequest request, ServerCallContext context)
{
var product = await _repository.GetByIdAsync(Guid.Parse(request.Id));
if (product == null) throw new RpcException(new Status(StatusCode.NotFound, "Product not found"));
product.Name = request.Name;
product.Description = request.Description;
product.Price = (decimal)request.Price;
product.Stock = request.Stock;
await _repository.UpdateAsync(product);
return new ProductMessage
{
Id = product.Id.ToString(),
Name = product.Name,
Description = product.Description,
Price = (double)product.Price,
Stock = product.Stock
};
}
public override async Task<Google.Protobuf.WellKnownTypes.BoolValue> DeleteProduct(ProductRequest request, ServerCallContext context)
{
var product = await _repository.GetByIdAsync(Guid.Parse(request.Id));
if (product == null) return new Google.Protobuf.WellKnownTypes.BoolValue { Value = false };
await _repository.DeleteAsync(product.Id);
return new Google.Protobuf.WellKnownTypes.BoolValue { Value = true };
}
}
Update Program file
using ApiDemo.gRPC.Services;
using ApiDemo.Shared.Data;
using ApiDemo.Shared.Repositories;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add EF Core DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// Add Repository
builder.Services.AddScoped<IProductRepository, ProductRepository>();
// Add gRPC
builder.Services.AddGrpc();
var app = builder.Build();
// Map gRPC service
app.MapGrpcService<ProductServiceGrpc>();
// Optional health endpoint
app.MapGet("/", () => "gRPC Product Service running...");
app.Run();
Update Appsettings.json as same we did in Rest,GraphQL
Lets Test
![13]()
Open Postman and click on New and select gPRC
![14]()
Add Url and Import .proto file
![15]()
Select Method from List to call
![16]()
Invoke the Method