Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions RateLimiter.DataStore/Entities/RequestLog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace RateLimiter.DataStore.Entities
{
public class RequestLog
{
public int RequestLogId { get; set; }

public string ClientToken { get; set; }

public string ResourceName { get; set; }

public string TimeStampString { get; set; } = string.Empty;

public int AccessCounts { get; set; } = 1;

public DateTime CreatedTime { get; set; } = DateTime.UtcNow;

public DateTime? UpdatedTime { get; set; } = null;
}
}
25 changes: 25 additions & 0 deletions RateLimiter.DataStore/InMemoryDataContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using RateLimiter.DataStore.Entities;
using RateLimiter.DataStore.Interfaces;

namespace RateLimiter.DataStore
{
/// <summary>
/// In-Memory data context implementation
/// </summary>
public class InMemoryDataContext : DbContext, IDataContext
{
public DbSet<RequestLog> RequestLogs { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// In-Memory database used for simplicity
options.UseInMemoryDatabase("RateLimiterDb");
}

void IDataContext.SaveChanges()
{
base.SaveChanges();
}
}
}
12 changes: 12 additions & 0 deletions RateLimiter.DataStore/Interfaces/IDataContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.EntityFrameworkCore;
using RateLimiter.DataStore.Entities;

namespace RateLimiter.DataStore.Interfaces
{
public interface IDataContext
{
public DbSet<RequestLog> RequestLogs { get; set; }

void SaveChanges();
}
}
23 changes: 23 additions & 0 deletions RateLimiter.DataStore/Interfaces/IRateLimitRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using RateLimiter.DataStore.Entities;

namespace RateLimiter.DataStore.Interfaces
{
public interface IRateLimitRepository
{
Task AddRequestLog(RequestLog requestLog);

Task DeleteExpiredLogs(int pastHours);

Task UpdateRequestLogCounts(RequestLog requestLog);

Task<RequestLog?> GetLogByTimeStamp(
string token,
string resourceName,
string timeStamp);

Task<IList<RequestLog>> GetLogsWithinTimeSpan(
string token,
string resourceName,
TimeSpan timeSpan);
}
}
64 changes: 64 additions & 0 deletions RateLimiter.DataStore/RateLimitRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using RateLimiter.DataStore.Entities;
using RateLimiter.DataStore.Interfaces;

namespace RateLimiter.DataStore
{
public class RateLimitRepository : IRateLimitRepository
{
private IDataContext _context;

public RateLimitRepository(IDataContext dataContext)
{
_context = dataContext;
}

public async Task AddRequestLog(RequestLog requestLog)
{
_context.RequestLogs.Add(requestLog);
_context.SaveChanges();
}

/// <summary>
/// Call this to save any changes made in Entity
/// </summary>
public async Task UpdateRequestLogCounts(RequestLog requestLog)
{
requestLog.AccessCounts += 1;
requestLog.UpdatedTime = DateTime.UtcNow;
_context.SaveChanges();
}

/// <summary>
/// Call this to remove older logs
/// </summary>
/// <param name="pastHours"></param>
public async Task DeleteExpiredLogs(int pastHours)
{
var oldLogs = _context.RequestLogs
.Where(x => x.CreatedTime < DateTime.Now.AddHours(pastHours));

_context.RequestLogs.RemoveRange(oldLogs);
_context.SaveChanges();
}

public async Task<RequestLog?> GetLogByTimeStamp(string token, string resourceName, string timeStamp)
{
return _context.RequestLogs
.Where(x =>
x.ClientToken == token &&
x.ResourceName == resourceName &&
x.TimeStampString == timeStamp)
.FirstOrDefault();
}

public async Task<IList<RequestLog>> GetLogsWithinTimeSpan(string token, string resourceName, TimeSpan timeSpan)
{
return _context.RequestLogs
.Where(x =>
x.ClientToken == token &&
x.ResourceName == resourceName.Trim() &&
x.CreatedTime > DateTime.UtcNow.AddTicks(-timeSpan.Ticks))
.ToList();
}
}
}
14 changes: 14 additions & 0 deletions RateLimiter.DataStore/RateLimiter.DataStore.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.20" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
</ItemGroup>

</Project>
9 changes: 9 additions & 0 deletions RateLimiter.Services/Enums/RuleType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace RateLimiter.Services.Enums
{
public enum RuleType
{
USA = 0,
Europe = 1,
Mixed = 2
}
}
47 changes: 47 additions & 0 deletions RateLimiter.Services/EuropeRuleService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using RateLimiter.DataStore.Entities;
using RateLimiter.DataStore.Interfaces;
using RateLimiter.Services.Interfaces;
using RateLimiter.Services.Rule;

namespace RateLimiter.Services
{
public class EuropeRuleService : IRuleService
{
private readonly IRateLimitRepository _rateLimitRepository;
private readonly RuleOptions _ruleOptions;

public EuropeRuleService(RuleOptions ruleOptions, IRateLimitRepository rateLimitRepository)
{
_rateLimitRepository = rateLimitRepository;
_ruleOptions = ruleOptions;
}

public async Task DeleteOldRequestLogs(int pastHours)
{
await _rateLimitRepository.DeleteExpiredLogs(pastHours);
}

public async Task<bool> HasRateLimitExceeded(string token, string resourceName)
{
var currentLogs = await _rateLimitRepository.GetLogsWithinTimeSpan(
token,
resourceName,
_ruleOptions.TimeSpan);

if (currentLogs.Count > _ruleOptions.MaxCounts ||
currentLogs.Sum(x=>x.AccessCounts) > _ruleOptions.MaxCounts)
{
return true;
}

_ = _rateLimitRepository.AddRequestLog(new RequestLog()
{
ClientToken = token,
ResourceName = resourceName,
TimeStampString = DateTime.UtcNow.ToString(_ruleOptions.TimeStampFormat)
});

return false;
}
}
}
37 changes: 37 additions & 0 deletions RateLimiter.Services/Factory/ServiceFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using RateLimiter.DataStore;
using RateLimiter.Services.Interfaces;
using RateLimiter.Services.Rule;

namespace RateLimiter.Services.Factory
{
public static class ServiceFactory
{
public static IRuleService CreateRuleService(RuleOptions ruleOptions)
{
var reposiotry = new RateLimitRepository(new InMemoryDataContext());

if (ruleOptions.RuleType == Enums.RuleType.USA)
{
return new USARuleService(
ruleOptions,
reposiotry);
}
else if (ruleOptions.RuleType == Enums.RuleType.Europe)
{
return new EuropeRuleService(
ruleOptions,
reposiotry);
}
else if (ruleOptions.RuleType == Enums.RuleType.Mixed)
{
return new MixedRuleService(
ruleOptions,
reposiotry);
}
else
{
throw new Exception("Rule not supported.");
}
}
}
}
9 changes: 9 additions & 0 deletions RateLimiter.Services/Interfaces/IRuleService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace RateLimiter.Services.Interfaces
{
public interface IRuleService
{
Task DeleteOldRequestLogs(int pastHours);

Task<bool> HasRateLimitExceeded(string token, string resourceName);
}
}
66 changes: 66 additions & 0 deletions RateLimiter.Services/MixedRuleService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using RateLimiter.DataStore.Entities;
using RateLimiter.DataStore.Interfaces;
using RateLimiter.Services.Interfaces;
using RateLimiter.Services.Rule;

namespace RateLimiter.Services
{
public class MixedRuleService : IRuleService
{
private readonly IRateLimitRepository _rateLimitRepository;
private readonly RuleOptions _ruleOptions;

public MixedRuleService(RuleOptions ruleOptions, IRateLimitRepository rateLimitRepository)
{
_rateLimitRepository = rateLimitRepository;
_ruleOptions = ruleOptions;
}

public async Task DeleteOldRequestLogs(int pastHours)
{
await _rateLimitRepository.DeleteExpiredLogs(pastHours);
}

public async Task<bool> HasRateLimitExceeded(string token, string resourceName)
{
var timeStampString = DateTime.UtcNow.ToString(_ruleOptions.TimeStampFormat);

var currentLog = await _rateLimitRepository.GetLogByTimeStamp(
token,
resourceName,
timeStampString);

if (currentLog is { })
{
if (currentLog.AccessCounts >= _ruleOptions.MaxCounts)
{
return true;
}

_ = _rateLimitRepository.UpdateRequestLogCounts(currentLog);
}
else
{
var currentLogs = await _rateLimitRepository.GetLogsWithinTimeSpan(
token,
resourceName,
_ruleOptions.TimeSpan);

if (currentLogs.Count > _ruleOptions.MaxCounts ||
currentLogs.Sum(x => x.AccessCounts) > _ruleOptions.MaxCounts)
{
return true;
}
}

_ = _rateLimitRepository.AddRequestLog(new RequestLog()
{
ClientToken = token,
ResourceName = resourceName,
TimeStampString = timeStampString
});

return false;
}
}
}
13 changes: 13 additions & 0 deletions RateLimiter.Services/RateLimiter.Services.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\RateLimiter.DataStore\RateLimiter.DataStore.csproj" />
</ItemGroup>

</Project>
28 changes: 28 additions & 0 deletions RateLimiter.Services/Rule/RuleOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using RateLimiter.Services.Enums;

namespace RateLimiter.Services.Rule
{
public class RuleOptions
{
/// <summary>
/// Default to 10
/// </summary>
public int MaxCounts { get; set; } = 10;

/// <summary>
/// Default to year month day hour and minutes
/// </summary>
public string TimeStampFormat { get; set; } = "yyyyMMddHHmm";

/// <summary>
/// Default to 0
/// </summary>
public TimeSpan TimeSpan { get; set; } = TimeSpan.FromTicks(10);


/// <summary>
/// Default to USA
/// </summary>
public RuleType RuleType { get; set; } = RuleType.USA;
}
}
Loading