← Назад к вопросам
Web API: Реализация Rate Limiting
2.2 Middle🔥 111 комментариев
#ASP.NET и Web API
Условие
Реализуйте middleware для ограничения количества запросов (rate limiting) в ASP.NET Core Web API.
Требования:
- Ограничение: 100 запросов в минуту на IP-адрес
- При превышении лимита возвращать HTTP 429 Too Many Requests
- Добавить заголовки X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
Интерфейс:
public class RateLimitingMiddleware { // TODO: реализовать }
public interface IRateLimitService { bool IsAllowed(string clientId); RateLimitInfo GetInfo(string clientId); }
Дополнительно:
- Как сделать rate limiting распределённым (для нескольких инстансов приложения)?
- Какие алгоритмы rate limiting существуют (Token Bucket, Sliding Window)?
Критерии оценки:
- Корректность подсчёта запросов
- Потокобезопасность
- Понимание распределённых систем (Redis)
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Структуры данных и моки
Перед написанием тестов нужно определить необходимые структуры:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
// Enum для статуса заказа
public enum OrderStatus
{
Pending = 0,
Paid = 1,
Shipped = 2,
Delivered = 3,
Cancelled = 4
}
// Модель заказа
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string CustomerEmail { get; set; }
public List<OrderItem> Items { get; set; } = new();
public OrderStatus Status { get; set; }
}
public class OrderItem
{
public int Id { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
// Результат обработки заказа
public class OrderResult
{
public bool Success { get; set; }
public int? OrderId { get; set; }
public string ErrorMessage { get; set; }
public static OrderResult Success(int orderId) =>
new() { Success = true, OrderId = orderId };
public static OrderResult PaymentFailed(string error) =>
new() { Success = false, ErrorMessage = error };
}
// Результат платежа
public class PaymentResult
{
public bool Success { get; set; }
public string Error { get; set; }
public string TransactionId { get; set; }
}
// Исключение для пустого заказа
public class InvalidOrderException : Exception
{
public InvalidOrderException(string message) : base(message) { }
}
// Интерфейсы зависимостей
public interface IOrderRepository
{
Task SaveAsync(Order order);
Task<Order> GetByIdAsync(int orderId);
}
public interface IPaymentGateway
{
Task<PaymentResult> ChargeAsync(int customerId, decimal amount);
}
public interface IEmailService
{
Task SendOrderConfirmationAsync(string email, int orderId);
Task SendOrderFailureAsync(string email, string reason);
}
// Сервис обработки заказов
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IPaymentGateway _paymentGateway;
private readonly IEmailService _emailService;
public OrderService(
IOrderRepository repository,
IPaymentGateway paymentGateway,
IEmailService emailService)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_paymentGateway = paymentGateway ?? throw new ArgumentNullException(nameof(paymentGateway));
_emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
}
public async Task<OrderResult> ProcessOrderAsync(Order order)
{
if (order.Items.Count == 0)
throw new InvalidOrderException("Order has no items");
var total = order.Items.Sum(i => i.Price * i.Quantity);
var paymentResult = await _paymentGateway.ChargeAsync(order.CustomerId, total);
if (!paymentResult.Success)
return OrderResult.PaymentFailed(paymentResult.Error);
order.Status = OrderStatus.Paid;
await _repository.SaveAsync(order);
await _emailService.SendOrderConfirmationAsync(order.CustomerEmail, order.Id);
return OrderResult.Success(order.Id);
}
}
Комплексные Unit-тесты (xUnit + Moq)
using Xunit;
using Moq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public class OrderServiceTests
{
// ===== Helper Method для Arrange =====
private OrderService CreateOrderService(
IOrderRepository repository = null,
IPaymentGateway paymentGateway = null,
IEmailService emailService = null)
{
repository ??= new Mock<IOrderRepository>().Object;
paymentGateway ??= new Mock<IPaymentGateway>().Object;
emailService ??= new Mock<IEmailService>().Object;
return new OrderService(repository, paymentGateway, emailService);
}
private Order CreateValidOrder(int id = 1, int customerId = 100, int itemCount = 2)
{
var order = new Order
{
Id = id,
CustomerId = customerId,
CustomerEmail = "customer@example.com",
Status = OrderStatus.Pending,
Items = new List<OrderItem>()
};
for (int i = 0; i < itemCount; i++)
{
order.Items.Add(new OrderItem
{
Id = i + 1,
Price = 50m,
Quantity = 1
});
}
return order;
}
// ===== TEST 1: Успешная обработка заказа =====
[Fact]
public async Task ProcessOrderAsync_WithValidOrder_ReturnsSuccess()
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var mockPaymentGateway = new Mock<IPaymentGateway>();
var mockEmailService = new Mock<IEmailService>();
var paymentResult = new PaymentResult { Success = true, TransactionId = "TXN123" };
mockPaymentGateway
.Setup(pg => pg.ChargeAsync(It.IsAny<int>(), It.IsAny<decimal>()))
.ReturnsAsync(paymentResult);
var service = new OrderService(mockRepository.Object, mockPaymentGateway.Object, mockEmailService.Object);
var order = CreateValidOrder(id: 1, customerId: 100);
// Act
var result = await service.ProcessOrderAsync(order);
// Assert
Assert.True(result.Success);
Assert.Equal(1, result.OrderId);
Assert.Null(result.ErrorMessage);
// Проверяем, что зависимости были вызваны правильно
mockPaymentGateway.Verify(
pg => pg.ChargeAsync(100, 100m),
Times.Once);
mockRepository.Verify(
r => r.SaveAsync(It.Is<Order>(o => o.Status == OrderStatus.Paid)),
Times.Once);
mockEmailService.Verify(
es => es.SendOrderConfirmationAsync("customer@example.com", 1),
Times.Once);
}
// ===== TEST 2: Заказ без товаров выбрасывает исключение =====
[Fact]
public async Task ProcessOrderAsync_WithEmptyItems_ThrowsInvalidOrderException()
{
// Arrange
var service = CreateOrderService();
var order = new Order
{
Id = 1,
CustomerId = 100,
CustomerEmail = "customer@example.com",
Items = new List<OrderItem>()
};
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOrderException>(
() => service.ProcessOrderAsync(order));
Assert.Equal("Order has no items", exception.Message);
}
// ===== TEST 3: Ошибка платежа =====
[Fact]
public async Task ProcessOrderAsync_WhenPaymentFails_ReturnsFailureResult()
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var mockPaymentGateway = new Mock<IPaymentGateway>();
var mockEmailService = new Mock<IEmailService>();
var paymentResult = new PaymentResult { Success = false, Error = "Card declined" };
mockPaymentGateway
.Setup(pg => pg.ChargeAsync(It.IsAny<int>(), It.IsAny<decimal>()))
.ReturnsAsync(paymentResult);
var service = new OrderService(mockRepository.Object, mockPaymentGateway.Object, mockEmailService.Object);
var order = CreateValidOrder();
// Act
var result = await service.ProcessOrderAsync(order);
// Assert
Assert.False(result.Success);
Assert.Null(result.OrderId);
Assert.Equal("Card declined", result.ErrorMessage);
// Email не должен быть отправлен
mockEmailService.Verify(
es => es.SendOrderConfirmationAsync(It.IsAny<string>(), It.IsAny<int>()),
Times.Never);
}
// ===== TEST 4: Email отправляется только после успешной оплаты =====
[Fact]
public async Task ProcessOrderAsync_SendsEmailOnlyAfterSuccessfulPayment()
{
// Arrange
var callOrder = new List<string>();
var mockRepository = new Mock<IOrderRepository>();
mockRepository
.Setup(r => r.SaveAsync(It.IsAny<Order>()))
.Callback(() => callOrder.Add("SaveAsync"))
.Returns(Task.CompletedTask);
var mockPaymentGateway = new Mock<IPaymentGateway>();
mockPaymentGateway
.Setup(pg => pg.ChargeAsync(It.IsAny<int>(), It.IsAny<decimal>()))
.Callback(() => callOrder.Add("ChargeAsync"))
.ReturnsAsync(new PaymentResult { Success = true, TransactionId = "TXN123" });
var mockEmailService = new Mock<IEmailService>();
mockEmailService
.Setup(es => es.SendOrderConfirmationAsync(It.IsAny<string>(), It.IsAny<int>()))
.Callback(() => callOrder.Add("SendEmailAsync"))
.Returns(Task.CompletedTask);
var service = new OrderService(mockRepository.Object, mockPaymentGateway.Object, mockEmailService.Object);
var order = CreateValidOrder();
// Act
await service.ProcessOrderAsync(order);
// Assert
Assert.Equal(new[] { "ChargeAsync", "SaveAsync", "SendEmailAsync" }, callOrder);
}
// ===== TEST 5: Email НЕ отправляется при ошибке платежа =====
[Fact]
public async Task ProcessOrderAsync_DoesNotSendEmail_WhenPaymentFails()
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var mockPaymentGateway = new Mock<IPaymentGateway>();
var mockEmailService = new Mock<IEmailService>();
mockPaymentGateway
.Setup(pg => pg.ChargeAsync(It.IsAny<int>(), It.IsAny<decimal>()))
.ReturnsAsync(new PaymentResult { Success = false, Error = "Insufficient funds" });
var service = new OrderService(mockRepository.Object, mockPaymentGateway.Object, mockEmailService.Object);
var order = CreateValidOrder();
// Act
await service.ProcessOrderAsync(order);
// Assert
mockEmailService.Verify(
es => es.SendOrderConfirmationAsync(It.IsAny<string>(), It.IsAny<int>()),
Times.Never);
}
// ===== TEST 6: Корректно рассчитывается сумма =====
[Theory]
[InlineData(1, 50, 50)]
[InlineData(2, 100, 200)]
[InlineData(3, 25, 75)]
public async Task ProcessOrderAsync_CalculatesTotalCorrectly(int quantity, decimal price, decimal expectedTotal)
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var mockPaymentGateway = new Mock<IPaymentGateway>();
var mockEmailService = new Mock<IEmailService>();
mockPaymentGateway
.Setup(pg => pg.ChargeAsync(It.IsAny<int>(), It.IsAny<decimal>()))
.ReturnsAsync(new PaymentResult { Success = true });
var service = new OrderService(mockRepository.Object, mockPaymentGateway.Object, mockEmailService.Object);
var order = new Order
{
Id = 1,
CustomerId = 100,
CustomerEmail = "test@example.com",
Items = new List<OrderItem>
{
new OrderItem { Id = 1, Price = price, Quantity = quantity }
}
};
// Act
await service.ProcessOrderAsync(order);
// Assert
mockPaymentGateway.Verify(
pg => pg.ChargeAsync(100, expectedTotal),
Times.Once);
}
}
Выводы
✅ Best Practices:
- Arrange-Act-Assert — чёткая структура теста
- Один тест = один сценарий — не объединяй проверки
- Моки для зависимостей — изолируй тестируемый код
- Verify для проверки — убедись, что методы вызваны правильно
- Theory tests — для множественных данных
- Helper методы — для создания тестовых объектов
✅ Покрытие основных сценариев:
- Успешная обработка
- Ошибки валидации
- Ошибки платежа
- Проверка порядка вызовов
- Расчёты сумм