الخدمات المصغّرة (Microservices) مع .NET: البنية والأنماط وأفضل الممارسات
دليل شامل لتصميم بنى الخدمات المصغّرة المتينة وتطويرها ونشرها باستخدام .NET وASP.NET Core.
InSkillCoach
الخدمات المصغّرة (Microservices) مع .NET: البنية والأنماط وأفضل الممارسات
مدخل إلى الخدمات المصغّرة
تُعدّ بنية الخدمات المصغّرة (Microservices) نهجًا في تطوير البرمجيات يقوم على هيكلة التطبيق كمجموعة من الخدمات ضعيفة الترابط والقابلة للنشر بشكل مستقل. وعلى عكس البنى المتجانسة (Monolithic) التقليدية، تتيح الخدمات المصغّرة مرونة أكبر وقابلية أعلى للتوسّع ومتانة أفضل للتطبيقات.
لماذا نعتمد الخدمات المصغّرة مع .NET؟
- نشر مستقل: يمكن لكل خدمة أن تتطور بمعزل عن غيرها
- توسّع موجَّه: التوسّع الأفقي (Scaling) للخدمات التي تحتاج إليه فقط
- متانة محسّنة: فشل إحدى الخدمات لا يؤدي إلى توقف النظام بالكامل
- فرق مستقلة: تستطيع الفرق تطوير خدماتها واختبارها ونشرها بشكل مستقل
- قابلية التكيّف التقني: إمكانية استخدام تقنيات مختلفة لخدمات مختلفة
- تكامل مثالي مع منظومة .NET وASP.NET Core
أساسيات الخدمات المصغّرة مع .NET
البنية الأساسية
تتكوّن بنية الخدمات المصغّرة النموذجية في .NET عادةً مما يلي:
- API Gateway: نقطة دخول واحدة للعملاء (غالبًا باستخدام Ocelot)
- الخدمات الوظيفية: خدمات ASP.NET Core التي تنفّذ منطق الأعمال
- خدمة الاكتشاف (Service Discovery): لتحديد مواقع نُسَخ الخدمات (Consul، Eureka)
- قاعدة بيانات لكل خدمة: تمتلك كل خدمة قاعدة بياناتها الخاصة
- Message Broker: للتواصل غير المتزامن (RabbitMQ، Azure Service Bus)
- خدمات الهوية: للمصادقة والتفويض (IdentityServer)
- الحاويات والتنسيق (Orchestration): Docker وKubernetes للنشر
إنشاء خدمة مصغّرة بواسطة ASP.NET Core
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Ajouter les services nécessaires
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<ProductContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("ProductDb")));
builder.Services.AddScoped<IProductRepository, ProductRepository>();
var app = builder.Build();
// Configurer le pipeline de requêtes HTTP
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
أنماط التصميم للخدمات المصغّرة
نمط CQRS (Command Query Responsibility Segregation)
// Commande
public class CreateProductCommand : IRequest<int>
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
// Gestionnaire de commande
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, int>
{
private readonly ProductContext _context;
public CreateProductCommandHandler(ProductContext context)
{
_context = context;
}
public async Task<int> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
var product = new Product
{
Name = request.Name,
Description = request.Description,
Price = request.Price
};
_context.Products.Add(product);
await _context.SaveChangesAsync(cancellationToken);
return product.Id;
}
}
// Requête
public class GetProductQuery : IRequest<ProductDto>
{
public int Id { get; set; }
}
// Gestionnaire de requête
public class GetProductQueryHandler : IRequestHandler<GetProductQuery, ProductDto>
{
private readonly ProductContext _context;
public GetProductQueryHandler(ProductContext context)
{
_context = context;
}
public async Task<ProductDto> Handle(GetProductQuery request, CancellationToken cancellationToken)
{
var product = await _context.Products.FindAsync(request.Id);
return new ProductDto
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price
};
}
}
نمط Saga للمعاملات الموزّعة
// Définition d'une saga pour le processus de commande
public class OrderSaga :
IAmStartedByMessages<CreateOrderCommand>,
IHandleMessages<PaymentProcessedEvent>,
IHandleMessages<InventoryReservedEvent>,
IHandleMessages<ShippingPreparedEvent>
{
private readonly IBus _bus;
private readonly IOrderRepository _orderRepository;
public OrderSaga(IBus bus, IOrderRepository orderRepository)
{
_bus = bus;
_orderRepository = orderRepository;
}
public async Task Handle(CreateOrderCommand message, IMessageHandlerContext context)
{
// Créer la commande
var order = new Order(message.OrderId, message.CustomerId, message.Products);
await _orderRepository.SaveAsync(order);
// Déclencher le processus de paiement
await _bus.Publish(new ProcessPaymentCommand
{
OrderId = message.OrderId,
Amount = order.TotalAmount,
PaymentDetails = message.PaymentDetails
});
}
public async Task Handle(PaymentProcessedEvent message, IMessageHandlerContext context)
{
// Mettre à jour le statut de la commande
var order = await _orderRepository.GetByIdAsync(message.OrderId);
order.MarkPaymentComplete();
await _orderRepository.UpdateAsync(order);
// Réserver l'inventaire
await _bus.Publish(new ReserveInventoryCommand
{
OrderId = message.OrderId,
Products = order.Products
});
}
// Gérer les autres événements...
// Gérer les compensations en cas d'échec
private async Task CompensateOrder(string orderId, string reason)
{
var order = await _orderRepository.GetByIdAsync(orderId);
order.Cancel(reason);
await _orderRepository.UpdateAsync(order);
// Émettre des événements de compensation (remboursement, libération d'inventaire, etc.)
}
}
API Gateway باستخدام Ocelot
// ocelot.json
{
"Routes": [
{
"DownstreamPathTemplate": "/api/products/{everything}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "product-service",
"Port": 443
}
],
"UpstreamPathTemplate": "/products/{everything}",
"UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ]
},
{
"DownstreamPathTemplate": "/api/orders/{everything}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "order-service",
"Port": 443
}
],
"UpstreamPathTemplate": "/orders/{everything}",
"UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ]
}
],
"GlobalConfiguration": {
"BaseUrl": "https://api.example.com"
}
}
// Program.cs pour API Gateway
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
builder.Services.AddOcelot(builder.Configuration);
var app = builder.Build();
app.UseOcelot().Wait();
app.Run();
التواصل بين الخدمات المصغّرة
التواصل المتزامن عبر HTTP/REST
// Service HTTP client
public class ProductService : IProductService
{
private readonly HttpClient _httpClient;
public ProductService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<ProductDto> GetProductAsync(int id)
{
var response = await _httpClient.GetAsync($"/api/products/{id}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ProductDto>();
}
}
// Configuration
services.AddHttpClient<IProductService, ProductService>(client =>
{
client.BaseAddress = new Uri("https://product-service");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
// Stratégies de résilience
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}
التواصل غير المتزامن عبر الأحداث
// Définition d'un événement
public class OrderCreatedEvent : IntegrationEvent
{
public int OrderId { get; set; }
public int CustomerId { get; set; }
public List<OrderItem> Items { get; set; }
public DateTime OrderDate { get; set; }
}
// Publication d'un événement avec MassTransit et RabbitMQ
public class OrderController : ControllerBase
{
private readonly IOrderRepository _orderRepository;
private readonly IPublishEndpoint _publishEndpoint;
public OrderController(IOrderRepository orderRepository, IPublishEndpoint publishEndpoint)
{
_orderRepository = orderRepository;
_publishEndpoint = publishEndpoint;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderCommand command)
{
// Logique de création de commande
var order = new Order(command);
await _orderRepository.AddAsync(order);
// Publication de l'événement
await _publishEndpoint.Publish(new OrderCreatedEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId,
Items = order.Items,
OrderDate = order.OrderDate
});
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
}
// Configuration de MassTransit
services.AddMassTransit(x =>
{
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host("rabbitmq://localhost");
cfg.ConfigureEndpoints(context);
});
});
استمرارية البيانات (Persistance)
قاعدة بيانات لكل خدمة
// DbContext pour le service Produit
public class ProductContext : DbContext
{
public ProductContext(DbContextOptions<ProductContext> options) : base(options)
{
}
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().ToTable("Products");
modelBuilder.Entity<Product>().HasKey(p => p.Id);
modelBuilder.Entity<Product>().Property(p => p.Name).IsRequired().HasMaxLength(100);
modelBuilder.Entity<Product>().Property(p => p.Price).HasColumnType("decimal(18,2)");
}
}
// Configuration dans Program.cs
services.AddDbContext<ProductContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("ProductDb")));
نمط Repository
// Interface du Repository
public interface IProductRepository
{
Task<Product> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
Task<IEnumerable<Product>> GetByNameAsync(string name);
Task AddAsync(Product product);
Task UpdateAsync(Product product);
Task DeleteAsync(int id);
}
// Implémentation du Repository
public class ProductRepository : IProductRepository
{
private readonly ProductContext _context;
public ProductRepository(ProductContext context)
{
_context = context;
}
public async Task<Product> GetByIdAsync(int id)
{
return await _context.Products
.FindAsync(id);
}
public async Task<IEnumerable<Product>> GetAllAsync()
{
return await _context.Products
.ToListAsync();
}
public async Task<IEnumerable<Product>> GetByNameAsync(string name)
{
return await _context.Products
.Where(p => p.Name.Contains(name))
.ToListAsync();
}
public async Task AddAsync(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(Product product)
{
_context.Entry(product).State = EntityState.Modified;
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var product = await _context.Products.FindAsync(id);
if (product != null)
{
_context.Products.Remove(product);
await _context.SaveChangesAsync();
}
}
}
النشر والتنسيق (Orchestration)
الحاويات مع Docker
# Dockerfile pour un microservice .NET
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["ProductService.csproj", "./"]
RUN dotnet restore "ProductService.csproj"
COPY . .
RUN dotnet build "ProductService.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "ProductService.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ProductService.dll"]
التنسيق باستخدام Kubernetes
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-service
labels:
app: product-service
spec:
replicas: 3
selector:
matchLabels:
app: product-service
template:
metadata:
labels:
app: product-service
spec:
containers:
- name: product-service
image: acr.io/myorg/product-service:latest
ports:
- containerPort: 80
env:
- name: ConnectionStrings__ProductDb
valueFrom:
secretKeyRef:
name: product-service-secrets
key: db-connection-string
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "256Mi"
livenessProbe:
httpGet:
path: /health/live
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 80
initialDelaySeconds: 15
periodSeconds: 5
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: product-service
spec:
selector:
app: product-service
ports:
- port: 80
targetPort: 80
type: ClusterIP
قابلية الرصد والمراقبة
فحوصات الصحة (Health Checks)
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Ajouter les health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<ProductContext>("database")
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddRabbitMQ($"amqp://guest:guest@rabbitmq:5672/", name: "rabbitmq");
var app = builder.Build();
// Configurer les endpoints de health check
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => true
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.Run();
التتبع الموزّع مع OpenTelemetry
// Program.cs
services.AddOpenTelemetryTracing(builder =>
{
builder
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("ProductService"))
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddJaegerExporter(options =>
{
options.AgentHost = "jaeger";
options.AgentPort = 6831;
});
});
أمان الخدمات المصغّرة
المصادقة باستخدام IdentityServer
// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://identity-service";
options.Audience = "product-api";
options.RequireHttpsMetadata = false;
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ReadProducts", policy =>
policy.RequireClaim("scope", "product.read"));
options.AddPolicy("WriteProducts", policy =>
policy.RequireClaim("scope", "product.write"));
});
// Controller
[Authorize(Policy = "ReadProducts")]
[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetProduct(int id)
{
// ...
}
[Authorize(Policy = "WriteProducts")]
[HttpPost]
public async Task<ActionResult<ProductDto>> CreateProduct(CreateProductCommand command)
{
// ...
}
تأمين واجهات API باستخدام تحديد معدل الطلبات (Rate Limiting)
// Program.cs
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
QueueLimit = 0,
Window = TimeSpan.FromMinutes(1)
});
});
options.OnRejected = async (context, token) =>
{
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.", token);
};
});
var app = builder.Build();
app.UseRateLimiter();
اختبارات الخدمات المصغّرة
اختبارات الوحدة
// ProductServiceTests.cs
public class ProductServiceTests
{
[Fact]
public async Task GetProductAsync_ExistingProduct_ReturnsProduct()
{
// Arrange
var mockRepository = new Mock<IProductRepository>();
mockRepository.Setup(repo => repo.GetByIdAsync(1))
.ReturnsAsync(new Product { Id = 1, Name = "Test Product", Price = 10.99m });
var service = new ProductService(mockRepository.Object);
// Act
var result = await service.GetProductAsync(1);
// Assert
Assert.NotNull(result);
Assert.Equal(1, result.Id);
Assert.Equal("Test Product", result.Name);
}
}
اختبارات التكامل
// ProductControllerIntegrationTests.cs
public class ProductControllerIntegrationTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ProductControllerIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// استبدال DbContext بقاعدة بيانات في الذاكرة
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<ProductContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
services.AddDbContext<ProductContext>(options =>
{
options.UseInMemoryDatabase("TestProductDb");
});
// إضافة بيانات اختبار
var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ProductContext>();
db.Products.Add(new Product { Id = 1, Name = "Test Product", Price = 10.99m });
db.SaveChanges();
});
});
}
[Fact]
public async Task GetProducts_ReturnsSuccessAndCorrectContentType()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/products");
// Assert
response.EnsureSuccessStatusCode();
Assert.Equal("application/json; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
أفضل الممارسات للخدمات المصغّرة في .NET
-
فصل المسؤوليات
- يجب أن تكون لكل خدمة مصغّرة مسؤولية واحدة ومحددة بوضوح
-
إدارة الإعدادات
- استخدم مزوّدي إعدادات خارجيين (Azure App Configuration وKubernetes ConfigMaps/Secrets)
-
المرونة والتعافي (Résilience)
- طبّق أنماطًا مثل Circuit Breaker وRetry وTimeout باستخدام Polly
-
إصدارات واجهات API (Versioning)
- استخدم إصدارات صريحة في المسارات أو في ترويسات HTTP
-
إمكانية التتبع
- استخدم معرّفات الارتباط (Correlation IDs) لتتبع الطلبات عبر الخدمات
-
التوثيق
- استخدم Swagger/OpenAPI لتوثيق واجهات API الخاصة بك
-
العمارة حسب المجال
- نظّم خدماتك المصغّرة حول مجالات الأعمال (DDD)
-
البنية التحتية كتعليمات برمجية (Infrastructure as Code)
- أتمتة النشر والإعداد باستخدام أدوات مثل Terraform أو Pulumi
مصادر للتعمق
- Microsoft Learn : Microservices avec .NET
- eShopOnContainers : Exemple de référence
- Steeltoe : Framework pour les microservices .NET
- Dapr : Runtime distribué pour les microservices
- كتاب : “Building Microservices with ASP.NET Core” لمؤلفه Kevin Hoffman
الخلاصة
توفّر عمارة الخدمات المصغّرة مزايا عديدة للتطبيقات الحديثة، لكنها تُضيف أيضًا تعقيدًا إضافيًا. وبفضل .NET ومنظومته الغنية، تمتلك جميع الأدوات اللازمة لتصميم خدمات مصغّرة قوية وقابلة للتوسع وسهلة الصيانة، ثم تطويرها ونشرها. وباتباع الأنماط وأفضل الممارسات المعروضة في هذا الدليل، ستتمكن من بناء عمارات خدمات مصغّرة فعّالة تلبّي احتياجات مؤسستك.
نبذة عن InSkillCoach
خبير في التدريب والتقنيات
مدرّب متخصص في التقنيات المتقدمة والذكاء الاصطناعي، تدعمه شركة GNeurone Inc.
الشهادات:
- AWS Certified Solutions Architect – Professional
- شهادات Google Cloud
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
التعليقات
التعليقات مدعومة عبر GitHub Discussions
سجّل الدخول باستخدام GitHub للمشاركة في النقاش