Skip to content

Commit

Permalink
Number of sent messages quota (#189)
Browse files Browse the repository at this point in the history
* Number of sent Messages quota: Building Blocks (#146)

* feat: fix pipelinebehaviors, add tokenEnforcer, add Common.Infrastructure

* refactor: StatusRepository → StatusesRepository, make repo non nullable in quotaenforcer

* fix: Add Common.Infrastructure to Dockerfiles

* Use Dapper instead of DbContext

* feat: make quota error return a 429. Add Data. Improve condition for exception throwing

* fix: remove bad metric AnotherMetricKey

* refactor: renames. Load Configuration of DapperContext dirrectly on AddMetricStatusesRepository

* fix: remove needless comments/attributes

* feat: make httperror have data from QuotaExhaustedException

* refactor: var names, file locations, remove comments

* fix: remove needless private setters

* test: add simple test for QuotaExhaustedException

* fix: bad comparison was checking for non-expired metric statuses

* test: add further tests to QuotaEnforcerBehavior

* fix: mock → stub

* fix: ensure single-responsability tests

* fix: test command registered with wrong MetricKey(s)

* refactor: extract userContextStub to test class

* fix: ensure we stay with FluentAssertions

* fix: behavior could not be applied to responseless queries

* fix: needless connectionstring validation

* fix: IsExhaustedUntil was not nullable. Further renames

* fix: test-required methods/ctors are public

* refactor: pass connectionstring loading to ConsumerApi Program

* refactor: separate testdata in behavior tests

* refactor: let Dapper manage connections instead of opening a new one each time

* fix: load configuration for MetricStatusesDapperContext

* refactor: move NonGenericAsyncFunctionAssertionsExtensions to UnitTestTools

* refactor: remove QuotaEnforcerBehavior and tests before moving on with TDD

* refactor: remove using shortcut for MetricStatus

* refactor: QuotaExhaustedException will feed all the exhausted metrics to the http error

* fix: bad metricAttribute in tests could deceive developers

* test: add first test and SUT generator

* test: add all tests for QuotaEnforcerBehavior

* fix: tests comparing object instead of property

* feat: add QuotaEnforcerBehavior

* fix: properties before ctor

* fix: returning ExhaustedMetricStatuses directly may lead to unwanted changes to the API

* fix: private methods before public methods

* fix: use a mock for nextHasRan instead of creating a new next fn for each test

* fix: MetricStatusesNoMatchStubRepository needs not a ctor

* fix: attributes after ctor

* refactor: remove usage of Domain reference

* refactor: simplify arrange for tests, rearrange tests and rename them

* fix: bad test names

* test: tests ensure the right number of exhaustedMetrics are returned

* refactor: use aninymous type for exception data

* fix: exception data attribute names must be hardcoded

* fix: tests should ensure elements exist before accessing them

* Number of sent messages quota: Quota.Application (#160)

* feat: add metricCalculator, implementation, and message repo

* feat: command RecalculateMetricStatuses, IdentityMetricExtensions, repos and calculatorFactory

* feat: add implementation to quotasrepo

* refactor: code reformat

* feat: add draft for quota.update

* refactor: domain logic to domain project

* fix: logic behind EndOfPeriod

* fix: period end/begin logic. Add tests. Meesages' access on quotas module

* fix: period calculations wrong for end of year

* fix: no need for reference grant

* feat: add grants to the postgres setup

* fix: prevent EF migration for read-only messages table on quotas

* fix: spacings, attribute/param names, EF methods usage

* refactor: use first letter in lambda

* refactor: tests readability

* refactor: removed unused attributes, add message to exception

* refactor: further renames

* fix: fetching quotas twice. Lack cancellationToken on MessagesRepository

* fix: domain logic in extension class

* fix: needless repository

* fix: MessageEntity configuration

* fix: QuotaPeriod min and max values

* test: UpdateExhaustion

* refactor: needless lines

* feat: add message queues to quotas module.

* fix: bad handler signature

* fix: event bus subscriptions

* chore: create test client on startup of AdminCli

* feat: add/fix required attributes to event, register repos and services, calls to findIdentities retrieve tierquotas too

* fix: remove changes to AdminUI file

* fix: no need to test quotas that get un-exhasuted due to lower newUsage

* Revert "fix: no need to test quotas that get un-exhasuted due to lower newUsage"

This reverts commit c33630e.

* test: update with newUsage under Quota.Max should unexhaust it

* fix: first day of the week was sunday

* refactor: utcNow → pivot

* refactor: remove unused method in IIdentitiesRepository

* refactor: findByIds → findByAddresses

* fix: method names, attribute names,

* fix: use includeAll

* chore: convert StronglyTypedId constructor to primary constructor

* refactor: introduce and use domain error and domain exception in BuildingBlocks.Domain

* refactor: Use ServiceCollectionMetricCalculatorFactory instead of ImetricCalculatorFactory

* fix: merge

* fix: quotaEnforcerBehavior runs on AdminAPI but no UserContext is present

* fix: test must receive list of IUserContext

* refactor: structure QuotaPeriodTests in Theory mode instead of repeating code

* refactor: minor changes to QuotaTests

* fix: consumer API would crash if no metrics were found for a certain identity

* refactor: quotaPeriods exception with descriptive message

* test: DateTimeExtensions such as .EndOfYear

* refactor: removed needless comment

* feat: add updateMetricStatus methods

* fix: needless creation of new MetricStatus, missing call to UpdateMetricStatus

* refactor: remove unused variable

* test: Quotas through Identities

* refactor: fix test data parameters order

* refactor: ServiceProviderMetricCalculatorFactory name

* refactor: IReadOnlyCollectionsmust be make readonly when returned

* refactor: needless where

* feat: split QuotaEnforcerBehavior

* fix: metricStatuses to be loaded via Dapper

* feat: AlwaysSuccessQuotaChecker

* Revert "fix: metricStatuses to be loaded via Dapper"

This reverts commit 9654ad9.

* fix: MetricStatus to be created even if key is not in quotas

* fix: bad name for IdentityTests file

* fix: needn't order for max, missing null metricStatus when not exhausted anymore

* fix: catch exception only in susceptible method

* test: Null MetricStatus.IsExhaustedUntil when Quotas are exhausted in the past

* refactor: remove unused UpdateAllMetricStatuses method

* fix: error messages & performance considerations

* refactor: add track to FindByAddresses

* refactor: Identity - initialize in ctor

* refactor: use service provider instead of direct DI for MetricCalculatorFactory

* test: add reference to tests on comment about individual quotas

* fix: missing update to interface

* refactor: ensure Test of Quotas via Identities uses Identities Domain methods exclusively.

* feat: implement AllQuotas to reduce pointf of failure where all quotas should be used.

* test: add more MetricStatuses to identity & identity withput quotas has null MetricStatuses' IsExhaustedUntil

* fix: QuotaEnforcerBehavior must be updated due to IQuotaChecker changes

* refactor: make comment more prominent

Co-authored-by: Timo Notheisen <[email protected]>

* refactor: renames, method orders, etc.

* refactor: Register NumberOfSentMessagesMetricCalculator and load it with sp

* fix: remove needless MetricStatusEntityTypeConfiguration for Dapper-accessed entity.

* refactor: remove Quota.IsExhaustedUntil

* test: ensure MetricStatus are set even when no quotas are exhausted

* refactor: singleOrDefault on MetricStatuses

* refactor: remove needless line

* refactor: restructure tests to reduce dependencies and cross-testing

* feat: add MetricStatus to database

* fix: do not use specific identity for tests

Co-authored-by: Timo Notheisen <[email protected]>

* refactor: QuotaCheckerImplTests

* fix: EndOfDay/Hour should not be specific to UtcNow

* refactor: minor renames

* refactor: do not use a Dictionary in the Identities.UpdateMetric method

* refactor: invert if

* chore: remove reference to MetricKey.TierQuotaDefinitions from migration

* refactor: undo metricStatus migration

* fix: Identity ignored MetricStatuses

* refactor: readd MetricStatus migration after entityconfiguration fix

* test: QuotaCheckerImpl CheckQuotaExhaustion should return isSuccess false when one metric is exhausted

* chore: move IdentityTests to Domain tests

* chore: move test doubles to separate folder

* feat: add Owner to MetricStatus ctor. Update migrations

* fix: remove migration script missing quotas module

* fix: missing reference to test doubles

* fix: untracked identities could not be updated correctly

* chore: remove unused 'using'

* fix: bad namespace on Domain class

* fix: minor notes by resharper

* chore: improve tests

* fix: readd Consumer API after merge problem

* chore: delete accidentally added plantuml files

* fix: Mediatr not installed for Backbone ArchUnit tests

* chore: readd reference from ArchUnit tests to ConsumerApi

* chore: fix casing of ConsumerApi folder

* fix: ConsumerApi project path

* chore: remove unused copy of DateTimeAssertionsExtensions from Quotas.Domain.Tests

* test: add validation of year, month and day

* chore: remove Backbone.sln.DotSettings

* chore: simplify QuotaEnforcerBehaviorTests

* feat: add ExhaustionDateValueConverter

* fix: names of paramaters of ApplyQuotasForMetricsAttribute not being parsed correctly. Dapper not yet used.

* chore: cleanup QuotaEnforcerBehavior

* chore: cleanup QuotaEnforcerBehavior

* chore: add missing import

* chore: cleanup QuotaEnforcerBehavior

* chore: remove unused variables and simplify MetricStatusesRepository

* test: remove unnecessary assertions from DateTimeAssertionsExtensions

* chore: remove redundant .ToList

* test: add tests for ExhaustionDate

* test: move fields to top

* test: fix method name

* chore: remove commented out code

* test: change some test data

* refactor: use empty private ctor for Dapper on MetricStatus

* refactor: make MetricKey a record

* test: improve QuotaEnforcerBehaviorTests

* fix: thowing GenericApplicationError instead of DomainError on QuotaExhausted

* chore: update MetricStatus migration

* fix: missing schema anotation on Quotas.Messages and ValueConverter for Dapper

* feat: add Postgres support to DapperContext

* feat: add MetricStatus migration for Postgres

---------

Co-authored-by: Timo Notheisen <[email protected]>
Co-authored-by: Timo Notheisen <[email protected]>

* chore: fix project references

* fix: AdminUI Dockerfile missing Common.Infrastructure

---------

Co-authored-by: Timo Notheisen <[email protected]>
Co-authored-by: Timo Notheisen <[email protected]>
  • Loading branch information
3 people authored Jul 5, 2023
1 parent 2fa03d2 commit bf18b33
Show file tree
Hide file tree
Showing 153 changed files with 2,729 additions and 444 deletions.
1 change: 1 addition & 0 deletions AdminUi/src/AdminUi/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ COPY ["Modules/Quotas/src/Quotas.ConsumerApi/Quotas.ConsumerApi.csproj", "Module
COPY ["Modules/Quotas/src/Quotas.Infrastructure.Database.Postgres/Quotas.Infrastructure.Database.Postgres.csproj", "Modules/Quotas/src/Quotas.Infrastructure.Database.Postgres/"]
COPY ["Modules/Quotas/src/Quotas.Infrastructure/Quotas.Infrastructure.csproj", "Modules/Quotas/src/Quotas.Infrastructure/"]
COPY ["Modules/Quotas/src/Quotas.Infrastructure.Database.SqlServer/Quotas.Infrastructure.Database.SqlServer.csproj", "Modules/Quotas/src/Quotas.Infrastructure.Database.SqlServer/"]
COPY ["Common/src/Common.Infrastructure/Common.Infrastructure.csproj", "Common/src/Common.Infrastructure/"]
RUN dotnet restore "AdminUi/src/AdminUi/AdminUi.csproj"
COPY . .
WORKDIR /src/AdminUi/src/AdminUi
Expand Down
5 changes: 5 additions & 0 deletions AdminUi/src/AdminUi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
using Autofac.Extensions.DependencyInjection;
using Backbone.Infrastructure.EventBus;
using Backbone.Modules.Devices.Application;
using Backbone.Modules.Quotas.Application.QuotaCheck;
using Enmeshed.BuildingBlocks.API.Extensions;
using Enmeshed.BuildingBlocks.Application.QuotaCheck;
using Enmeshed.Common.Infrastructure;
using Enmeshed.Tooling.Extensions;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -55,6 +58,8 @@ static void ConfigureServices(IServiceCollection services, IConfiguration config
.AddQuotas(parsedConfiguration.Modules.Quotas)
.AddHealthChecks();

services.AddTransient<IQuotaChecker, AlwaysSuccessQuotaChecker>();

services.AddEventBus(parsedConfiguration.Infrastructure.EventBus);
}

Expand Down
1 change: 1 addition & 0 deletions Backbone.Tests.ArchUnit/Backbone.Tests.ArchUnit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MediatR" Version="12.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="TngTech.ArchUnitNET.xUnit" Version="0.10.5" />
Expand Down
30 changes: 19 additions & 11 deletions Backbone.sln
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33122.133
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsumerApi", "ConsumerApi\ConsumerApi.csproj", "{1236D230-99E8-4352-9D93-94EEDCC0E064}"
ProjectSection(ProjectDependencies) = postProject
{A326741C-C030-4535-BA73-2B508E337CF0} = {A326741C-C030-4535-BA73-2B508E337CF0}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Challenges", "Challenges", "{DF1C4335-5043-4365-B753-6A8698528E4B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildingBlocks", "BuildingBlocks", "{13BA71F8-58D7-45F6-997D-4DE87E7B41F3}"
Expand Down Expand Up @@ -276,16 +270,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A8C20813-97C
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F9090A43-9FEB-4208-A34D-A0DCB2A6C701}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{B147B99D-3FC7-4D99-A3B7-796AA9FA126C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AC21BC09-864E-4C35-A8BE-8575A9C14134}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Infrastructure", "Common\src\Common.Infrastructure\Common.Infrastructure.csproj", "{67A5BBF4-88FF-49D7-9EAE-5A37BBAE084C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsumerApi", "ConsumerApi\ConsumerApi.csproj", "{F2823AB7-4361-437F-A5C7-D06540BCB362}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1236D230-99E8-4352-9D93-94EEDCC0E064}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1236D230-99E8-4352-9D93-94EEDCC0E064}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1236D230-99E8-4352-9D93-94EEDCC0E064}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1236D230-99E8-4352-9D93-94EEDCC0E064}.Release|Any CPU.Build.0 = Release|Any CPU
{B9616684-0252-428D-8D64-CAFC8708F7D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9616684-0252-428D-8D64-CAFC8708F7D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9616684-0252-428D-8D64-CAFC8708F7D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -682,12 +680,19 @@ Global
{17301932-8889-4564-9AB7-A8816A91B190}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17301932-8889-4564-9AB7-A8816A91B190}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17301932-8889-4564-9AB7-A8816A91B190}.Release|Any CPU.Build.0 = Release|Any CPU
{67A5BBF4-88FF-49D7-9EAE-5A37BBAE084C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{67A5BBF4-88FF-49D7-9EAE-5A37BBAE084C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{67A5BBF4-88FF-49D7-9EAE-5A37BBAE084C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{67A5BBF4-88FF-49D7-9EAE-5A37BBAE084C}.Release|Any CPU.Build.0 = Release|Any CPU
{F2823AB7-4361-437F-A5C7-D06540BCB362}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2823AB7-4361-437F-A5C7-D06540BCB362}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2823AB7-4361-437F-A5C7-D06540BCB362}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2823AB7-4361-437F-A5C7-D06540BCB362}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{1236D230-99E8-4352-9D93-94EEDCC0E064} = {E6BFD37D-335D-4975-B661-BC67FB82F5AA}
{DF1C4335-5043-4365-B753-6A8698528E4B} = {0EAF57B8-E97C-469E-A74B-596D78C978B2}
{C16FE68A-6CDC-4074-A8E7-8CD16EBDDFAA} = {DF1C4335-5043-4365-B753-6A8698528E4B}
{06D714AE-EDF4-421C-9340-EDA6FCDF491F} = {13BA71F8-58D7-45F6-997D-4DE87E7B41F3}
Expand Down Expand Up @@ -812,6 +817,9 @@ Global
{17301932-8889-4564-9AB7-A8816A91B190} = {A8C20813-97C8-42A9-B45A-4A0D650DA647}
{A8C20813-97C8-42A9-B45A-4A0D650DA647} = {285E30DF-68B4-4A13-981E-E8BAB05489F5}
{F9090A43-9FEB-4208-A34D-A0DCB2A6C701} = {285E30DF-68B4-4A13-981E-E8BAB05489F5}
{AC21BC09-864E-4C35-A8BE-8575A9C14134} = {B147B99D-3FC7-4D99-A3B7-796AA9FA126C}
{67A5BBF4-88FF-49D7-9EAE-5A37BBAE084C} = {AC21BC09-864E-4C35-A8BE-8575A9C14134}
{F2823AB7-4361-437F-A5C7-D06540BCB362} = {E6BFD37D-335D-4975-B661-BC67FB82F5AA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1F3BD2C6-7CB3-450F-A21A-23EA520D5B7A}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Net;
using System;
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
using Enmeshed.BuildingBlocks.API.Extensions;
using Backbone.Modules.Devices.Domain;
using Enmeshed.BuildingBlocks.Application.Abstractions.Exceptions;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.AspNetCore.Hosting;
Expand All @@ -11,6 +13,8 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ApplicationException = Enmeshed.BuildingBlocks.Application.Abstractions.Exceptions.ApplicationException;
using Enmeshed.BuildingBlocks.Domain;
using Enmeshed.BuildingBlocks.Domain.Errors;

namespace Enmeshed.BuildingBlocks.API.Mvc.ExceptionFilters;

Expand Down Expand Up @@ -41,11 +45,20 @@ public override void OnException(ExceptionContext context)
_logger.LogInformation(
$"An {nameof(ApplicationException)} occurred. Error Code: {applicationException.Code}. Error message: {applicationException.Message}");

httpError = CreateHttpErrorApplicationException(applicationException);
httpError = CreateHttpErrorForApplicationException(applicationException);

context.HttpContext.Response.StatusCode =
(int)GetStatusCodeForApplicationException(applicationException);

break;
case DomainException domainException:
_logger.LogInformation(
$"A {nameof(DomainException)} occurred. Error Code: {domainException.Code}. Error message: {domainException.Message}");

httpError = CreateHttpErrorForDomainException(domainException);

context.HttpContext.Response.StatusCode = (int)GetStatusCodeForDomainException(domainException);

break;
case BadHttpRequestException _:
_logger.LogInformation(
Expand Down Expand Up @@ -78,27 +91,62 @@ public override void OnException(ExceptionContext context)
});
}

private HttpError CreateHttpErrorApplicationException(ApplicationException applicationException)
private HttpError CreateHttpErrorForApplicationException(ApplicationException applicationException)
{
var httpError = HttpError.ForProduction(
applicationException.Code,
applicationException.Message,
"", // TODO: add documentation link
data: GetCustomData(applicationException)
);

return httpError;
}

private HttpError CreateHttpErrorForDomainException(DomainException domainException)
{
var httpError = HttpError.ForProduction(
domainException.Code,
domainException.Message,
"" // TODO: add documentation link
);

return httpError;
}

private dynamic? GetCustomData(ApplicationException applicationException)
{
if (applicationException is QuotaExhaustedException quotaExhautedException)
{
return quotaExhautedException.ExhaustedMetricStatuses.Select(m => new
{
MetricKey = m.MetricKey,
IsExhaustedUntil = m.IsExhaustedUntil
});
}

return null;
}

private HttpStatusCode GetStatusCodeForApplicationException(ApplicationException exception)
{
return exception switch
{
NotFoundException _ => HttpStatusCode.NotFound,
ActionForbiddenException _ => HttpStatusCode.Forbidden,
QuotaExhaustedException _ => HttpStatusCode.TooManyRequests,
_ => HttpStatusCode.BadRequest
};
}

private HttpStatusCode GetStatusCodeForDomainException(DomainException exception)
{
if (exception.Code == GenericDomainErrors.NotFound().Code)
return HttpStatusCode.NotFound;

return HttpStatusCode.BadRequest;
}

private HttpError CreateHttpErrorForUnexpectedException(ExceptionContext context)
{
HttpError httpError;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ public static ApplicationError Unauthorized()
public static ApplicationError Forbidden()
{
return new ApplicationError("error.platform.forbidden",
"You are not allowed to perform this action. This could be due to insufficient privileges or an exhausted quota.");
"You are not allowed to perform this action due to insufficient privileges.");
}

public static ApplicationError QuotaExhausted()
{
return new ApplicationError("error.platform.quotaExhausted",
"You are not allowed to perform this action because one or more quotas have been exhausted.");
}

public static class Validation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Enmeshed.BuildingBlocks.Domain;

namespace Enmeshed.BuildingBlocks.Application.Abstractions.Exceptions;

public class QuotaExhaustedException : ApplicationException
{
public QuotaExhaustedException(MetricStatus[] exhaustedMetricStatuses) : base(GenericApplicationErrors.QuotaExhausted())
{
ExhaustedMetricStatuses = exhaustedMetricStatuses.Select(it=> new ExhaustedMetricStatus(it.MetricKey, it.IsExhaustedUntil));
}

public IEnumerable<ExhaustedMetricStatus> ExhaustedMetricStatuses { get; }
}

public class ExhaustedMetricStatus
{
public ExhaustedMetricStatus(MetricKey metricKey, DateTime? isExhaustedUntil)
{
MetricKey = metricKey;
IsExhaustedUntil = isExhaustedUntil.Value;
}

public MetricKey MetricKey { get; }
public DateTime IsExhaustedUntil { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Enmeshed.BuildingBlocks.Domain;

namespace Enmeshed.BuildingBlocks.Application.Attributes;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ApplyQuotasForMetricsAttribute : Attribute
{
private MetricKey[] MetricKeys { get; }

public ApplyQuotasForMetricsAttribute(params string[] metricKeys)
{
MetricKeys = metricKeys.Select(it=> new MetricKey(it)).ToArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@


<ItemGroup>
<ProjectReference Include="..\..\..\Common\src\Common.Infrastructure\Common.Infrastructure.csproj" />
<ProjectReference Include="..\BuildingBlocks.Application.Abstractions\BuildingBlocks.Application.Abstractions.csproj" />
<ProjectReference Include="..\Tooling\Tooling.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using MediatR;
using Enmeshed.BuildingBlocks.Application.Abstractions.Exceptions;
using Enmeshed.BuildingBlocks.Application.Attributes;
using Enmeshed.BuildingBlocks.Domain;
using Enmeshed.BuildingBlocks.Application.QuotaCheck;
using System.Reflection;
using System.Collections.ObjectModel;

namespace Enmeshed.BuildingBlocks.Application.MediatR;
public class QuotaEnforcerBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
{
private readonly IQuotaChecker _quotaChecker;

public QuotaEnforcerBehavior(IQuotaChecker quotaChecker)
{
_quotaChecker = quotaChecker;
}

public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{

var attributes = request.GetType().CustomAttributes;

var applyQuotasForMetricsAttribute = attributes.FirstOrDefault(attribute => attribute.AttributeType == typeof(ApplyQuotasForMetricsAttribute));
if (applyQuotasForMetricsAttribute != null)
{
var metricKeys = new List<MetricKey>();
foreach (var customAttributeTypedArgument in applyQuotasForMetricsAttribute.ConstructorArguments) {
foreach (var element in (ReadOnlyCollection<CustomAttributeTypedArgument>) customAttributeTypedArgument.Value!)
{
metricKeys.Add(new MetricKey((element.Value as string)!));
}
}

var result = await _quotaChecker.CheckQuotaExhaustion(metricKeys.AsEnumerable());

if (!result.IsSuccess)
{
throw new QuotaExhaustedException(result.ExhaustedStatuses.ToArray());
}
}

var response = await next();
return response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Enmeshed.BuildingBlocks.Domain;

namespace Enmeshed.BuildingBlocks.Application.QuotaCheck;
public class AlwaysSuccessQuotaChecker : IQuotaChecker
{
public Task<CheckQuotaResult> CheckQuotaExhaustion(IEnumerable<MetricKey> metricKeys)
{
return Task.FromResult(CheckQuotaResult.Success());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Enmeshed.BuildingBlocks.Domain;

namespace Enmeshed.BuildingBlocks.Application.QuotaCheck;
public class CheckQuotaResult
{
public CheckQuotaResult(IEnumerable<MetricStatus> exhaustedStatuses)
{
ExhaustedStatuses = exhaustedStatuses;
IsSuccess = !exhaustedStatuses.Any();
}

public IEnumerable<MetricStatus> ExhaustedStatuses { get; }
public bool IsSuccess { get; internal set; }

public static CheckQuotaResult Success() => new(Enumerable.Empty<MetricStatus>());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using Enmeshed.BuildingBlocks.Domain;

namespace Enmeshed.BuildingBlocks.Application.QuotaCheck;
public interface IQuotaChecker
{
Task<CheckQuotaResult> CheckQuotaExhaustion(IEnumerable<MetricKey> metricKeys);
}
5 changes: 5 additions & 0 deletions BuildingBlocks/src/BuildingBlocks.Domain/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("BuildingBlocks.Application.Tests")]
[assembly: InternalsVisibleTo("Backbone.Modules.Quotas.Application.Tests")]
namespace Enmeshed.BuildingBlocks.Domain;
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@
<RootNamespace>Enmeshed.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Tooling\Tooling.csproj" />
</ItemGroup>

</Project>
19 changes: 19 additions & 0 deletions BuildingBlocks/src/BuildingBlocks.Domain/DomainException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Enmeshed.BuildingBlocks.Domain.Errors;

namespace Enmeshed.BuildingBlocks.Domain;

public class DomainException : Exception
{
public DomainException(DomainError error) : base(error.Message)
{
Code = error.Code;
}

public DomainException(DomainError error, Exception innerException) : base(error.Message,
innerException)
{
Code = error.Code;
}

public string Code { get; set; }
}
Loading

0 comments on commit bf18b33

Please sign in to comment.