diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d10a99f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +env: + DOTNET_VERSION: '9.0.x' + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore dependencies + run: dotnet restore ManagedCode.Orleans.RateLimiting.sln + + - name: Build + run: dotnet build ManagedCode.Orleans.RateLimiting.sln --configuration Release --no-restore + + - name: Test + run: dotnet test ManagedCode.Orleans.RateLimiting.Tests/ManagedCode.Orleans.RateLimiting.Tests.csproj --configuration Release --no-build --verbosity normal --collect:"XPlat Code Coverage" + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./**/coverage.cobertura.xml + fail_ci_if_error: false diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7ae370e..7b796f5 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,67 +1,49 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL" on: push: branches: [ main ] pull_request: - # The branches below must be a subset of the branches above branches: [ main ] schedule: - - cron: '35 11 * * 4' + - cron: '0 0 * * 1' + +env: + DOTNET_VERSION: '9.0.x' jobs: analyze: name: Analyze runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write strategy: fail-fast: false matrix: language: [ 'csharp' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + - name: Restore dependencies + run: dotnet restore ManagedCode.Orleans.RateLimiting.sln - #- run: | - # make bootstrap - # make release + - name: Build + run: dotnet build ManagedCode.Orleans.RateLimiting.sln --no-restore - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml deleted file mode 100644 index a03ea6b..0000000 --- a/.github/workflows/dotnet.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: .NET - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -jobs: - - build-and-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 7.0.x - - # run build and test - - name: Restore dependencies - run: dotnet restore - - name: Build - run: dotnet build --no-restore - - name: Test - run: dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=ManagedCode.Orleans.RateLimiting.Tests/lcov.info - - #- name: coverlet - # uses: b3b00/coverlet-action@1.1.9 - # with: - # testProject: 'ManagedCode.Database.Tests/ManagedCode.Database.Tests.csproj' - # output: 'lcov.info' - # outputFormat: 'lcov' - # excludes: '[program]*,[test]test.*' - #- name: coveralls - # uses: coverallsapp/github-action@master - # with: - # github-token: ${{secrets.GITHUB_TOKEN }} - # path-to-lcov: ManagedCode.Storage.Tests/lcov.info diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml deleted file mode 100644 index c7e8c83..0000000 --- a/.github/workflows/nuget.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: nuget - -on: - push: - branches: [ main ] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -jobs: - nuget-pack: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 7.0.x - - - name: Restore dependencies - run: dotnet restore - - name: Build - run: dotnet build --configuration Release - - name: Test - run: dotnet test --configuration Release - - name: Pack - run: dotnet pack -p:IncludeSymbols=false -p:SymbolPackageFormat=snupkg --configuration Release - - - name: publish nuget packages - run: | - shopt -s globstar - for file in **/*.nupkg - do - dotnet nuget push "$file" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate - done \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3c4d29d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,227 @@ +name: Release + +on: + push: + branches: [ main ] + workflow_dispatch: + +env: + DOTNET_VERSION: '9.0.x' + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + + outputs: + version: ${{ steps.version.outputs.version }} + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Extract version from Directory.Build.props + id: version + run: | + VERSION=$(grep -oPm1 "(?<=)[^<]+" Directory.Build.props) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Version from Directory.Build.props: $VERSION" + + - name: Restore dependencies + run: dotnet restore ManagedCode.Orleans.RateLimiting.sln + + - name: Build + run: dotnet build ManagedCode.Orleans.RateLimiting.sln --configuration Release --no-restore + + - name: Test + run: dotnet test ManagedCode.Orleans.RateLimiting.Tests/ManagedCode.Orleans.RateLimiting.Tests.csproj --configuration Release --no-build --verbosity normal + + - name: Pack NuGet packages + run: dotnet pack ManagedCode.Orleans.RateLimiting.sln --configuration Release --no-build -p:IncludeSymbols=false -p:SymbolPackageFormat=snupkg --output ./artifacts + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: ./artifacts/*.nupkg + retention-days: 5 + + publish-nuget: + name: Publish to NuGet + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + + outputs: + published: ${{ steps.publish.outputs.published }} + version: ${{ needs.build.outputs.version }} + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Download artifacts + uses: actions/download-artifact@v5 + with: + name: nuget-packages + path: ./artifacts + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Publish to NuGet + id: publish + continue-on-error: true + run: | + set +e + OUTPUT="" + PUBLISHED=false + + for package in ./artifacts/*.nupkg; do + echo "Publishing $package..." + RESULT=$(dotnet nuget push "$package" \ + --api-key ${{ secrets.NUGET_API_KEY }} \ + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate 2>&1) + EXIT_CODE=$? + echo "$RESULT" + OUTPUT="$OUTPUT$RESULT" + + if [ $EXIT_CODE -eq 0 ]; then + echo "Successfully published $package" + PUBLISHED=true + elif echo "$RESULT" | grep -q "already exists"; then + echo "Package already exists, skipping..." + else + echo "Failed to publish $package" + exit 1 + fi + done + + if [ "$PUBLISHED" = true ] || echo "$OUTPUT" | grep -q "Your package was pushed"; then + echo "published=true" >> $GITHUB_OUTPUT + echo "At least one package was successfully published" + else + echo "published=false" >> $GITHUB_OUTPUT + echo "No new packages were published (all already exist)" + fi + + create-release: + name: Create GitHub Release and Tag + needs: publish-nuget + runs-on: ubuntu-latest + if: needs.publish-nuget.outputs.published == 'true' + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download artifacts + uses: actions/download-artifact@v5 + with: + name: nuget-packages + path: ./artifacts + + - name: Create and push tag + id: create_tag + run: | + VERSION="${{ needs.publish-nuget.outputs.version }}" + TAG="v$VERSION" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists" + echo "tag_exists=true" >> $GITHUB_OUTPUT + else + echo "Creating tag $TAG" + git tag -a "$TAG" -m "Release $VERSION" + git push origin "$TAG" + echo "tag_exists=false" >> $GITHUB_OUTPUT + fi + + - name: Get previous tag + id: prev_tag + run: | + CURRENT_TAG="v${{ needs.publish-nuget.outputs.version }}" + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -A1 "^$CURRENT_TAG$" | tail -n1 || echo "") + if [ "$PREVIOUS_TAG" = "$CURRENT_TAG" ] || [ -z "$PREVIOUS_TAG" ]; then + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -v "^$CURRENT_TAG$" | head -n1 || echo "") + fi + echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT + echo "Current tag: $CURRENT_TAG" + echo "Previous tag: $PREVIOUS_TAG" + + - name: Generate release notes + id: release_notes + run: | + VERSION="${{ needs.publish-nuget.outputs.version }}" + CURRENT_TAG="v$VERSION" + PREVIOUS_TAG="${{ steps.prev_tag.outputs.previous_tag }}" + + echo "# Release $VERSION" > release_notes.md + echo "" >> release_notes.md + echo "Released on $(date +'%Y-%m-%d')" >> release_notes.md + echo "" >> release_notes.md + + if [ -n "$PREVIOUS_TAG" ]; then + echo "## 📋 Changes since $PREVIOUS_TAG" >> release_notes.md + echo "" >> release_notes.md + + echo "### ✨ Features" >> release_notes.md + git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD --grep="^feat" --grep="^feature" >> release_notes.md || true + echo "" >> release_notes.md + + echo "### 🐛 Bug Fixes" >> release_notes.md + git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD --grep="^fix" --grep="^bugfix" >> release_notes.md || true + echo "" >> release_notes.md + + echo "### 📚 Documentation" >> release_notes.md + git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD --grep="^docs" --grep="^doc" >> release_notes.md || true + echo "" >> release_notes.md + + echo "### 🔧 Other Changes" >> release_notes.md + git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD --invert-grep --grep="^feat" --grep="^feature" --grep="^fix" --grep="^bugfix" --grep="^docs" --grep="^doc" >> release_notes.md || true + echo "" >> release_notes.md + else + echo "## 🎉 Initial Release" >> release_notes.md + echo "" >> release_notes.md + echo "### Recent Changes" >> release_notes.md + git log --pretty=format:"- %s (%h)" --max-count=20 >> release_notes.md + echo "" >> release_notes.md + fi + + echo "" >> release_notes.md + echo "## đŸ“Ļ NuGet Packages" >> release_notes.md + echo "" >> release_notes.md + for package in ./artifacts/*.nupkg; do + PACKAGE_NAME=$(basename "$package" .nupkg) + BASE_NAME=$(echo "$PACKAGE_NAME" | sed "s/\.$VERSION//") + echo "- [$BASE_NAME v$VERSION](https://www.nuget.org/packages/$BASE_NAME/$VERSION)" >> release_notes.md + done + + echo "" >> release_notes.md + echo "---" >> release_notes.md + echo "*This release was automatically created by GitHub Actions*" >> release_notes.md + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.publish-nuget.outputs.version }} + name: v${{ needs.publish-nuget.outputs.version }} + body_path: release_notes.md + draft: false + prerelease: false + files: ./artifacts/*.nupkg + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c5a02a8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# Agent Guidelines + +- Follow standard C#/.NET coding conventions when modifying source files. +- Run `dotnet build` and relevant tests before concluding changes whenever possible. +- Update documentation alongside functional changes that affect public behavior. diff --git a/Directory.Build.props b/Directory.Build.props index c047f7f..12bf3e0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -17,8 +17,8 @@ https://github.com/managedcode/Orleans.RateLimiting https://github.com/managedcode/Orleans.RateLimiting Managed Code - Orleans RateLimiting - 0.0.8 - 0.0.8 + 0.0.9 + 0.0.9 @@ -29,9 +29,9 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - \ No newline at end of file + diff --git a/ManagedCode.Orleans.RateLimiting.Client/ManagedCode.Orleans.RateLimiting.Client.csproj b/ManagedCode.Orleans.RateLimiting.Client/ManagedCode.Orleans.RateLimiting.Client.csproj index 425965c..e5e7d1a 100644 --- a/ManagedCode.Orleans.RateLimiting.Client/ManagedCode.Orleans.RateLimiting.Client.csproj +++ b/ManagedCode.Orleans.RateLimiting.Client/ManagedCode.Orleans.RateLimiting.Client.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 12 Library enable @@ -18,11 +18,11 @@ - + - - - + + + diff --git a/ManagedCode.Orleans.RateLimiting.Client/Middlewares/OrleansBaseRateLimitingMiddleware.cs b/ManagedCode.Orleans.RateLimiting.Client/Middlewares/OrleansBaseRateLimitingMiddleware.cs index 1f6d70c..8fa665f 100644 --- a/ManagedCode.Orleans.RateLimiting.Client/Middlewares/OrleansBaseRateLimitingMiddleware.cs +++ b/ManagedCode.Orleans.RateLimiting.Client/Middlewares/OrleansBaseRateLimitingMiddleware.cs @@ -49,7 +49,7 @@ public async Task Invoke(HttpContext httpContext) { httpContext.Response.Clear(); httpContext.Response.StatusCode = (int)HttpStatusCode.TooManyRequests; - await httpContext.Response.WriteAsJsonAsync(Result.Fail(HttpStatusCode.TooManyRequests, error.ToException())); + await httpContext.Response.WriteAsJsonAsync(Result.Fail(error.ToException(), HttpStatusCode.TooManyRequests)); } } @@ -62,7 +62,7 @@ public async Task Invoke(HttpContext httpContext) // first try to get attribute from endpoint, var attribute = endpoint.Metadata.GetMetadata(); - var postfix = endpoint.ToString()!; + var postfix = endpoint.DisplayName ?? endpoint.ToString() ?? string.Empty; if (attribute is null) { @@ -72,7 +72,7 @@ public async Task Invoke(HttpContext httpContext) if (controllerType != null) { attribute = controllerType.GetCustomAttribute(true); - postfix = controllerType.ToString(); + postfix = controllerType.FullName ?? controllerType.Name; } } @@ -84,10 +84,17 @@ public async Task Invoke(HttpContext httpContext) protected ILimiterHolder? TryGetLimiterHolder(string key, string configurationName) { - var limiter = _client.GetRateLimiterByConfig(key, configurationName, _services.GetService>()); + var configs = _services.GetService>(); + if (configs is null) + { + _logger.LogError("No rate limiter configurations registered when looking up {Configuration}", configurationName); + return null; + } + + var limiter = _client.GetRateLimiterByConfig(key, configurationName, configs); if (limiter is null) - _logger.LogError($"Configuration {configurationName} not found for RateLimiter"); + _logger.LogError("Configuration {Configuration} not found for RateLimiter", configurationName); return limiter; } diff --git a/ManagedCode.Orleans.RateLimiting.Client/Middlewares/RateLimitingHubFilter.cs b/ManagedCode.Orleans.RateLimiting.Client/Middlewares/RateLimitingHubFilter.cs index 5c2a733..d29a458 100644 --- a/ManagedCode.Orleans.RateLimiting.Client/Middlewares/RateLimitingHubFilter.cs +++ b/ManagedCode.Orleans.RateLimiting.Client/Middlewares/RateLimitingHubFilter.cs @@ -20,7 +20,15 @@ public RateLimitingHubFilter(ILogger logger, IClusterClie public async ValueTask InvokeMethodAsync(HubInvocationContext invocationContext, Func> next) { - var limiter = _client.GetFixedWindowRateLimiter(invocationContext.Context.User.Identity.Name); + var userName = invocationContext.Context.User?.Identity?.Name; + + if (string.IsNullOrWhiteSpace(userName)) + { + _logger.LogDebug("Skipping rate limiter acquisition because no user identity was provided for {Hub}", invocationContext.Hub?.GetType().FullName); + return await next(invocationContext); + } + + var limiter = _client.GetFixedWindowRateLimiter(userName); await using var lease = await limiter.AcquireAsync(); lease.ThrowIfNotAcquired(); @@ -34,7 +42,7 @@ public Task OnConnectedAsync(HubLifetimeContext context, Func next) + public Task OnDisconnectedAsync(HubLifetimeContext context, Exception? exception, Func next) { return next(context, exception); } diff --git a/ManagedCode.Orleans.RateLimiting.Core/Attributes/ConcurrencyLimiterAttribute.cs b/ManagedCode.Orleans.RateLimiting.Core/Attributes/ConcurrencyLimiterAttribute.cs index d16fc05..ce7426e 100644 --- a/ManagedCode.Orleans.RateLimiting.Core/Attributes/ConcurrencyLimiterAttribute.cs +++ b/ManagedCode.Orleans.RateLimiting.Core/Attributes/ConcurrencyLimiterAttribute.cs @@ -20,7 +20,7 @@ public class ConcurrencyLimiterAttribute : Attribute, ILimiterAttribute= 0 by the time these options are passed to the constructor of . /// /// Determines the behaviour of when not enough resources can be leased. - public ConcurrencyLimiterAttribute(KeyType keyType = KeyType.GrainId, string key = default, int permitLimit = default, int queueLimit = 0, + public ConcurrencyLimiterAttribute(KeyType keyType = KeyType.GrainId, string? key = null, int permitLimit = default, int queueLimit = 0, QueueProcessingOrder queueProcessingOrder = QueueProcessingOrder.OldestFirst) { Key = key; @@ -41,8 +41,9 @@ public ConcurrencyLimiterAttribute(KeyType keyType = KeyType.GrainId, string key }; } - public ConcurrencyLimiterAttribute(string configurationName, KeyType keyType = KeyType.GrainId, string key = default) + public ConcurrencyLimiterAttribute(string configurationName, KeyType keyType = KeyType.GrainId, string? key = null) { + ArgumentException.ThrowIfNullOrWhiteSpace(configurationName); ConfigurationName = configurationName; Key = key; KeyType = keyType; diff --git a/ManagedCode.Orleans.RateLimiting.Core/Attributes/FixedWindowRateLimiterAttribute.cs b/ManagedCode.Orleans.RateLimiting.Core/Attributes/FixedWindowRateLimiterAttribute.cs index 2d12637..58300a9 100644 --- a/ManagedCode.Orleans.RateLimiting.Core/Attributes/FixedWindowRateLimiterAttribute.cs +++ b/ManagedCode.Orleans.RateLimiting.Core/Attributes/FixedWindowRateLimiterAttribute.cs @@ -28,7 +28,7 @@ public class FixedWindowRateLimiterAttribute : Attribute, ILimiterAttribute 0 by the time these options are passed to the constructor of . /// /// Determines the behaviour of when not enough resources can be leased. - public FixedWindowRateLimiterAttribute(KeyType keyType = KeyType.GrainId, string key = default, int windowInSeconds = default, int permitLimit = default, + public FixedWindowRateLimiterAttribute(KeyType keyType = KeyType.GrainId, string? key = null, int windowInSeconds = default, int permitLimit = default, int queueLimit = 0, bool autoReplenishment = true, QueueProcessingOrder queueProcessingOrder = QueueProcessingOrder.OldestFirst) { Key = key; @@ -52,8 +52,9 @@ public FixedWindowRateLimiterAttribute(KeyType keyType = KeyType.GrainId, string }; } - public FixedWindowRateLimiterAttribute(string configurationName, KeyType keyType = KeyType.GrainId, string key = default) + public FixedWindowRateLimiterAttribute(string configurationName, KeyType keyType = KeyType.GrainId, string? key = null) { + ArgumentException.ThrowIfNullOrWhiteSpace(configurationName); ConfigurationName = configurationName; Key = key; KeyType = keyType; diff --git a/ManagedCode.Orleans.RateLimiting.Core/Attributes/SlidingWindowRateLimiterAttribute.cs b/ManagedCode.Orleans.RateLimiting.Core/Attributes/SlidingWindowRateLimiterAttribute.cs index 8ce07f8..52a71c5 100644 --- a/ManagedCode.Orleans.RateLimiting.Core/Attributes/SlidingWindowRateLimiterAttribute.cs +++ b/ManagedCode.Orleans.RateLimiting.Core/Attributes/SlidingWindowRateLimiterAttribute.cs @@ -32,7 +32,7 @@ public class SlidingWindowRateLimiterAttribute : Attribute, ILimiterAttribute 0 by the time these options are passed to the constructor of . /// /// Determines the behaviour of when not enough resources can be leased. - public SlidingWindowRateLimiterAttribute(KeyType keyType = KeyType.GrainId, string key = default, int windowinSeconds = default, int permitLimit = default, + public SlidingWindowRateLimiterAttribute(KeyType keyType = KeyType.GrainId, string? key = null, int windowinSeconds = default, int permitLimit = default, int queueLimit = 0, int segmentsPerWindow = default, bool autoReplenishment = true, QueueProcessingOrder queueProcessingOrder = QueueProcessingOrder.OldestFirst) { Key = key; @@ -59,8 +59,9 @@ public SlidingWindowRateLimiterAttribute(KeyType keyType = KeyType.GrainId, stri }; } - public SlidingWindowRateLimiterAttribute(string configurationName, KeyType keyType = KeyType.GrainId, string key = default) + public SlidingWindowRateLimiterAttribute(string configurationName, KeyType keyType = KeyType.GrainId, string? key = null) { + ArgumentException.ThrowIfNullOrWhiteSpace(configurationName); ConfigurationName = configurationName; Key = key; KeyType = keyType; diff --git a/ManagedCode.Orleans.RateLimiting.Core/Attributes/TokenBucketRateLimiterAttribute.cs b/ManagedCode.Orleans.RateLimiting.Core/Attributes/TokenBucketRateLimiterAttribute.cs index d4cf910..84c44fa 100644 --- a/ManagedCode.Orleans.RateLimiting.Core/Attributes/TokenBucketRateLimiterAttribute.cs +++ b/ManagedCode.Orleans.RateLimiting.Core/Attributes/TokenBucketRateLimiterAttribute.cs @@ -32,7 +32,7 @@ public class TokenBucketRateLimiterAttribute : Attribute, ILimiterAttribute 0 by the time these options are passed to the constructor of . /// /// Determines the behaviour of when not enough resources can be leased. - public TokenBucketRateLimiterAttribute(KeyType keyType = KeyType.GrainId, string key = default, int replenishmentPeriodInSeconds = default, + public TokenBucketRateLimiterAttribute(KeyType keyType = KeyType.GrainId, string? key = null, int replenishmentPeriodInSeconds = default, int tokensPerPeriod = default, int queueLimit = 0, int tokenLimit = default, bool autoReplenishment = true, QueueProcessingOrder queueProcessingOrder = QueueProcessingOrder.OldestFirst) { @@ -59,8 +59,9 @@ public TokenBucketRateLimiterAttribute(KeyType keyType = KeyType.GrainId, string }; } - public TokenBucketRateLimiterAttribute(string configurationName, KeyType keyType = KeyType.GrainId, string key = default) + public TokenBucketRateLimiterAttribute(string configurationName, KeyType keyType = KeyType.GrainId, string? key = null) { + ArgumentException.ThrowIfNullOrWhiteSpace(configurationName); ConfigurationName = configurationName; Key = key; KeyType = keyType; diff --git a/ManagedCode.Orleans.RateLimiting.Core/Extensions/GrainFactoryExtensions.cs b/ManagedCode.Orleans.RateLimiting.Core/Extensions/GrainFactoryExtensions.cs index a87381e..2fc4736 100644 --- a/ManagedCode.Orleans.RateLimiting.Core/Extensions/GrainFactoryExtensions.cs +++ b/ManagedCode.Orleans.RateLimiting.Core/Extensions/GrainFactoryExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.RateLimiting; @@ -19,7 +20,7 @@ public static ILimiterHolder GetRateLimiter(this IGrainFactory factory, strin ISlidingWindowRateLimiterGrain => factory.GetSlidingWindowRateLimiter(key), ITokenBucketRateLimiterGrain => factory.GetTokenBucketRateLimiter(key), - _ => null //throw new ArgumentException("Unknown rate limiter grain type") + _ => throw new ArgumentOutOfRangeException(nameof(T), typeof(T), "Unknown rate limiter grain type") }; return limiter; @@ -27,6 +28,7 @@ public static ILimiterHolder GetRateLimiter(this IGrainFactory factory, strin public static ILimiterHolder? GetRateLimiterByConfig(this IGrainFactory factory, string key, string configurationName, IEnumerable configs) { + ArgumentNullException.ThrowIfNull(configs); var name = configurationName.ToLowerInvariant(); var option = configs.FirstOrDefault(f => f.Name == name); if (option is null) diff --git a/ManagedCode.Orleans.RateLimiting.Core/ManagedCode.Orleans.RateLimiting.Core.csproj b/ManagedCode.Orleans.RateLimiting.Core/ManagedCode.Orleans.RateLimiting.Core.csproj index f1cae3b..b921974 100644 --- a/ManagedCode.Orleans.RateLimiting.Core/ManagedCode.Orleans.RateLimiting.Core.csproj +++ b/ManagedCode.Orleans.RateLimiting.Core/ManagedCode.Orleans.RateLimiting.Core.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 12 Library enable @@ -17,10 +17,10 @@ - - - - + + + + diff --git a/ManagedCode.Orleans.RateLimiting.Core/Models/Holders/BaseRateLimiterHolder.cs b/ManagedCode.Orleans.RateLimiting.Core/Models/Holders/BaseRateLimiterHolder.cs index 681fe15..32da4d8 100644 --- a/ManagedCode.Orleans.RateLimiting.Core/Models/Holders/BaseRateLimiterHolder.cs +++ b/ManagedCode.Orleans.RateLimiting.Core/Models/Holders/BaseRateLimiterHolder.cs @@ -6,11 +6,13 @@ namespace ManagedCode.Orleans.RateLimiting.Core.Models.Holders; -public abstract class BaseRateLimiterHolder : ILimiterHolderWithConfiguration where TGrain : IRateLimiterGrainWithConfiguration +public abstract class BaseRateLimiterHolder : ILimiterHolderWithConfiguration + where TGrain : IRateLimiterGrainWithConfiguration + where TOption : class { private readonly TGrain _grain; private readonly IGrainFactory _grainFactory; - private readonly TOption _option; + private readonly TOption? _option; internal BaseRateLimiterHolder(TGrain grain, IGrainFactory grainFactory) { @@ -32,7 +34,7 @@ public async Task AcquireAsync(int permitCount = 1) var metadata = await _grain.AcquireAsync(permitCount); return new OrleansRateLimitLease(metadata, _grainFactory); } - catch (TimeoutException timeoutException) + catch (TimeoutException) { return new OrleansRateLimitLease(new RateLimitLeaseMetadata(_grain.GetGrainId()), _grainFactory); } @@ -40,12 +42,15 @@ public async Task AcquireAsync(int permitCount = 1) public async Task AcquireAndConfigureAsync(int permitCount = 1) { + if (_option is null) + return await AcquireAsync(permitCount); + try { var metadata = await _grain.AcquireAndCheckConfigurationAsync(_option); return new OrleansRateLimitLease(metadata, _grainFactory); } - catch (TimeoutException timeoutException) + catch (TimeoutException) { return new OrleansRateLimitLease(new RateLimitLeaseMetadata(_grain.GetGrainId()), _grainFactory); } @@ -56,39 +61,40 @@ public async Task AcquireAndConfigureAsync(int permitCoun return _grain.GetStatisticsAsync(); } - public async Task AcquireAndCheckConfigurationAsync(TOption options) + public async Task AcquireAndCheckConfigurationAsync(TOption? options) { if (options is null && _option is null) return await AcquireAsync(); - if (_option is not null) - options = _option; + options ??= _option; + + if (options is null) + return await AcquireAsync(); try { var metadata = await _grain.AcquireAndCheckConfigurationAsync(options); return new OrleansRateLimitLease(metadata, _grainFactory); } - catch (TimeoutException timeoutException) + catch (TimeoutException) { return new OrleansRateLimitLease(new RateLimitLeaseMetadata(_grain.GetGrainId()), _grainFactory); } } - public async Task AcquireAndCheckConfigurationAsync(int permitCount, TOption options) + public async Task AcquireAndCheckConfigurationAsync(int permitCount, TOption? options) { + options ??= _option; + if (options is null) return await AcquireAsync(); - if (_option is not null) - options = _option; - try { var metadata = await _grain.AcquireAndCheckConfigurationAsync(permitCount, options); return new OrleansRateLimitLease(metadata, _grainFactory); } - catch (TimeoutException timeoutException) + catch (TimeoutException) { return new OrleansRateLimitLease(new RateLimitLeaseMetadata(_grain.GetGrainId()), _grainFactory); } diff --git a/ManagedCode.Orleans.RateLimiting.Core/Models/Holders/GroupLimiterHolder.cs b/ManagedCode.Orleans.RateLimiting.Core/Models/Holders/GroupLimiterHolder.cs index d9487cb..9419056 100644 --- a/ManagedCode.Orleans.RateLimiting.Core/Models/Holders/GroupLimiterHolder.cs +++ b/ManagedCode.Orleans.RateLimiting.Core/Models/Holders/GroupLimiterHolder.cs @@ -11,7 +11,9 @@ public class GroupLimiterHolder : IAsyncDisposable, IDisposable public async ValueTask DisposeAsync() { - await Task.WhenAll(_holders.Values.Where(w => w != null).Select(s => s.DisposeAsync().AsTask())); + await Task.WhenAll(_holders.Values + .OfType() + .Select(lease => lease.DisposeAsync().AsTask())); } public void Dispose() diff --git a/ManagedCode.Orleans.RateLimiting.Core/Models/RateLimitLeaseMetadata.cs b/ManagedCode.Orleans.RateLimiting.Core/Models/RateLimitLeaseMetadata.cs index 92753e1..bff37be 100644 --- a/ManagedCode.Orleans.RateLimiting.Core/Models/RateLimitLeaseMetadata.cs +++ b/ManagedCode.Orleans.RateLimiting.Core/Models/RateLimitLeaseMetadata.cs @@ -15,7 +15,10 @@ public RateLimitLeaseMetadata(Guid guid, GrainId grainId, RateLimitLease lease) LeaseId = guid; GrainId = grainId; IsAcquired = lease.IsAcquired; - Metadata = lease.GetAllMetadata().ToArray(); + Metadata = lease + .GetAllMetadata() + .Select(static kvp => new KeyValuePair(kvp.Key, kvp.Value)) + .ToArray(); } public RateLimitLeaseMetadata(GrainId grainId) @@ -23,7 +26,7 @@ public RateLimitLeaseMetadata(GrainId grainId) LeaseId = Guid.Empty; GrainId = grainId; IsAcquired = false; - Metadata = new[] { new KeyValuePair("REASON_PHRASE", "Lease not acquired") }; + Metadata = new[] { new KeyValuePair("REASON_PHRASE", "Lease not acquired") }; } [Id(0)] diff --git a/ManagedCode.Orleans.RateLimiting.Server/GrainCallFilter/BaseRateLimitingIncomingFilter.cs b/ManagedCode.Orleans.RateLimiting.Server/GrainCallFilter/BaseRateLimitingIncomingFilter.cs index 61ea131..94a98a4 100644 --- a/ManagedCode.Orleans.RateLimiting.Server/GrainCallFilter/BaseRateLimitingIncomingFilter.cs +++ b/ManagedCode.Orleans.RateLimiting.Server/GrainCallFilter/BaseRateLimitingIncomingFilter.cs @@ -27,7 +27,10 @@ public async Task Invoke(IIncomingGrainCallContext context) if (limiter.HasValue) { - await using var lease = await limiter.Value.Item1.AcquireAndCheckConfigurationAsync(limiter.Value.Item2); + var (holder, options) = limiter.Value; + await using var lease = options is null + ? await holder.AcquireAsync() + : await holder.AcquireAndCheckConfigurationAsync(options); if (lease.IsAcquired) await context.Invoke(); else @@ -39,7 +42,7 @@ public async Task Invoke(IIncomingGrainCallContext context) } } - private (ILimiterHolderWithConfiguration, TOptions)? IsRateLimiter(IIncomingGrainCallContext context) + private (ILimiterHolderWithConfiguration limiter, TOptions? options)? IsRateLimiter(IIncomingGrainCallContext context) { if (Attribute.IsDefined(context.ImplementationMethod, typeof(TAttribute))) { @@ -56,14 +59,15 @@ public async Task Invoke(IIncomingGrainCallContext context) return null; } - private (ILimiterHolderWithConfiguration, TOptions)? CreateRiteLimiter(IIncomingGrainCallContext context, Attribute attribute) + private (ILimiterHolderWithConfiguration limiter, TOptions? options)? CreateRiteLimiter(IIncomingGrainCallContext context, Attribute? attribute) { - var limiterAttribute = (ILimiterAttribute)attribute; + if (attribute is not ILimiterAttribute limiterAttribute) + return null; var limiter = limiterAttribute.KeyType switch { - KeyType.Key => GetLimiter(limiterAttribute.Key), - KeyType.GrainType => GetLimiter(context.ImplementationMethod.DeclaringType.FullName), + KeyType.Key => string.IsNullOrWhiteSpace(limiterAttribute.Key) ? null : GetLimiter(limiterAttribute.Key), + KeyType.GrainType => context.ImplementationMethod.DeclaringType?.FullName is { Length: > 0 } typeName ? GetLimiter(typeName) : null, KeyType.GrainId => GetLimiter(context.TargetContext.GrainId.ToString()), _ => null }; diff --git a/ManagedCode.Orleans.RateLimiting.Server/ManagedCode.Orleans.RateLimiting.Server.csproj b/ManagedCode.Orleans.RateLimiting.Server/ManagedCode.Orleans.RateLimiting.Server.csproj index 2ef00f5..687258d 100644 --- a/ManagedCode.Orleans.RateLimiting.Server/ManagedCode.Orleans.RateLimiting.Server.csproj +++ b/ManagedCode.Orleans.RateLimiting.Server/ManagedCode.Orleans.RateLimiting.Server.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 12 Library enable @@ -17,9 +17,9 @@ - - - + + + diff --git a/ManagedCode.Orleans.RateLimiting.Tests/ConcurrencyLimiterGrainTests.cs b/ManagedCode.Orleans.RateLimiting.Tests/ConcurrencyLimiterGrainTests.cs index 7dcc047..ac6ed8c 100644 --- a/ManagedCode.Orleans.RateLimiting.Tests/ConcurrencyLimiterGrainTests.cs +++ b/ManagedCode.Orleans.RateLimiting.Tests/ConcurrencyLimiterGrainTests.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using Shouldly; using ManagedCode.Orleans.RateLimiting.Tests.Cluster; using ManagedCode.Orleans.RateLimiting.Tests.Cluster.Grains.Interfaces; using Xunit; @@ -40,9 +40,9 @@ public async Task GrainIdTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().Be(count); - errors.Should().Be(0); + (success + errors).ShouldBe(count); + success.ShouldBe(count); + errors.ShouldBe(0); } [Fact] @@ -67,8 +67,8 @@ public async Task KeyTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } [Fact] @@ -93,8 +93,8 @@ public async Task TypeTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } [Fact] @@ -119,7 +119,7 @@ public async Task RateLimiterConfigTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } } \ No newline at end of file diff --git a/ManagedCode.Orleans.RateLimiting.Tests/FixedWindowRateLimiterGrainTests.cs b/ManagedCode.Orleans.RateLimiting.Tests/FixedWindowRateLimiterGrainTests.cs index 2fb2087..1996d23 100644 --- a/ManagedCode.Orleans.RateLimiting.Tests/FixedWindowRateLimiterGrainTests.cs +++ b/ManagedCode.Orleans.RateLimiting.Tests/FixedWindowRateLimiterGrainTests.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using Shouldly; using ManagedCode.Orleans.RateLimiting.Tests.Cluster; using ManagedCode.Orleans.RateLimiting.Tests.Cluster.Grains.Interfaces; using Xunit; @@ -40,9 +40,9 @@ public async Task GrainIdTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().Be(count); - errors.Should().Be(0); + (success + errors).ShouldBe(count); + success.ShouldBe(count); + errors.ShouldBe(0); } [Fact] @@ -67,8 +67,8 @@ public async Task KeyTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } [Fact] @@ -93,8 +93,8 @@ public async Task TypeTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } [Fact] @@ -119,7 +119,7 @@ public async Task RateLimiterConfigTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } } \ No newline at end of file diff --git a/ManagedCode.Orleans.RateLimiting.Tests/ManagedCode.Orleans.RateLimiting.Tests.csproj b/ManagedCode.Orleans.RateLimiting.Tests/ManagedCode.Orleans.RateLimiting.Tests.csproj index 5aede60..f36d1b3 100644 --- a/ManagedCode.Orleans.RateLimiting.Tests/ManagedCode.Orleans.RateLimiting.Tests.csproj +++ b/ManagedCode.Orleans.RateLimiting.Tests/ManagedCode.Orleans.RateLimiting.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 12 true false @@ -11,41 +11,41 @@ - - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + + + + + + + diff --git a/ManagedCode.Orleans.RateLimiting.Tests/RateLimiterTests.cs b/ManagedCode.Orleans.RateLimiting.Tests/RateLimiterTests.cs index b82475b..d509030 100644 --- a/ManagedCode.Orleans.RateLimiting.Tests/RateLimiterTests.cs +++ b/ManagedCode.Orleans.RateLimiting.Tests/RateLimiterTests.cs @@ -1,6 +1,6 @@ using System.Diagnostics; using System.Threading.RateLimiting; -using FluentAssertions; +using Shouldly; using ManagedCode.Orleans.RateLimiting.Core.Extensions; using ManagedCode.Orleans.RateLimiting.Tests.Cluster; using ManagedCode.TimeSeries.Summers; @@ -35,11 +35,9 @@ await rateLimiter.Configure(new ConcurrencyLimiterOptions }); var errors = 0; - var success = 0; var calls = 0; var token1 = new CancellationTokenSource(); - var token2 = new CancellationTokenSource(); var tasks = Enumerable.Range(0, permit + extra).Select(s => Task.Run(async () => @@ -59,30 +57,34 @@ await rateLimiter.Configure(new ConcurrencyLimiterOptions await Task.Delay(TimeSpan.FromSeconds(5)); var statistics1 = await rateLimiter.GetStatisticsAsync(); - _outputHelper.WriteLine("TotalSuccessfulLeases " + statistics1.TotalSuccessfulLeases); - _outputHelper.WriteLine("TotalFailedLeases " + statistics1.TotalFailedLeases); - _outputHelper.WriteLine("CurrentAvailablePermits " + statistics1.CurrentAvailablePermits); - _outputHelper.WriteLine("CurrentQueuedCount " + statistics1.CurrentQueuedCount); + statistics1.ShouldNotBeNull(); + var stats1 = statistics1!; + + _outputHelper.WriteLine("TotalSuccessfulLeases " + stats1.TotalSuccessfulLeases); + _outputHelper.WriteLine("TotalFailedLeases " + stats1.TotalFailedLeases); + _outputHelper.WriteLine("CurrentAvailablePermits " + stats1.CurrentAvailablePermits); + _outputHelper.WriteLine("CurrentQueuedCount " + stats1.CurrentQueuedCount); token1.Cancel(); await Task.Delay(TimeSpan.FromSeconds(10)); _outputHelper.WriteLine("------------------------"); var statistics2 = await rateLimiter.GetStatisticsAsync(); + statistics2.ShouldNotBeNull(); + var stats2 = statistics2!; - _outputHelper.WriteLine("TotalSuccessfulLeases " + statistics2.TotalSuccessfulLeases); - _outputHelper.WriteLine("TotalFailedLeases " + statistics2.TotalFailedLeases); - _outputHelper.WriteLine("CurrentAvailablePermits " + statistics2.CurrentAvailablePermits); - _outputHelper.WriteLine("CurrentQueuedCount " + statistics2.CurrentQueuedCount); - + _outputHelper.WriteLine("TotalSuccessfulLeases " + stats2.TotalSuccessfulLeases); + _outputHelper.WriteLine("TotalFailedLeases " + stats2.TotalFailedLeases); + _outputHelper.WriteLine("CurrentAvailablePermits " + stats2.CurrentAvailablePermits); + _outputHelper.WriteLine("CurrentQueuedCount " + stats2.CurrentQueuedCount); - statistics1.TotalSuccessfulLeases.Should().Be(permit); - statistics1.CurrentQueuedCount.Should().Be(extra); - statistics1.CurrentAvailablePermits.Should().Be(0); + stats1.TotalSuccessfulLeases.ShouldBe(permit); + stats1.CurrentQueuedCount.ShouldBe(extra); + stats1.CurrentAvailablePermits.ShouldBe(0); - statistics2.TotalSuccessfulLeases.Should().Be(permit + extra); - statistics2.CurrentQueuedCount.Should().Be(0); - statistics2.CurrentAvailablePermits.Should().Be(permit); + stats2.TotalSuccessfulLeases.ShouldBe(permit + extra); + stats2.CurrentQueuedCount.ShouldBe(0); + stats2.CurrentAvailablePermits.ShouldBe(permit); } [Fact] @@ -125,20 +127,22 @@ await rateLimiter.Configure(new FixedWindowRateLimiterOptions await Task.WhenAll(tasks); sw.Stop(); var statistics = await rateLimiter.GetStatisticsAsync(); - statistics.TotalSuccessfulLeases.Should().Be(success); + statistics.ShouldNotBeNull(); + var stats = statistics!; + stats.TotalSuccessfulLeases.ShouldBe(success); _outputHelper.WriteLine("Samples " + summer.Samples.Count); - _outputHelper.WriteLine("TotalSuccessfulLeases " + statistics.TotalSuccessfulLeases); - _outputHelper.WriteLine("TotalFailedLeases " + statistics.TotalFailedLeases); - _outputHelper.WriteLine("CurrentAvailablePermits " + statistics.CurrentAvailablePermits); - _outputHelper.WriteLine("CurrentQueuedCount " + statistics.CurrentQueuedCount); + _outputHelper.WriteLine("TotalSuccessfulLeases " + stats.TotalSuccessfulLeases); + _outputHelper.WriteLine("TotalFailedLeases " + stats.TotalFailedLeases); + _outputHelper.WriteLine("CurrentAvailablePermits " + stats.CurrentAvailablePermits); + _outputHelper.WriteLine("CurrentQueuedCount " + stats.CurrentQueuedCount); _outputHelper.WriteLine("Average " + summer.Average()); foreach (var item in summer.Samples) _outputHelper.WriteLine(item.Key.ToString("O") + " " + item.Value); - summer.Average().Should().Be(permit); + summer.Average().ShouldBe(permit); } [Fact] @@ -182,20 +186,22 @@ await rateLimiter.Configure(new SlidingWindowRateLimiterOptions await Task.WhenAll(tasks); sw.Stop(); var statistics = await rateLimiter.GetStatisticsAsync(); - statistics.TotalSuccessfulLeases.Should().Be(success); + statistics.ShouldNotBeNull(); + var stats = statistics!; + stats.TotalSuccessfulLeases.ShouldBe(success); _outputHelper.WriteLine("Samples " + summer.Samples.Count); - _outputHelper.WriteLine("TotalSuccessfulLeases " + statistics.TotalSuccessfulLeases); - _outputHelper.WriteLine("TotalFailedLeases " + statistics.TotalFailedLeases); - _outputHelper.WriteLine("CurrentAvailablePermits " + statistics.CurrentAvailablePermits); - _outputHelper.WriteLine("CurrentQueuedCount " + statistics.CurrentQueuedCount); + _outputHelper.WriteLine("TotalSuccessfulLeases " + stats.TotalSuccessfulLeases); + _outputHelper.WriteLine("TotalFailedLeases " + stats.TotalFailedLeases); + _outputHelper.WriteLine("CurrentAvailablePermits " + stats.CurrentAvailablePermits); + _outputHelper.WriteLine("CurrentQueuedCount " + stats.CurrentQueuedCount); _outputHelper.WriteLine("Average " + summer.Average()); foreach (var item in summer.Samples) _outputHelper.WriteLine(item.Key.ToString("O") + " " + item.Value); - summer.Average().Should().Be(permit); + summer.Average().ShouldBe(permit); } [Fact] @@ -239,19 +245,21 @@ await rateLimiter.Configure(new TokenBucketRateLimiterOptions await Task.WhenAll(tasks); sw.Stop(); var statistics = await rateLimiter.GetStatisticsAsync(); - statistics.TotalSuccessfulLeases.Should().Be(success); + statistics.ShouldNotBeNull(); + var stats = statistics!; + stats.TotalSuccessfulLeases.ShouldBe(success); _outputHelper.WriteLine("Samples " + summer.Samples.Count); - _outputHelper.WriteLine("TotalSuccessfulLeases " + statistics.TotalSuccessfulLeases); - _outputHelper.WriteLine("TotalFailedLeases " + statistics.TotalFailedLeases); - _outputHelper.WriteLine("CurrentAvailablePermits " + statistics.CurrentAvailablePermits); - _outputHelper.WriteLine("CurrentQueuedCount " + statistics.CurrentQueuedCount); + _outputHelper.WriteLine("TotalSuccessfulLeases " + stats.TotalSuccessfulLeases); + _outputHelper.WriteLine("TotalFailedLeases " + stats.TotalFailedLeases); + _outputHelper.WriteLine("CurrentAvailablePermits " + stats.CurrentAvailablePermits); + _outputHelper.WriteLine("CurrentQueuedCount " + stats.CurrentQueuedCount); _outputHelper.WriteLine("Average " + summer.Average()); foreach (var item in summer.Samples) _outputHelper.WriteLine(item.Key.ToString("O") + " " + item.Value); - summer.Average().Should().Be(permit); + summer.Average().ShouldBe(permit); } } \ No newline at end of file diff --git a/ManagedCode.Orleans.RateLimiting.Tests/SignalRTests.cs b/ManagedCode.Orleans.RateLimiting.Tests/SignalRTests.cs index 2acb12a..bb6cd13 100644 --- a/ManagedCode.Orleans.RateLimiting.Tests/SignalRTests.cs +++ b/ManagedCode.Orleans.RateLimiting.Tests/SignalRTests.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using Shouldly; using ManagedCode.Orleans.RateLimiting.Tests.Cluster; using ManagedCode.Orleans.RateLimiting.Tests.TestApp; using Microsoft.AspNetCore.SignalR.Client; @@ -27,7 +27,7 @@ public async Task Some() { var anonymousHub11 = _testApp.CreateSignalRClient(nameof(TestHub)); await anonymousHub11.StartAsync(); - anonymousHub11.State.Should().Be(HubConnectionState.Connected); + anonymousHub11.State.ShouldBe(HubConnectionState.Connected); } catch (Exception e) { @@ -37,7 +37,7 @@ public async Task Some() var anonymousHub = _testApp.CreateSignalRClient(nameof(TestHub)); await anonymousHub.StartAsync(); - anonymousHub.State.Should().Be(HubConnectionState.Connected); + anonymousHub.State.ShouldBe(HubConnectionState.Connected); } } \ No newline at end of file diff --git a/ManagedCode.Orleans.RateLimiting.Tests/SlidingWindowRateLimiterGrainTests.cs b/ManagedCode.Orleans.RateLimiting.Tests/SlidingWindowRateLimiterGrainTests.cs index 123c717..738babf 100644 --- a/ManagedCode.Orleans.RateLimiting.Tests/SlidingWindowRateLimiterGrainTests.cs +++ b/ManagedCode.Orleans.RateLimiting.Tests/SlidingWindowRateLimiterGrainTests.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using Shouldly; using ManagedCode.Orleans.RateLimiting.Tests.Cluster; using ManagedCode.Orleans.RateLimiting.Tests.Cluster.Grains.Interfaces; using Xunit; @@ -40,9 +40,9 @@ public async Task GrainIdTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().Be(count); - errors.Should().Be(0); + (success + errors).ShouldBe(count); + success.ShouldBe(count); + errors.ShouldBe(0); } [Fact] @@ -67,8 +67,8 @@ public async Task KeyTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } [Fact] @@ -93,8 +93,8 @@ public async Task TypeTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } [Fact] @@ -119,7 +119,7 @@ public async Task RateLimiterConfigTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } } \ No newline at end of file diff --git a/ManagedCode.Orleans.RateLimiting.Tests/TokenBucketRateLimiterGrainTests.cs b/ManagedCode.Orleans.RateLimiting.Tests/TokenBucketRateLimiterGrainTests.cs index 8ba9053..fbdf236 100644 --- a/ManagedCode.Orleans.RateLimiting.Tests/TokenBucketRateLimiterGrainTests.cs +++ b/ManagedCode.Orleans.RateLimiting.Tests/TokenBucketRateLimiterGrainTests.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using Shouldly; using ManagedCode.Orleans.RateLimiting.Tests.Cluster; using ManagedCode.Orleans.RateLimiting.Tests.Cluster.Grains.Interfaces; using Xunit; @@ -40,9 +40,9 @@ public async Task GrainIdTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().Be(count); - errors.Should().Be(0); + (success + errors).ShouldBe(count); + success.ShouldBe(count); + errors.ShouldBe(0); } [Fact] @@ -67,8 +67,8 @@ public async Task KeyTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } [Fact] @@ -93,8 +93,8 @@ public async Task TypeTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } [Fact] @@ -119,7 +119,7 @@ public async Task RateLimiterConfigTests() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } } \ No newline at end of file diff --git a/ManagedCode.Orleans.RateLimiting.Tests/WebApiTests.cs b/ManagedCode.Orleans.RateLimiting.Tests/WebApiTests.cs index 92dc28f..9596c07 100644 --- a/ManagedCode.Orleans.RateLimiting.Tests/WebApiTests.cs +++ b/ManagedCode.Orleans.RateLimiting.Tests/WebApiTests.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using Shouldly; using ManagedCode.Orleans.RateLimiting.Tests.Cluster; using Xunit; using Xunit.Abstractions; @@ -46,7 +46,7 @@ public async Task ControllerTest() await Task.WhenAll(tasks); - (success + errors).Should().Be(count); - success.Should().BeLessThan(errors); + (success + errors).ShouldBe(count); + success.ShouldBeLessThan(errors); } } \ No newline at end of file diff --git a/README.md b/README.md index 77703dc..7a15cf0 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,19 @@ # Orleans.RateLimiting +[![CI](https://github.com/managedcode/Orleans.RateLimiting/actions/workflows/ci.yml/badge.svg)](https://github.com/managedcode/Orleans.RateLimiting/actions/workflows/ci.yml) +[![CodeQL](https://github.com/managedcode/Orleans.RateLimiting/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/managedcode/Orleans.RateLimiting/actions/workflows/codeql-analysis.yml) +[![Release](https://github.com/managedcode/Orleans.RateLimiting/actions/workflows/release.yml/badge.svg)](https://github.com/managedcode/Orleans.RateLimiting/actions/workflows/release.yml) + Orleans.RateLimiting is a library for Microsoft Orleans that provides a set of rate limiting algorithms for controlling the flow of requests in your distributed applications. It is designed to be easy to use and to integrate with your Orleans-based applications seamlessly. With Orleans.RateLimiting, you can ensure your applications handle a safe number of requests without the risk of overloading your system resources. -RateLimiting -on [learn.microsoft.com](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-7.0) + +> **Heads up!** Orleans.RateLimiting now targets **.NET 9** and uses the latest Orleans 9.2 packages, ensuring compatibility with the newest Orleans primitives and ASP.NET Core 9 runtime. + +Learn more about the core rate limiting concepts on +[learn.microsoft.com](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-9.0) and [devblogs.microsoft.com](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/) ## Features @@ -22,6 +29,8 @@ and [devblogs.microsoft.com](https://devblogs.microsoft.com/dotnet/announcing-ra ## Installation +Orleans.RateLimiting ships as a pair of NuGet packages that target .NET 9. Install them from the command line or your favourite IDE: + You can install Orleans.RateLimiting via NuGet Package Manager: ```sh @@ -44,7 +53,7 @@ clientBuilder.AddOrleansRateLimiting(); siloBuilder.AddOrleansRateLimiting(); ``` -Also if you would like to use incoming filter and **Attributes**, you have to add default options for Limiter: +Also if you would like to use the incoming filter and **Attributes**, you have to add default options for Limiter: ```csharp //Add default options and IncomingFilter @@ -137,7 +146,7 @@ var slidingWindowRateLimiter = _factory.GetSlidingWindowRateLimiter("key"); var tokenBucketRateLimiter = _factory.GetTokenBucketRateLimiter("key"); ``` -### Attrubutes for Grains +### Attributes for Grains You can use attributes to decorate your grain methods and apply rate limiting to them. Make sure you check configuration section for default options. @@ -168,7 +177,7 @@ public class TestFixedWindowRateLimiterGrain : Grain, ITestFixedWindowRateLimite } ``` -### Attrubutes for WebAPI +### Attributes for WebAPI You can define OrleansRateLimiterOptions with specific name. ``` cs @@ -195,7 +204,7 @@ builder.Services.AddOrleansRateLimiterOptions("Authorized", new FixedWindowRateL ``` -then add middelware +then add middleware ``` cs app.UseOrleansIpRateLimiting(); // as earlier as possible ..... @@ -222,8 +231,18 @@ public async Task> GetSome() return "OK"; } ``` +## Build & test locally + +To work with the repository locally you need the .NET 9 SDK. After cloning the repository run: + +```bash +dotnet restore ManagedCode.Orleans.RateLimiting.sln +dotnet build ManagedCode.Orleans.RateLimiting.sln --configuration Release +dotnet test ManagedCode.Orleans.RateLimiting.Tests/ManagedCode.Orleans.RateLimiting.Tests.csproj --configuration Release +``` + ## Contributing We welcome contributions to Orleans.RateLimiting! Feel free to submit issues, feature requests, and pull requests on -the [GitHub repository](https://github.com/yourusername/Orleans.RateLimiter). +the [GitHub repository](https://github.com/managedcode/Orleans.RateLimiting).