Skip to content

Commit

Permalink
Merge pull request #8 from Yves57/dev
Browse files Browse the repository at this point in the history
First alpha
  • Loading branch information
Yves57 committed Apr 21, 2018
2 parents 953a748 + d40fcf7 commit df18c7e
Show file tree
Hide file tree
Showing 85 changed files with 2,856 additions and 130 deletions.
74 changes: 73 additions & 1 deletion AspNetCoreExt.Qos.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,29 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreExt.Qos.Quota", "
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7CCB8C14-0093-4440-A278-4D9CAAA22C67}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreExt.Qos.Tests", "test\AspNetCoreExt.Qos.Tests\AspNetCoreExt.Qos.Tests.csproj", "{9E3DC226-9AB5-407B-93A8-D123C89F562A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreExt.Qos.Tests", "test\AspNetCoreExt.Qos.Tests\AspNetCoreExt.Qos.Tests.csproj", "{9E3DC226-9AB5-407B-93A8-D123C89F562A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreExt.Qos.ExpressionPolicyKeyComputer.Tests", "test\AspNetCoreExt.Qos.ExpressionPolicyKeyComputer.Tests\AspNetCoreExt.Qos.ExpressionPolicyKeyComputer.Tests.csproj", "{33E3AA2E-F86B-42F7-BB4F-3DC9687758ED}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreExt.Qos.Concurrency.Tests", "test\AspNetCoreExt.Qos.Concurrency.Tests\AspNetCoreExt.Qos.Concurrency.Tests.csproj", "{9CBDC2A9-27BC-4E52-8E56-38EA27C91A16}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreExt.Qos.Quota.Tests", "test\AspNetCoreExt.Qos.Quota.Tests\AspNetCoreExt.Qos.Quota.Tests.csproj", "{EA3C4392-9033-4C00-99A9-1AE69A3498CE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreExt.Qos.RateLimit.Tests", "test\AspNetCoreExt.Qos.RateLimit.Tests\AspNetCoreExt.Qos.RateLimit.Tests.csproj", "{9BB335B0-798E-4EF2-8181-A50143BBEA77}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreExt.Qos.Vip.Tests", "test\AspNetCoreExt.Qos.Vip.Tests\AspNetCoreExt.Qos.Vip.Tests.csproj", "{4286408E-A56A-4BDB-9FFD-29ECB9B12086}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{495070EF-AC12-446F-B8D9-1E9EBD062F4C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QosSandbox", "samples\QosSandbox\QosSandbox.csproj", "{B933BAC7-0B75-4895-BADE-7EF9E1A8A6DB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreExt.Qos.Mvc.Tests", "test\AspNetCoreExt.Qos.Mvc.Tests\AspNetCoreExt.Qos.Mvc.Tests.csproj", "{51EAB754-AC3D-44CF-A329-A6D86BF550F4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreExt.Qos.Abstractions.Tests", "test\AspNetCoreExt.Qos.Abstractions.Tests\AspNetCoreExt.Qos.Abstractions.Tests.csproj", "{6685F90A-D69A-45E4-A794-CA8CED00AFAD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreExt.Qos.Store.Redis", "src\AspNetCoreExt.Qos.Store.Redis\AspNetCoreExt.Qos.Store.Redis.csproj", "{BBF65927-D77B-4CE4-A1F0-5032FA8CBC34}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreExt.Qos.Store.Redis.Tests", "test\AspNetCoreExt.Qos.Store.Redis.Tests\AspNetCoreExt.Qos.Store.Redis.Tests.csproj", "{220C3BFD-062E-4BD3-9F87-C8DF2587BA7E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -67,6 +89,46 @@ Global
{9E3DC226-9AB5-407B-93A8-D123C89F562A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E3DC226-9AB5-407B-93A8-D123C89F562A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E3DC226-9AB5-407B-93A8-D123C89F562A}.Release|Any CPU.Build.0 = Release|Any CPU
{33E3AA2E-F86B-42F7-BB4F-3DC9687758ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{33E3AA2E-F86B-42F7-BB4F-3DC9687758ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{33E3AA2E-F86B-42F7-BB4F-3DC9687758ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33E3AA2E-F86B-42F7-BB4F-3DC9687758ED}.Release|Any CPU.Build.0 = Release|Any CPU
{9CBDC2A9-27BC-4E52-8E56-38EA27C91A16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9CBDC2A9-27BC-4E52-8E56-38EA27C91A16}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9CBDC2A9-27BC-4E52-8E56-38EA27C91A16}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9CBDC2A9-27BC-4E52-8E56-38EA27C91A16}.Release|Any CPU.Build.0 = Release|Any CPU
{EA3C4392-9033-4C00-99A9-1AE69A3498CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA3C4392-9033-4C00-99A9-1AE69A3498CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA3C4392-9033-4C00-99A9-1AE69A3498CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA3C4392-9033-4C00-99A9-1AE69A3498CE}.Release|Any CPU.Build.0 = Release|Any CPU
{9BB335B0-798E-4EF2-8181-A50143BBEA77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9BB335B0-798E-4EF2-8181-A50143BBEA77}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9BB335B0-798E-4EF2-8181-A50143BBEA77}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9BB335B0-798E-4EF2-8181-A50143BBEA77}.Release|Any CPU.Build.0 = Release|Any CPU
{4286408E-A56A-4BDB-9FFD-29ECB9B12086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4286408E-A56A-4BDB-9FFD-29ECB9B12086}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4286408E-A56A-4BDB-9FFD-29ECB9B12086}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4286408E-A56A-4BDB-9FFD-29ECB9B12086}.Release|Any CPU.Build.0 = Release|Any CPU
{B933BAC7-0B75-4895-BADE-7EF9E1A8A6DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B933BAC7-0B75-4895-BADE-7EF9E1A8A6DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B933BAC7-0B75-4895-BADE-7EF9E1A8A6DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B933BAC7-0B75-4895-BADE-7EF9E1A8A6DB}.Release|Any CPU.Build.0 = Release|Any CPU
{51EAB754-AC3D-44CF-A329-A6D86BF550F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51EAB754-AC3D-44CF-A329-A6D86BF550F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51EAB754-AC3D-44CF-A329-A6D86BF550F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51EAB754-AC3D-44CF-A329-A6D86BF550F4}.Release|Any CPU.Build.0 = Release|Any CPU
{6685F90A-D69A-45E4-A794-CA8CED00AFAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6685F90A-D69A-45E4-A794-CA8CED00AFAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6685F90A-D69A-45E4-A794-CA8CED00AFAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6685F90A-D69A-45E4-A794-CA8CED00AFAD}.Release|Any CPU.Build.0 = Release|Any CPU
{BBF65927-D77B-4CE4-A1F0-5032FA8CBC34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BBF65927-D77B-4CE4-A1F0-5032FA8CBC34}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBF65927-D77B-4CE4-A1F0-5032FA8CBC34}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BBF65927-D77B-4CE4-A1F0-5032FA8CBC34}.Release|Any CPU.Build.0 = Release|Any CPU
{220C3BFD-062E-4BD3-9F87-C8DF2587BA7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{220C3BFD-062E-4BD3-9F87-C8DF2587BA7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{220C3BFD-062E-4BD3-9F87-C8DF2587BA7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{220C3BFD-062E-4BD3-9F87-C8DF2587BA7E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -81,6 +143,16 @@ Global
{21BF9B6C-D25A-46BA-900C-C803AA091E82} = {BBB4D112-80ED-45EF-B2C4-AE8778B7FADB}
{3D38856B-6B6C-4C9A-B46E-DBB00CD4348B} = {BBB4D112-80ED-45EF-B2C4-AE8778B7FADB}
{9E3DC226-9AB5-407B-93A8-D123C89F562A} = {7CCB8C14-0093-4440-A278-4D9CAAA22C67}
{33E3AA2E-F86B-42F7-BB4F-3DC9687758ED} = {7CCB8C14-0093-4440-A278-4D9CAAA22C67}
{9CBDC2A9-27BC-4E52-8E56-38EA27C91A16} = {7CCB8C14-0093-4440-A278-4D9CAAA22C67}
{EA3C4392-9033-4C00-99A9-1AE69A3498CE} = {7CCB8C14-0093-4440-A278-4D9CAAA22C67}
{9BB335B0-798E-4EF2-8181-A50143BBEA77} = {7CCB8C14-0093-4440-A278-4D9CAAA22C67}
{4286408E-A56A-4BDB-9FFD-29ECB9B12086} = {7CCB8C14-0093-4440-A278-4D9CAAA22C67}
{B933BAC7-0B75-4895-BADE-7EF9E1A8A6DB} = {495070EF-AC12-446F-B8D9-1E9EBD062F4C}
{51EAB754-AC3D-44CF-A329-A6D86BF550F4} = {7CCB8C14-0093-4440-A278-4D9CAAA22C67}
{6685F90A-D69A-45E4-A794-CA8CED00AFAD} = {7CCB8C14-0093-4440-A278-4D9CAAA22C67}
{BBF65927-D77B-4CE4-A1F0-5032FA8CBC34} = {BBB4D112-80ED-45EF-B2C4-AE8778B7FADB}
{220C3BFD-062E-4BD3-9F87-C8DF2587BA7E} = {7CCB8C14-0093-4440-A278-4D9CAAA22C67}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A0057FFF-1502-4B6C-B415-C203FEFFEAEC}
Expand Down
222 changes: 221 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,222 @@
# AspNetCoreExt-Qos
ASP.NET QoS middlewares

ASP.NET Core QoS is a set of high-performance and highly customizable middlewares that allow to set different limits on requests (quotas, rates).

## Simple example

**Startup.cs**
```csharp
public void ConfigureServices(IServiceCollection services)
{
// Add base QoS middleware
services.AddQos();

// Allows to use C# expressions directly in the 'appsettings.json' file
services.AddExpressionPolicyKeyComputer();

// Add middleware that allows special request to bypass the QoS policies
services.Configure<QosVipOptions>(Configuration.GetSection("Vip"));
services.AddQosVip();

// Add concurrency policies
services.Configure<QosConcurrencyOptions>(Configuration.GetSection("Concurrency"));
services.AddQosConcurrency();

// Add rate-limit policies
services.Configure<QosRateLimitOptions>(Configuration.GetSection("RateLimit"));
services.PostConfigure<QosRateLimitOptions>(o =>
{
o.Policies.Add("R_HARDCODED", new QosRateLimitPolicy()
{
MaxCount = 2,
Period = new TimeSpan(0, 0, 30),
UrlTemplates = new[] { "/api/ratelimit2" },
Key = QosPolicyKeyComputer.Create(c => c.HttpContext.Connection.RemoteIpAddress.ToString())
});
});

services.AddQosRateLimit();

// Add quotas
services.Configure<QosQuotaOptions>(Configuration.GetSection("Quota"));
services.AddQosQuota();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseQosVip();
app.UseQos();
}
```

**appsettings.json**
```json
"Vip": {
"IpAddresses": [
"11.22.33.44",
"22.33.44.55"
]
},

"Concurrency": {
"Policies": {
"C1": {
"UrlTemplates": [ "*" ],
"Key": "@(context.Request.IpAddress)",
"MaxCount": 2
}
}
},

"RateLimit": {
"Policies": {
"R1": {
"UrlTemplates": [ "/api/ratelimit/{id}" ],
"Key": "@(context.Request.IpAddress)",
"MaxCount": 3,
"Period": "0:0:10"
}
}
},

"Quota": {
"Policies": {
"Q1": {
"UrlTemplates": [ "/api/quota1", "/api/quota2" ],
"Key": "@(context.Request.Url)",
"MaxCount": 300,
"Period": "0:0:30",
"Distributed": false
}
}

```

In the example above, there are several policies:
- Requests with two specific IP addresses bypass all QoS policies.
- It is forbidden to process more than 2 simultaneous requests from the same IP address.
- The route '/api/ratelimit/{id}' accepts only 3 requests per 10 seconds from a same IP address.
- The routes '/api/quota1' and '/api/quota2' accept only 300KB per 30 seconds.
- Other routes has no specific policy (except the concurreny policy that is applied to all routes).

For more detailed information, see the example web site in the Github sources.

## Policy parameters

You can set different parameters to setup the policies:
- The URL templates indicates the routes to check. The special `"*"` URL is used as wildcard.
- The counters are separated depending on the 'key'. Several formats are accepted to set the key (see below).
- The period defines a time window that start when the counter increment is done.
- Max count is the limit (does not exists in the concurrency policy). It is expressed in KB for the quotas.
- The Distributed property can be used in conjunction with Redis (call `services.AddQosRedisStore(...)` in this case) to create distributed counters. It is possible to mix local and distributed counters depending on the policy.

## Key parameter syntax

- If the key expression analyzer is setup (`services.AddExpressionPolicyKeyComputer()`), it is possible to write key C# expression as strings directly in the 'appsettings.json'.
- Text enclosed between `@(` and `)` is considered as a C# expression.
- Text enclosed between `@{` and `}` is considered as a C# method body (and must include a `return...` somewhere).
- Both syntaxes can use a `context` variable that contains several properties and methods (see Github `AspNetCoreExt.Qos.ExpressionPolicyKeyComputer.Internal.Context.DefaultContext` source for details).
- The special string `"*"` means that the key is the same for every request. It is useful to setup a global rate limit policy of 100 calls per second, for example.
- It is possible to setup a custom policy by code: you have to write a class that implements `IQosPolicyKeyComputer`, and assign it in the options configuration.

## VIP (Very Important Person) middleware

Some administration requests can bypass the policies. A white list of special IP addresses can be set for that in a specific middleware.

## Policies in MVC

It is easy to link a policy directly to a MVC action.

The following code...

**Startup.cs**
```csharp
public void ConfigureServices(IServiceCollection services)
{
services.AddQos();
services.AddExpressionPolicyKeyComputer();
services.Configure<QosQuotaOptions>(Configuration.GetSection("Quota"));
services.AddQosQuota();

services.AddMvc();
services.AddQosMvc(); // Necessary to be able to use the QoS attribute
}
```

**MyController.cs**
```csharp
public class MyController : ControllerBase
{
[HttpGet("/foo/bar1")]
public string Get1() => "value1";

[HttpGet("/foo/bar2")]
[QosPolicy("MyPolicy")] // The special QoS attribute
public string Get2() => "value2";
}
```

**appsettings.json**
```json
"Quota": {
"Policies": {
"MyPolicy": {
"UrlTemplates": [ "/foo/bar1" ],
"Key": "@(context.Request.Url)",
"MaxCount": 300,
"Period": "0:0:30"
}
}
```

... is equivalent to...

**Startup.cs**
```csharp
public void ConfigureServices(IServiceCollection services)
{
services.AddQos();
services.AddExpressionPolicyKeyComputer();
services.Configure<QosQuotaOptions>(Configuration.GetSection("Quota"));
services.AddQosQuota();

services.AddMvc();
}
```

**MyController.cs**
```csharp
public class MyController : ControllerBase
{
[HttpGet("/foo/bar1")]
public string Get1() => "value1";

[HttpGet("/foo/bar2")]
public string Get2() => "value2";
}
```

**appsettings.json**
```json
"Quota": {
"Policies": {
"MyPolicy": {
"UrlTemplates": [ "/foo/bar1", "/foo/bar2" ],
"Key": "@(context.Request.Url)",
"MaxCount": 300,
"Period": "0:0:30"
}
}
```

You don't have to copy/paste the routes.

## Advanced customization

- Custom policy gates can be created by implementing the interface `IQosPolicyGate`.
- To accept specific syntax for the keys, implement `IQosPolicyKeyComputerProvider`.
- To globally change/adjust parameters on policies, implement `IQosPolicyPostConfigure`.
- To create a totally new category of policies, see `IQosPolicyProvider`.
- To customize the error response, see the interface `IQosRejectResponse` or the default base class `DefaultQosRejectResponse`.
- To bypass policies according to different criteria of the IP address, create your own middleware that set a `IVipFeature` instance into the `HttpContext.Features` property before calling the QoS middleware.
- If you don't want to use Redis for your distributed counters, implement the interface `IQosDistributedCounterStore` in a new class.
42 changes: 42 additions & 0 deletions samples/QosSandbox/Controllers/ValuesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using AspNetCoreExt.Qos.Mvc;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace QosSandbox.Controllers
{
[Route("api")]
public class ValuesController : ControllerBase
{
[HttpGet("ratelimit/{id}")]
public string GetRateLimit(int id)
{
return "value";
}

[HttpGet("ratelimit2")]
public async Task<string> GetRateLimit2()
{
await Task.Delay(5000);
return "value2";
}

[HttpGet("quota1")]
public string GetQuota1()
{
return new string('1', 10000);
}

[HttpGet("quota2")]
public string GetQuota2()
{
return new string('2', 20000);
}

[HttpGet("ratelimitmvc")]
[QosPolicy("R_MVC")]
public string GetRateLimitMvc()
{
return "value";
}
}
}
29 changes: 29 additions & 0 deletions samples/QosSandbox/CustomPolicy/MyCustomPolicyGate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using AspNetCoreExt.Qos;
using System.Threading.Tasks;

namespace QosSandbox.CustomPolicy
{
public class MyCustomPolicyGate : IQosPolicyGate
{
public Task<QosGateEnterResult> TryEnterAsync(QosGateEnterContext context)
{
if (context.Key == "OK")
{
return Task.FromResult(new QosGateEnterResult()
{
Success = true
});
}

return Task.FromResult(new QosGateEnterResult()
{
Success = false
});
}

public Task ExitAsync(QosGateExitContext context)
{
return Task.CompletedTask;
}
}
}
Loading

0 comments on commit df18c7e

Please sign in to comment.