From 47ed531e9d8542e96ec174d72cea7163fe74644b Mon Sep 17 00:00:00 2001 From: Jezz Santos Date: Sun, 23 Jun 2024 12:21:58 +1200 Subject: [PATCH] Refactorings. Adding permissions, supporting optional serialization, currencycodes, changes roles and features to be unique, fixed namespaces, and fixed templates --- .../inspectionProfiles/Project_Default.xml | 1 - src/Common.UnitTests/CurrencyCodesSpec.cs | 139 +++++++++++ .../Extensions/DictionaryExtensionsSpec.cs | 55 ++++- src/Common/Common.csproj | 1 + src/Common/CurrencyCodes.cs | 230 ++++++++++++++++++ src/Common/Error.cs | 14 +- src/Common/Extensions/DateTimeExtensions.cs | 36 +++ src/Common/Extensions/DictionaryExtensions.cs | 19 ++ src/Common/Extensions/StringExtensions.cs | 48 +++- src/Common/OptionalConverter.cs | 45 ++++ src/Common/OptionalConverterFactory.cs | 22 ++ src/Common/Permission.cs | 69 ++++++ src/Common/Resources.Designer.cs | 18 ++ src/Common/Resources.resx | 7 +- .../ValueObjects/SingleValueObjectBase.cs | 3 + .../Authorization/PlatformFeatures.cs | 15 +- .../Authorization/TenantFeatures.cs | 13 +- .../Authorization/TenantRoles.cs | 8 +- .../Validations/CommonValidations.cs | 2 +- .../IEncryptionService.cs | 2 +- .../{DomainServices => }/ITokensService.cs | 2 +- .../EndUsersApplicationSpec.cs | 2 +- ...ionsApplication.DomainEventHandlersSpec.cs | 2 +- .../InvitationsApplicationSpec.cs | 2 +- .../InvitationsApplication.cs | 2 +- .../EndUserRootSpec.cs | 2 +- src/EndUsersDomain/EndUserRoot.cs | 2 +- src/EndUsersInfrastructure/EndUsersModule.cs | 2 +- .../APIKeysApplicationSpec.cs | 2 +- .../PasswordCredentialsApplicationSpec.cs | 2 +- .../SSOProvidersServiceSpec.cs | 2 +- src/IdentityApplication/APIKeysApplication.cs | 2 +- .../SSOProvidersService.cs | 2 +- .../PasswordCredentialsApplication.cs | 4 +- .../PasswordCredentialRootSpec.cs | 2 +- .../SSOUserRootSpec.cs | 2 +- src/IdentityDomain/PasswordCredentialRoot.cs | 2 +- src/IdentityDomain/SSOUserRoot.cs | 2 +- .../JWTTokensServiceSpec.cs | 6 +- .../DomainServices/EmailAddressServiceSpec.cs | 2 +- .../ApplicationServices/JWTTokensService.cs | 2 +- src/IdentityInfrastructure/IdentityModule.cs | 2 +- .../DomainServices/AesEncryptionService.cs | 2 +- .../DomainServices/TenantSettingService.cs | 2 +- .../DomainServices/TokensService.cs | 2 +- .../AuthorizeAttributeSpec.cs | 34 +-- .../AuthNApiSpec.cs | 2 +- .../AuthZApiSpec.cs | 2 +- .../MultiTenancySpec.cs | 2 +- .../HttpConstants.cs | 3 +- .../Auth/HMACAuthenticationHandlerSpec.cs | 4 +- ...FeaturesAuthorizationPolicyProviderSpec.cs | 2 +- .../Pipeline/CSRFService.cs | 2 +- .../Pipeline/CSRFTokenPair.cs | 2 +- src/SaaStack.sln.DotSettings | 34 ++- .../MinimalApiMediatRGeneratorSpec.cs | 14 +- .../WebApiAssemblyVisitorSpec.cs | 6 +- .../.template.config/template.json | 5 +- .../{SubDomainName}s/{SubDomainName}sApi.cs | 8 +- .../Notifications/{SubDomainName}Notifier.cs | 8 + .../ReadModels/{SubDomainName}Projection.cs | 40 +++ ...ameModule.cs => {SubDomainName}sModule.cs} | 18 +- src/Tools.Templates/Tools.Templates.csproj | 1 + src/WebsiteHost/BackEndForFrontEndModule.cs | 2 +- 64 files changed, 873 insertions(+), 119 deletions(-) create mode 100644 src/Common.UnitTests/CurrencyCodesSpec.cs create mode 100644 src/Common/CurrencyCodes.cs create mode 100644 src/Common/OptionalConverter.cs create mode 100644 src/Common/OptionalConverterFactory.cs create mode 100644 src/Common/Permission.cs rename src/Domain.Services.Shared/{DomainServices => }/IEncryptionService.cs (71%) rename src/Domain.Services.Shared/{DomainServices => }/ITokensService.cs (87%) create mode 100644 src/Tools.Templates/InfrastructureProject/Notifications/{SubDomainName}Notifier.cs create mode 100644 src/Tools.Templates/InfrastructureProject/Persistence/ReadModels/{SubDomainName}Projection.cs rename src/Tools.Templates/InfrastructureProject/{ProjectNameModule.cs => {SubDomainName}sModule.cs} (70%) diff --git a/src/.idea/.idea.SaaStack/.idea/inspectionProfiles/Project_Default.xml b/src/.idea/.idea.SaaStack/.idea/inspectionProfiles/Project_Default.xml index 37844881..26326669 100644 --- a/src/.idea/.idea.SaaStack/.idea/inspectionProfiles/Project_Default.xml +++ b/src/.idea/.idea.SaaStack/.idea/inspectionProfiles/Project_Default.xml @@ -4,7 +4,6 @@ diff --git a/src/Common.UnitTests/CurrencyCodesSpec.cs b/src/Common.UnitTests/CurrencyCodesSpec.cs new file mode 100644 index 00000000..7792c47e --- /dev/null +++ b/src/Common.UnitTests/CurrencyCodesSpec.cs @@ -0,0 +1,139 @@ +using Common.Extensions; +using FluentAssertions; +using ISO._4217; +using Xunit; + +namespace Common.UnitTests; + +[Trait("Category", "Unit")] +public class CurrencyCodesSpec +{ + [Fact] + public void WhenExistsAndUnknown_ThenReturnsFalse() + { + var result = CurrencyCodes.Exists("notacurrencycode"); + + result.Should().BeFalse(); + } + + [Fact] + public void WhenExistsByCode_ThenReturnsTrue() + { + var result = CurrencyCodes.Exists(CurrencyCodes.Default.Code); + + result.Should().BeTrue(); + } + + [Fact] + public void WhenExistsByNumeric_ThenReturnsTrue() + { + var result = CurrencyCodes.Exists(CurrencyCodes.Default.Numeric); + + result.Should().BeTrue(); + } + + [Fact] + public void WhenFindAndUnknown_ThenReturnsNull() + { + var result = CurrencyCodes.Find("notacurrencycode"); + + result.Should().BeNull(); + } + + [Fact] + public void WhenFindByCode_ThenReturnsTrue() + { + var result = CurrencyCodes.Find(CurrencyCodes.Default.Code); + + result.Should().Be(CurrencyCodes.Default); + } + + [Fact] + public void WhenFindByNumeric_ThenReturnsTrue() + { + var result = CurrencyCodes.Find(CurrencyCodes.Default.Numeric); + + result.Should().Be(CurrencyCodes.Default); + } + + [Fact] + public void WhenFindForEveryCurrency_ThenReturnsCode() + { + var currencies = CurrencyCodesResolver.Codes + .Where(cur => cur.Code.HasValue()) + .ToList(); + foreach (var currency in currencies) + { + var result = CurrencyCodes.Find(currency.Code); + + result.Should().NotBeNull($"{currency.Name} should have been found by Code"); + } + + foreach (var currency in currencies) + { + var result = CurrencyCodes.Find(currency.Num); + + result.Should().NotBeNull($"{currency.Name} should have been found by NumericCode"); + } + } + + [Fact] + public void WhenCreateIso4217_ThenReturnsInstance() + { + var result = CurrencyCodeIso4217.Create("ashortname", "analpha2", "100", CurrencyDecimalKind.TwoDecimal); + + result.ShortName.Should().Be("ashortname"); + result.Code.Should().Be("analpha2"); + result.Kind.Should().Be(CurrencyDecimalKind.TwoDecimal); + result.Numeric.Should().Be("100"); + } + + [Fact] + public void WhenEqualsAndNotTheSameNumeric_ThenReturnsFalse() + { + var currency1 = CurrencyCodeIso4217.Create("ashortname", "analpha2", "100", CurrencyDecimalKind.Unknown); + var currency2 = CurrencyCodeIso4217.Create("ashortname", "analpha2", "101", CurrencyDecimalKind.Unknown); + + var result = currency1 == currency2; + + result.Should().BeFalse(); + } + + [Fact] + public void WhenEqualsAndSameNumeric_ThenReturnsTrue() + { + var currency1 = CurrencyCodeIso4217.Create("ashortname1", "analpha21", "100", CurrencyDecimalKind.Unknown); + var currency2 = CurrencyCodeIso4217.Create("ashortname2", "analpha22", "100", CurrencyDecimalKind.Unknown); + + var result = currency1 == currency2; + + result.Should().BeTrue(); + } + + [Fact] + public void WhenToCurrencyWithAThousandAndOne_ThenReturnsUnitedStatesDollars() + { + var code = CurrencyCodes.UnitedStatesDollar.Code; + var result = CurrencyCodes.ToCurrency(code, 1001); + + result.Should().Be(10.01M); + } + + [Fact] + public void WhenToCurrencyWithAThousandAndOne_ThenReturnsKuwaitiDinars() + { + var code = CurrencyCodes.KuwaitiDinar.Code; + var result = CurrencyCodes.ToCurrency(code, 1001); + + result.Should().Be(1.001M); + } + + [Fact] + public void WhenToCurrencyWithAThousandAndOne_ThenReturnsChileanFomentos() + { + var code = CurrencyCodes.ChileanFomento.Code; + var result = CurrencyCodes.ToCurrency(code, 1001); + + result.Should().Be(0.1001M); + } +} \ No newline at end of file diff --git a/src/Common.UnitTests/Extensions/DictionaryExtensionsSpec.cs b/src/Common.UnitTests/Extensions/DictionaryExtensionsSpec.cs index b0ae4d5a..7a932b1c 100644 --- a/src/Common.UnitTests/Extensions/DictionaryExtensionsSpec.cs +++ b/src/Common.UnitTests/Extensions/DictionaryExtensionsSpec.cs @@ -177,7 +177,7 @@ public void WhenFromObjectDictionaryWithMatchingNullProperties_ThenReturnsUpdate [Fact] public void WhenToObjectDictionaryWithNullInstance_ThenReturnsEmpty() { - var result = ((TestMappingClass)null!).ToObjectDictionary(); + var result = ((TestMappingClass?)null).ToObjectDictionary(); result.Should().BeEmpty(); } @@ -223,6 +223,59 @@ public void WhenToObjectDictionaryWithInstanceWithDefaultValues_ThenReturnsPrope result[nameof(TestMappingClass.AnOptionalDateTimeProperty)].Should().Be(Optional.None); result[nameof(TestMappingClass.AnOptionalNullableDateTimeProperty)].Should().Be(Optional.None); } + + [Fact] + public void WhenToStringDictionaryWithNullInstance_ThenreturnsEmpty() + { + var result = ((TestMappingClass?)null).ToStringDictionary(); + + result.Should().BeEmpty(); + } + + [Fact] + public void WhenToStringDictionaryWithInstanceWithValues_ThenReturnsProperties() + { + var datum = DateTime.UtcNow; + var result = new TestMappingClass + { + ADateTimeProperty = datum, + AnOptionalStringProperty = "avalue", + AnOptionalNullableStringProperty = "avalue", + AnOptionalDateTimeProperty = datum, + AnOptionalNullableDateTimeProperty = datum + }.ToStringDictionary(); + + result.Count.Should().Be(8); + result[nameof(TestMappingClass.AStringProperty)].Should().Be("adefaultvalue"); + result[nameof(TestMappingClass.AnIntProperty)].Should().Be("1"); + result[nameof(TestMappingClass.AnBoolProperty)].Should().Be("True"); + result[nameof(TestMappingClass.ADateTimeProperty)].Should().Be(datum.ToIso8601()); + result[nameof(TestMappingClass.AnOptionalStringProperty)].Should().Be("avalue"); + result[nameof(TestMappingClass.AnOptionalNullableStringProperty)].Should().Be("avalue"); + result[nameof(TestMappingClass.AnOptionalDateTimeProperty)].Should().Be(datum.ToIso8601()); + result[nameof(TestMappingClass.AnOptionalNullableDateTimeProperty)].Should().Be(datum.ToIso8601()); + } + + [Fact] + public void WhenToStringDictionaryWithInstanceWithDefaultValues_ThenReturnsProperties() + { + var result = new TestMappingClass + { + AStringProperty = default!, + AnIntProperty = default, + AnBoolProperty = default, + ADateTimeProperty = default, + AnOptionalStringProperty = default, + AnOptionalNullableStringProperty = default, + AnOptionalDateTimeProperty = default, + AnOptionalNullableDateTimeProperty = default + }.ToStringDictionary(); + + result.Count.Should().Be(3); + result[nameof(TestMappingClass.AnIntProperty)].Should().Be("0"); + result[nameof(TestMappingClass.AnBoolProperty)].Should().Be("False"); + result[nameof(TestMappingClass.ADateTimeProperty)].Should().Be("0001-01-01T00:00:00"); + } } [UsedImplicitly] diff --git a/src/Common/Common.csproj b/src/Common/Common.csproj index f3470b9f..478a1ac1 100644 --- a/src/Common/Common.csproj +++ b/src/Common/Common.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Common/CurrencyCodes.cs b/src/Common/CurrencyCodes.cs new file mode 100644 index 00000000..1051719b --- /dev/null +++ b/src/Common/CurrencyCodes.cs @@ -0,0 +1,230 @@ +using Common.Extensions; +using ISO._4217; + +namespace Common; + +/// +/// See: https://en.wikipedia.org/wiki/ISO_4217#Numeric_codes +/// +public static class CurrencyCodes +{ + public static readonly CurrencyCodeIso4217 ChileanFomento = + CurrencyCodeIso4217.Create("Unidad de Fomento", "CLF", "990", CurrencyDecimalKind.FourDecimal); + public static readonly CurrencyCodeIso4217 NewZealandDollar = + CurrencyCodeIso4217.Create("New Zealand dollar", "NZD", "554", CurrencyDecimalKind.TwoDecimal); + public static readonly CurrencyCodeIso4217 Default = NewZealandDollar; + public static readonly CurrencyCodeIso4217 KuwaitiDinar = + CurrencyCodeIso4217.Create("Kuwaiti dinar", "KWD", "414", CurrencyDecimalKind.ThreeDecimal); + +#if TESTINGONLY + public static readonly CurrencyCodeIso4217 Test = + CurrencyCodeIso4217.Create("Test", "XXX", "001", CurrencyDecimalKind.TwoDecimal); +#endif + public static readonly CurrencyCodeIso4217 UnitedStatesDollar = + CurrencyCodeIso4217.Create("United States dollar", "USD", "840", CurrencyDecimalKind.TwoDecimal); + + /// + /// Whether the specified currency by its exists + /// + public static bool Exists(string currencyCodeOrNumber) + { + return Find(currencyCodeOrNumber).Exists(); + } + + /// + /// Returns the specified currency by its if it exists + /// + public static CurrencyCodeIso4217? Find(string? currencyCodeOrNumber) + { + if (currencyCodeOrNumber.NotExists()) + { + return null; + } + +#if TESTINGONLY + if (currencyCodeOrNumber == Test.Code + || currencyCodeOrNumber == Test.Numeric) + { + return Test; + } +#endif + + var code = CurrencyCodesResolver.GetCurrenciesByCode(currencyCodeOrNumber) + .FirstOrDefault(cur => cur.Code.HasValue()); + if (code.Exists()) + { + return CurrencyCodeIso4217.Create(code.Name, code.Code, code.Num, code.Exponent.ToKind()); + } + + var numeric = CurrencyCodesResolver.GetCurrenciesByNumber(currencyCodeOrNumber) + .FirstOrDefault(cur => cur.Code.HasValue()); + if (numeric.Exists()) + { + return CurrencyCodeIso4217.Create(numeric.Name, numeric.Code, numeric.Num, numeric.Exponent.ToKind()); + } + + return null; + } + + /// + /// Returns the specified currency by its if it exists, + /// or returns + /// + public static CurrencyCodeIso4217 FindOrDefault(string? currencyCodeOrNumber) + { + var exists = Find(currencyCodeOrNumber); + return exists.Exists() + ? exists + : Default; + } + + /// + /// Converts the amount in cents to a currency + /// + public static decimal ToCurrency(string code, int amountInCents) + { + var currency = Find(code); + if (currency.NotExists()) + { + return amountInCents; + } + + return currency.Kind switch + { + CurrencyDecimalKind.ZeroDecimal => amountInCents, + CurrencyDecimalKind.TwoDecimal => (decimal)amountInCents / 100, + CurrencyDecimalKind.ThreeDecimal => (decimal)amountInCents / 1000, + CurrencyDecimalKind.FourDecimal => (decimal)amountInCents / 10000, + _ => amountInCents + }; + } +} + +/// +/// See: https://en.wikipedia.org/wiki/ISO_4217 for details +/// +public sealed class CurrencyCodeIso4217 : IEquatable +{ + private CurrencyCodeIso4217(string numeric, string shortName, string code) + { + numeric.ThrowIfInvalidParameter(num => + { + if (!int.TryParse(num, out var integer)) + { + return false; + } + + return integer is >= 1 and < 1000; + }, nameof(numeric), Resources.CurrencyIso4217_InvalidNumeric.Format(numeric)); + Numeric = numeric; + ShortName = shortName; + Code = code; + } + + public string Code { get; } + + public CurrencyDecimalKind Kind { get; private init; } + + public string Numeric { get; } + + public string ShortName { get; } + + public bool Equals(CurrencyCodeIso4217? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Numeric == other.Numeric; + } + + internal static CurrencyCodeIso4217 Create(string shortName, string code, string numeric, CurrencyDecimalKind kind) + { + shortName.ThrowIfNotValuedParameter(nameof(shortName)); + code.ThrowIfNotValuedParameter(nameof(code)); + + var instance = new CurrencyCodeIso4217(numeric, shortName, code) + { + Kind = kind + }; + + return instance; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((CurrencyCodeIso4217)obj); + } + + public override int GetHashCode() + { + ArgumentException.ThrowIfNullOrEmpty(Numeric); + + return Numeric.GetHashCode(); + } + + public static bool operator ==(CurrencyCodeIso4217 left, CurrencyCodeIso4217 right) + { + return Equals(left, right); + } + + public static bool operator !=(CurrencyCodeIso4217 left, CurrencyCodeIso4217 right) + { + return !Equals(left, right); + } +} + +/// +/// The decimal kind of currency +/// +public enum CurrencyDecimalKind +{ + ZeroDecimal = 0, + TwoDecimal = 2, + ThreeDecimal = 3, + FourDecimal = 4, + Unknown = -1 +} + +internal static class ConversionExtensions +{ + public static CurrencyDecimalKind ToKind(this string exponent) + { + if (exponent.HasNoValue()) + { + return CurrencyDecimalKind.Unknown; + } + + var numeral = exponent.ToIntOrDefault(-1); + return numeral switch + { + -1 => CurrencyDecimalKind.Unknown, + 0 => CurrencyDecimalKind.ZeroDecimal, + 2 => CurrencyDecimalKind.TwoDecimal, + 3 => CurrencyDecimalKind.ThreeDecimal, + 4 => CurrencyDecimalKind.FourDecimal, + _ => CurrencyDecimalKind.Unknown + }; + } +} \ No newline at end of file diff --git a/src/Common/Error.cs b/src/Common/Error.cs index 7613afbd..1f76f37f 100644 --- a/src/Common/Error.cs +++ b/src/Common/Error.cs @@ -138,11 +138,11 @@ public static Error ForbiddenAccess(string? message = null) } /// - /// Creates a error + /// Creates a error /// - public static Error NotSubscribed(string? message = null) + public static Error FeatureViolation(string? message = null) { - return new Error(ErrorCode.NotSubscribed, message); + return new Error(ErrorCode.FeatureViolation, message); } /// @@ -176,14 +176,14 @@ public enum ErrorCode // EXTEND: add other kinds of errors you want to support in Result NoError = -1, Validation, - RuleViolation, + RuleViolation, RoleViolation, - PreconditionViolation, + PreconditionViolation, // the resource is not in a valid state to begin with EntityNotFound, EntityExists, + EntityDeleted, NotAuthenticated, ForbiddenAccess, - NotSubscribed, + FeatureViolation, Unexpected, - EntityDeleted } \ No newline at end of file diff --git a/src/Common/Extensions/DateTimeExtensions.cs b/src/Common/Extensions/DateTimeExtensions.cs index 23d84a90..de822a11 100644 --- a/src/Common/Extensions/DateTimeExtensions.cs +++ b/src/Common/Extensions/DateTimeExtensions.cs @@ -37,6 +37,24 @@ public static DateTime FromIso8601(this string? value) return default; } + /// + /// Converts the to a UTC date only, + /// but only if the is in the + /// ISO8601 format. + /// + public static DateOnly FromIso8601DateOnly(this string? value) + { + if (value.HasNoValue()) + { + return DateOnly.MinValue; + } + + var dateOnly = DateOnly.Parse(value); + return dateOnly.HasValue() + ? dateOnly + : DateOnly.MinValue; + } + /// /// Converts the to a UTC date, /// but only if the is in the UNIX Timestamp format. @@ -213,6 +231,24 @@ public static string ToIso8601(this DateTime? value) return value.Value.ToIso8601(); } + /// + /// Converts the to UTC and then to + /// ISO8601 + /// + public static string? ToIso8601(this DateOnly? date) + { + return date?.ToIso8601(); + } + + /// + /// Converts the to UTC and then to + /// ISO8601 + /// + public static string ToIso8601(this DateOnly date) + { + return date.ToString("O"); + } + /// /// Converts the to UTC and then to /// ISO8601 diff --git a/src/Common/Extensions/DictionaryExtensions.cs b/src/Common/Extensions/DictionaryExtensions.cs index a02da238..8f8787f6 100644 --- a/src/Common/Extensions/DictionaryExtensions.cs +++ b/src/Common/Extensions/DictionaryExtensions.cs @@ -36,4 +36,23 @@ public static void Merge(this IDictionary source, ID return mapper.Map>(instance); } + + /// + /// Converts the instance of the to a , + /// where null properties are removed + /// + public static IReadOnlyDictionary ToStringDictionary(this TObject instance) + { + var objectJson = instance.ToJson(false) ?? "{}"; + + var properties = objectJson.FromJson>(); + if (properties.NotExists()) + { + return new Dictionary(); + } + + return properties + .Where(entry => entry.Value.Exists() && entry.Value.ToString().Exists()) + .ToDictionary(entry => entry.Key, entry => entry.Value!.ToString()!); + } } \ No newline at end of file diff --git a/src/Common/Extensions/StringExtensions.cs b/src/Common/Extensions/StringExtensions.cs index 30e3c208..9c77e781 100644 --- a/src/Common/Extensions/StringExtensions.cs +++ b/src/Common/Extensions/StringExtensions.cs @@ -82,7 +82,11 @@ public static string Format(this string value, params object[] arguments) return JsonSerializer.Deserialize(json, new JsonSerializerOptions { - PropertyNameCaseInsensitive = true + PropertyNameCaseInsensitive = true, + Converters = + { + new OptionalConverterFactory() + } }); } #elif GENERATORS_WEB_API_PROJECT || ANALYZERS_NONPLATFORM @@ -283,7 +287,7 @@ public static double ToDouble(this string? value) } /// - /// Converts the to a integer value + /// Converts the to an integer value /// public static int ToInt(this string? value) { @@ -296,7 +300,7 @@ public static int ToInt(this string? value) } /// - /// Converts the to a integer value, + /// Converts the to an integer value, /// and in the case where the value cannot be converted, uses the /// public static int ToIntOrDefault(this string? value, int defaultValue) @@ -314,6 +318,38 @@ public static int ToIntOrDefault(this string? value, int defaultValue) return defaultValue; } + /// + /// Converts the to a decimal value + /// + public static decimal ToDecimal(this string? value) + { + if (value.HasNoValue()) + { + return -1; + } + + return decimal.Parse(value); + } + + /// + /// Converts the to a decimal value, + /// and in the case where the value cannot be converted, uses the + /// + public static decimal ToDecimalOrDefault(this string? value, decimal defaultValue) + { + if (value.HasNoValue()) + { + return defaultValue; + } + + if (decimal.TryParse(value, out var converted)) + { + return converted; + } + + return defaultValue; + } + /// /// Converts the to a long value /// @@ -370,7 +406,11 @@ public static long ToLongOrDefault(this string? value, long defaultValue) PropertyNamingPolicy = namingPolicy, DefaultIgnoreCondition = includeNulls ? JsonIgnoreCondition.Never - : JsonIgnoreCondition.WhenWritingNull + : JsonIgnoreCondition.WhenWritingNull, + Converters = + { + new OptionalConverterFactory() + } }); } #elif GENERATORS_WEB_API_PROJECT || ANALYZERS_NONPLATFORM || ANALYZERS_NONPLATFORM diff --git a/src/Common/OptionalConverter.cs b/src/Common/OptionalConverter.cs new file mode 100644 index 00000000..0a7a231c --- /dev/null +++ b/src/Common/OptionalConverter.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Common; + +/// +/// Handles JSON serialization/deserialization for data +/// +public sealed class OptionalConverter : JsonConverter> +{ + public override Optional Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + JsonTypeInfo typeInfo; + return new Optional( + reader.TokenType is JsonTokenType.Null + ? default + : (typeInfo = options.GetTypeInfo(typeof(T))) is JsonTypeInfo + ? JsonSerializer.Deserialize(ref reader, (JsonTypeInfo)typeInfo) + : (T?)JsonSerializer.Deserialize(ref reader, typeInfo)); + } + + public override void Write(Utf8JsonWriter writer, Optional value, JsonSerializerOptions options) + { + switch (value) + { + case { HasValue: false }: + writer.WriteNullValue(); + break; + + default: + var typeInfo = options.GetTypeInfo(typeof(T)); + if (typeInfo is JsonTypeInfo typed) + { + JsonSerializer.Serialize(writer, value.ValueOrDefault, typed); + } + else + { + JsonSerializer.Serialize(writer, value.ValueOrDefault, typeInfo); + } + + break; + } + } +} \ No newline at end of file diff --git a/src/Common/OptionalConverterFactory.cs b/src/Common/OptionalConverterFactory.cs new file mode 100644 index 00000000..c09bc168 --- /dev/null +++ b/src/Common/OptionalConverterFactory.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Common; + +/// +/// Provides a factory to create instances. +/// +public sealed class OptionalConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return Optional.IsOptionalType(typeToConvert); + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return Optional.TryGetContainedType(typeToConvert, out var containedType) + ? Activator.CreateInstance(typeof(OptionalConverter<>).MakeGenericType(containedType!)) as JsonConverter + : null; + } +} \ No newline at end of file diff --git a/src/Common/Permission.cs b/src/Common/Permission.cs new file mode 100644 index 00000000..87172127 --- /dev/null +++ b/src/Common/Permission.cs @@ -0,0 +1,69 @@ +using Common.Extensions; + +namespace Common; + +/// +/// Defines a permission given for an action +/// +public class Permission +{ + public static readonly Permission Allowed = new(true, null, PermissionResult.Allowed); + + private Permission(bool isAllowed, string? disallowedReason, PermissionResult result) + { + IsAllowed = isAllowed; + DisallowedReason = disallowedReason ?? Resources.Permission_Disallowed; + Result = result; + } + + public string DisallowedReason { get; } + + public bool IsAllowed { get; } + + public bool IsDenied => !IsAllowed; + + public PermissionResult Result { get; } + + public static Permission Denied_Evaluating(Error error) + { + return new Permission(false, error.Message, PermissionResult.Rule); + } + + public static Permission Denied_Role(string reason) + { + return new Permission(false, reason, PermissionResult.Role); + } + + public static Permission Denied_Rule(string reason) + { + return new Permission(false, reason, PermissionResult.Rule); + } + + /// + /// Converts the permission to an error, + /// using the specified formatted message (if provided), + /// + public Error ToError(string? formattedMessage = null) + { + var message = formattedMessage.HasValue() + ? formattedMessage.Format(DisallowedReason) + : DisallowedReason; + + return Result switch + { + PermissionResult.Rule => Error.RuleViolation(message), + PermissionResult.Role => Error.RoleViolation(message), + _ => Error.RuleViolation(message) + }; + } +} + +/// +/// Defines the result of the permission +/// +public enum PermissionResult +{ + Allowed = 0, + Rule = 1, + Role = 2 +} \ No newline at end of file diff --git a/src/Common/Resources.Designer.cs b/src/Common/Resources.Designer.cs index 0a44748e..2e392fa9 100644 --- a/src/Common/Resources.Designer.cs +++ b/src/Common/Resources.Designer.cs @@ -68,6 +68,15 @@ internal static string CountryCodeIso3166_InvalidNumeric { } } + /// + /// Looks up a localized string similar to The Numeric '{0}' must be a 3 decimal number between 1 and 1000. + /// + internal static string CurrencyIso4217_InvalidNumeric { + get { + return ResourceManager.GetString("CurrencyIso4217_InvalidNumeric", resourceCulture); + } + } + /// /// Looks up a localized string similar to The value of the optional variable is null. /// @@ -77,6 +86,15 @@ internal static string Optional_NullValue { } } + /// + /// Looks up a localized string similar to This action is not allowed. + /// + internal static string Permission_Disallowed { + get { + return ResourceManager.GetString("Permission_Disallowed", resourceCulture); + } + } + /// /// Looks up a localized string similar to The expression is not a property expression or is not convertable. /// diff --git a/src/Common/Resources.resx b/src/Common/Resources.resx index 7b605b80..c7eeebae 100644 --- a/src/Common/Resources.resx +++ b/src/Common/Resources.resx @@ -54,5 +54,10 @@ The DaylightSavingsCode '{0}' must be a three or four letter code, or the offset in hrs - + + The Numeric '{0}' must be a 3 decimal number between 1 and 1000 + + + This action is not allowed + \ No newline at end of file diff --git a/src/Domain.Common/ValueObjects/SingleValueObjectBase.cs b/src/Domain.Common/ValueObjects/SingleValueObjectBase.cs index e0fd26e0..7a6bbba4 100644 --- a/src/Domain.Common/ValueObjects/SingleValueObjectBase.cs +++ b/src/Domain.Common/ValueObjects/SingleValueObjectBase.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Common.Extensions; using Domain.Interfaces.ValueObjects; @@ -13,6 +14,7 @@ public abstract partial class SingleValueObjectBase : Valu ISingleValueObject where TValue : notnull { + [DebuggerStepThrough] protected SingleValueObjectBase(TValue value) { Value = value; @@ -27,6 +29,7 @@ protected SingleValueObjectBase(TValue value) TValue ISingleValueObject.Value => Value; + [DebuggerStepThrough] public static implicit operator TValue(SingleValueObjectBase valueObject) { return valueObject.NotExists() || valueObject.Value.NotExists() diff --git a/src/Domain.Interfaces/Authorization/PlatformFeatures.cs b/src/Domain.Interfaces/Authorization/PlatformFeatures.cs index fd624e18..027482fd 100644 --- a/src/Domain.Interfaces/Authorization/PlatformFeatures.cs +++ b/src/Domain.Interfaces/Authorization/PlatformFeatures.cs @@ -8,12 +8,15 @@ namespace Domain.Interfaces.Authorization; /// public static class PlatformFeatures { - public static readonly FeatureLevel Basic = new("basic_features"); // Free/Basic features, everyone can use - public static readonly FeatureLevel PaidTrial = new("paidtrial_features", Basic); // a.k.a Standard plan features - public static readonly FeatureLevel Paid2 = new("paid2_features", PaidTrial); // a.k.a Professional plan features - public static readonly FeatureLevel Paid3 = new("paid3_features", Paid2); // a.k.a Enterprise plan features - public static readonly FeatureLevel TestingOnly = new("testingonly_platform"); - public static readonly FeatureLevel TestingOnlySuperUser = new("testingonly_platform_super", TestingOnly); + public static readonly FeatureLevel + Basic = new("platform_basic_features"); // Free/Basic limited features, everyone can use + public static readonly FeatureLevel + PaidTrial = new("platform_paidtrial_features", Basic); // a.k.a Standard plan features + public static readonly FeatureLevel + Paid2 = new("platform_paid2_features", PaidTrial); // a.k.a Professional plan features + public static readonly FeatureLevel Paid3 = new("platform_paid3_features", Paid2); // a.k.a Enterprise plan features + public static readonly FeatureLevel TestingOnly = new("platform_testingonly_platform"); + public static readonly FeatureLevel TestingOnlySuperUser = new("platform_testingonly_platform_super", TestingOnly); public static readonly Dictionary AllFeatures = new() { { Basic.Name, Basic }, diff --git a/src/Domain.Interfaces/Authorization/TenantFeatures.cs b/src/Domain.Interfaces/Authorization/TenantFeatures.cs index 6686b4ad..49394f13 100644 --- a/src/Domain.Interfaces/Authorization/TenantFeatures.cs +++ b/src/Domain.Interfaces/Authorization/TenantFeatures.cs @@ -8,11 +8,14 @@ namespace Domain.Interfaces.Authorization; /// public static class TenantFeatures { - public static readonly FeatureLevel Basic = PlatformFeatures.Basic; // Free/Basic features, everyone can use - public static readonly FeatureLevel Paid2 = PlatformFeatures.Paid2; // a.k.a Professional plan features - public static readonly FeatureLevel Paid3 = PlatformFeatures.Paid3; // a.k.a Enterprise plan features - public static readonly FeatureLevel PaidTrial = PlatformFeatures.PaidTrial; // a.k.a Standard plan features - public static readonly FeatureLevel TestingOnly = PlatformFeatures.TestingOnly; + public static readonly FeatureLevel + Basic = new("tenant_basic_features"); // Free/Basic limited features, everyone can use + public static readonly FeatureLevel + PaidTrial = new("tenant_paidtrial_features", Basic); // a.k.a Standard plan features + public static readonly FeatureLevel + Paid2 = new("tenant_paid2_features", PaidTrial); // a.k.a Professional plan features + public static readonly FeatureLevel Paid3 = new("tenant_paid3_features", Paid2); // a.k.a Enterprise plan features + public static readonly FeatureLevel TestingOnly = new("tenant_testingonly_platform"); public static readonly Dictionary AllFeatures = new() { { Basic.Name, Basic }, diff --git a/src/Domain.Interfaces/Authorization/TenantRoles.cs b/src/Domain.Interfaces/Authorization/TenantRoles.cs index f6cc85fd..aaad694a 100644 --- a/src/Domain.Interfaces/Authorization/TenantRoles.cs +++ b/src/Domain.Interfaces/Authorization/TenantRoles.cs @@ -8,10 +8,10 @@ namespace Domain.Interfaces.Authorization; /// public static class TenantRoles { - public static readonly RoleLevel Member = new("org_member"); - public static readonly RoleLevel Owner = new("org_owner", Member); - public static readonly RoleLevel BillingAdmin = new("org_billing_admin", Owner); - public static readonly RoleLevel TestingOnly = new("org_testingonly"); + public static readonly RoleLevel Member = new("tenant_member"); + public static readonly RoleLevel Owner = new("tenant_owner", Member); + public static readonly RoleLevel BillingAdmin = new("tenant_billing_admin", Owner); + public static readonly RoleLevel TestingOnly = new("tenant_testingonly"); public static readonly Dictionary AllRoles = new() { { Member.Name, Member }, diff --git a/src/Domain.Interfaces/Validations/CommonValidations.cs b/src/Domain.Interfaces/Validations/CommonValidations.cs index 2786b35f..ee75ff13 100644 --- a/src/Domain.Interfaces/Validations/CommonValidations.cs +++ b/src/Domain.Interfaces/Validations/CommonValidations.cs @@ -12,7 +12,7 @@ public static class CommonValidations public static readonly Validation CountryCode = new(CountryCodes.Exists); public static readonly Validation EmailAddress = new( @"^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!\.)){0,61}[a-zA-Z0-9]?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$"); - public static readonly Validation FeatureLevel = new(@"^[\w\d]{4,30}$", 4, 30); + public static readonly Validation FeatureLevel = new(@"^[\w\d]{4,60}$", 4, 60); public static readonly Validation Identifier = new(@"^[\w]{1,20}_[\d\w]{10,22}$", 12, 43); public static readonly Validation IdentifierPrefix = new(@"^[^\W_]*$", 1, 20); diff --git a/src/Domain.Services.Shared/DomainServices/IEncryptionService.cs b/src/Domain.Services.Shared/IEncryptionService.cs similarity index 71% rename from src/Domain.Services.Shared/DomainServices/IEncryptionService.cs rename to src/Domain.Services.Shared/IEncryptionService.cs index 237e080e..ba7f11eb 100644 --- a/src/Domain.Services.Shared/DomainServices/IEncryptionService.cs +++ b/src/Domain.Services.Shared/IEncryptionService.cs @@ -1,4 +1,4 @@ -namespace Domain.Services.Shared.DomainServices; +namespace Domain.Services.Shared; public interface IEncryptionService { diff --git a/src/Domain.Services.Shared/DomainServices/ITokensService.cs b/src/Domain.Services.Shared/ITokensService.cs similarity index 87% rename from src/Domain.Services.Shared/DomainServices/ITokensService.cs rename to src/Domain.Services.Shared/ITokensService.cs index 10907a7c..3a54303e 100644 --- a/src/Domain.Services.Shared/DomainServices/ITokensService.cs +++ b/src/Domain.Services.Shared/ITokensService.cs @@ -1,7 +1,7 @@ using Common; using Domain.Shared.Identities; -namespace Domain.Services.Shared.DomainServices; +namespace Domain.Services.Shared; public interface ITokensService { diff --git a/src/EndUsersApplication.UnitTests/EndUsersApplicationSpec.cs b/src/EndUsersApplication.UnitTests/EndUsersApplicationSpec.cs index 03e0be2b..1db9a4ec 100644 --- a/src/EndUsersApplication.UnitTests/EndUsersApplicationSpec.cs +++ b/src/EndUsersApplication.UnitTests/EndUsersApplicationSpec.cs @@ -8,7 +8,7 @@ using Domain.Interfaces; using Domain.Interfaces.Authorization; using Domain.Interfaces.Entities; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using Domain.Shared.EndUsers; using EndUsersApplication.Persistence; diff --git a/src/EndUsersApplication.UnitTests/InvitationsApplication.DomainEventHandlersSpec.cs b/src/EndUsersApplication.UnitTests/InvitationsApplication.DomainEventHandlersSpec.cs index d7b02929..24ca47d7 100644 --- a/src/EndUsersApplication.UnitTests/InvitationsApplication.DomainEventHandlersSpec.cs +++ b/src/EndUsersApplication.UnitTests/InvitationsApplication.DomainEventHandlersSpec.cs @@ -7,7 +7,7 @@ using Domain.Common.ValueObjects; using Domain.Interfaces.Authorization; using Domain.Interfaces.Entities; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using Domain.Shared.EndUsers; using EndUsersApplication.Persistence; diff --git a/src/EndUsersApplication.UnitTests/InvitationsApplicationSpec.cs b/src/EndUsersApplication.UnitTests/InvitationsApplicationSpec.cs index 65771f99..62b26fa7 100644 --- a/src/EndUsersApplication.UnitTests/InvitationsApplicationSpec.cs +++ b/src/EndUsersApplication.UnitTests/InvitationsApplicationSpec.cs @@ -6,7 +6,7 @@ using Domain.Common.Identity; using Domain.Common.ValueObjects; using Domain.Interfaces.Entities; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using Domain.Shared.EndUsers; using EndUsersApplication.Persistence; diff --git a/src/EndUsersApplication/InvitationsApplication.cs b/src/EndUsersApplication/InvitationsApplication.cs index 171b2434..d1408098 100644 --- a/src/EndUsersApplication/InvitationsApplication.cs +++ b/src/EndUsersApplication/InvitationsApplication.cs @@ -6,7 +6,7 @@ using Common.Extensions; using Domain.Common.Identity; using Domain.Common.ValueObjects; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using Domain.Shared.EndUsers; using EndUsersApplication.Persistence; diff --git a/src/EndUsersDomain.UnitTests/EndUserRootSpec.cs b/src/EndUsersDomain.UnitTests/EndUserRootSpec.cs index 71d96f55..bf3c8777 100644 --- a/src/EndUsersDomain.UnitTests/EndUserRootSpec.cs +++ b/src/EndUsersDomain.UnitTests/EndUserRootSpec.cs @@ -6,7 +6,7 @@ using Domain.Interfaces; using Domain.Interfaces.Authorization; using Domain.Interfaces.Entities; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using Domain.Shared.EndUsers; using Domain.Shared.Organizations; diff --git a/src/EndUsersDomain/EndUserRoot.cs b/src/EndUsersDomain/EndUserRoot.cs index 4ee203d8..02c825c4 100644 --- a/src/EndUsersDomain/EndUserRoot.cs +++ b/src/EndUsersDomain/EndUserRoot.cs @@ -8,7 +8,7 @@ using Domain.Interfaces.Authorization; using Domain.Interfaces.Entities; using Domain.Interfaces.ValueObjects; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using Domain.Shared.EndUsers; using Domain.Shared.Organizations; diff --git a/src/EndUsersInfrastructure/EndUsersModule.cs b/src/EndUsersInfrastructure/EndUsersModule.cs index 7fa3f6cc..03d3ceb9 100644 --- a/src/EndUsersInfrastructure/EndUsersModule.cs +++ b/src/EndUsersInfrastructure/EndUsersModule.cs @@ -5,7 +5,7 @@ using Common.Configuration; using Domain.Common.Identity; using Domain.Interfaces; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using EndUsersApplication; using EndUsersApplication.Persistence; using EndUsersDomain; diff --git a/src/IdentityApplication.UnitTests/APIKeysApplicationSpec.cs b/src/IdentityApplication.UnitTests/APIKeysApplicationSpec.cs index 02337594..567b129e 100644 --- a/src/IdentityApplication.UnitTests/APIKeysApplicationSpec.cs +++ b/src/IdentityApplication.UnitTests/APIKeysApplicationSpec.cs @@ -7,7 +7,7 @@ using Domain.Common.Identity; using Domain.Common.ValueObjects; using Domain.Interfaces.Entities; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared.Identities; using FluentAssertions; using IdentityApplication.Persistence; diff --git a/src/IdentityApplication.UnitTests/PasswordCredentialsApplicationSpec.cs b/src/IdentityApplication.UnitTests/PasswordCredentialsApplicationSpec.cs index ffee9087..f18cf16b 100644 --- a/src/IdentityApplication.UnitTests/PasswordCredentialsApplicationSpec.cs +++ b/src/IdentityApplication.UnitTests/PasswordCredentialsApplicationSpec.cs @@ -7,7 +7,7 @@ using Domain.Common.Identity; using Domain.Common.ValueObjects; using Domain.Interfaces.Entities; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using FluentAssertions; using IdentityApplication.ApplicationServices; diff --git a/src/IdentityApplication.UnitTests/SSOProvidersServiceSpec.cs b/src/IdentityApplication.UnitTests/SSOProvidersServiceSpec.cs index 62c33251..d48d3f48 100644 --- a/src/IdentityApplication.UnitTests/SSOProvidersServiceSpec.cs +++ b/src/IdentityApplication.UnitTests/SSOProvidersServiceSpec.cs @@ -5,7 +5,7 @@ using Domain.Common.Identity; using Domain.Common.ValueObjects; using Domain.Interfaces.Entities; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using FluentAssertions; using IdentityApplication.ApplicationServices; diff --git a/src/IdentityApplication/APIKeysApplication.cs b/src/IdentityApplication/APIKeysApplication.cs index 655a2461..ccfd2747 100644 --- a/src/IdentityApplication/APIKeysApplication.cs +++ b/src/IdentityApplication/APIKeysApplication.cs @@ -6,7 +6,7 @@ using Common.Extensions; using Domain.Common.Identity; using Domain.Common.ValueObjects; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using IdentityApplication.Persistence; using IdentityDomain; using IdentityDomain.DomainServices; diff --git a/src/IdentityApplication/ApplicationServices/SSOProvidersService.cs b/src/IdentityApplication/ApplicationServices/SSOProvidersService.cs index cbbba9de..11696624 100644 --- a/src/IdentityApplication/ApplicationServices/SSOProvidersService.cs +++ b/src/IdentityApplication/ApplicationServices/SSOProvidersService.cs @@ -2,7 +2,7 @@ using Common.Extensions; using Domain.Common.Identity; using Domain.Common.ValueObjects; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using IdentityApplication.Persistence; using IdentityDomain; diff --git a/src/IdentityApplication/PasswordCredentialsApplication.cs b/src/IdentityApplication/PasswordCredentialsApplication.cs index 37d9059d..78a44c34 100644 --- a/src/IdentityApplication/PasswordCredentialsApplication.cs +++ b/src/IdentityApplication/PasswordCredentialsApplication.cs @@ -6,7 +6,7 @@ using Common.Configuration; using Domain.Common.Identity; using Domain.Common.ValueObjects; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using IdentityApplication.ApplicationServices; using IdentityApplication.Persistence; @@ -380,7 +380,7 @@ public async Task> GetPersonRegist { return Error.EntityNotFound(); } - + var credential = retrieved.Value.Value; var token = credential.VerificationKeep.Token; diff --git a/src/IdentityDomain.UnitTests/PasswordCredentialRootSpec.cs b/src/IdentityDomain.UnitTests/PasswordCredentialRootSpec.cs index 0a22014e..2e142efd 100644 --- a/src/IdentityDomain.UnitTests/PasswordCredentialRootSpec.cs +++ b/src/IdentityDomain.UnitTests/PasswordCredentialRootSpec.cs @@ -4,7 +4,7 @@ using Domain.Common.ValueObjects; using Domain.Events.Shared.Identities.PasswordCredentials; using Domain.Interfaces.Entities; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using FluentAssertions; using IdentityDomain.DomainServices; diff --git a/src/IdentityDomain.UnitTests/SSOUserRootSpec.cs b/src/IdentityDomain.UnitTests/SSOUserRootSpec.cs index 195f98ae..cbb86b20 100644 --- a/src/IdentityDomain.UnitTests/SSOUserRootSpec.cs +++ b/src/IdentityDomain.UnitTests/SSOUserRootSpec.cs @@ -4,7 +4,7 @@ using Domain.Common.ValueObjects; using Domain.Events.Shared.Identities.SSOUsers; using Domain.Interfaces.Entities; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using FluentAssertions; using Moq; diff --git a/src/IdentityDomain/PasswordCredentialRoot.cs b/src/IdentityDomain/PasswordCredentialRoot.cs index 2d88fc72..991962bd 100644 --- a/src/IdentityDomain/PasswordCredentialRoot.cs +++ b/src/IdentityDomain/PasswordCredentialRoot.cs @@ -8,7 +8,7 @@ using Domain.Interfaces; using Domain.Interfaces.Entities; using Domain.Interfaces.ValueObjects; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using IdentityDomain.DomainServices; diff --git a/src/IdentityDomain/SSOUserRoot.cs b/src/IdentityDomain/SSOUserRoot.cs index 31f06673..1f36175f 100644 --- a/src/IdentityDomain/SSOUserRoot.cs +++ b/src/IdentityDomain/SSOUserRoot.cs @@ -7,7 +7,7 @@ using Domain.Interfaces; using Domain.Interfaces.Entities; using Domain.Interfaces.ValueObjects; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; namespace IdentityDomain; diff --git a/src/IdentityInfrastructure.UnitTests/ApplicationServices/JWTTokensServiceSpec.cs b/src/IdentityInfrastructure.UnitTests/ApplicationServices/JWTTokensServiceSpec.cs index 80c9f237..7555f5ae 100644 --- a/src/IdentityInfrastructure.UnitTests/ApplicationServices/JWTTokensServiceSpec.cs +++ b/src/IdentityInfrastructure.UnitTests/ApplicationServices/JWTTokensServiceSpec.cs @@ -2,7 +2,7 @@ using Application.Resources.Shared; using Common.Configuration; using Domain.Interfaces.Authorization; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using FluentAssertions; using IdentityInfrastructure.ApplicationServices; using Infrastructure.Common.Extensions; @@ -84,11 +84,11 @@ public async Task WhenIssueTokensAsync_ThenReturnsTokens() == $"Tenant_{TenantRoles.Member.Name}{ClaimExtensions.TenantIdDelimiter}anorganizationid"); token.Claims.Should() .Contain(claim => claim.Type == AuthenticationConstants.Claims.ForFeature - && claim.Value == "Platform_basic_features"); + && claim.Value == "Platform_platform_basic_features"); token.Claims.Should() .Contain(claim => claim.Type == AuthenticationConstants.Claims.ForFeature && claim.Value - == $"Tenant_basic_features{ClaimExtensions.TenantIdDelimiter}anorganizationid"); + == $"Tenant_tenant_basic_features{ClaimExtensions.TenantIdDelimiter}anorganizationid"); _tokensService.Verify(ts => ts.CreateJWTRefreshToken()); } } \ No newline at end of file diff --git a/src/IdentityInfrastructure.UnitTests/DomainServices/EmailAddressServiceSpec.cs b/src/IdentityInfrastructure.UnitTests/DomainServices/EmailAddressServiceSpec.cs index 5304761d..d679229c 100644 --- a/src/IdentityInfrastructure.UnitTests/DomainServices/EmailAddressServiceSpec.cs +++ b/src/IdentityInfrastructure.UnitTests/DomainServices/EmailAddressServiceSpec.cs @@ -1,7 +1,7 @@ using Common; using Common.Configuration; using Domain.Common.ValueObjects; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared; using FluentAssertions; using IdentityApplication.Persistence; diff --git a/src/IdentityInfrastructure/ApplicationServices/JWTTokensService.cs b/src/IdentityInfrastructure/ApplicationServices/JWTTokensService.cs index 4e070fcd..fb198b10 100644 --- a/src/IdentityInfrastructure/ApplicationServices/JWTTokensService.cs +++ b/src/IdentityInfrastructure/ApplicationServices/JWTTokensService.cs @@ -3,7 +3,7 @@ using Application.Resources.Shared; using Common; using Common.Configuration; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using IdentityApplication.ApplicationServices; using Infrastructure.Common.Extensions; using Infrastructure.Interfaces; diff --git a/src/IdentityInfrastructure/IdentityModule.cs b/src/IdentityInfrastructure/IdentityModule.cs index 17a7d813..56469067 100644 --- a/src/IdentityInfrastructure/IdentityModule.cs +++ b/src/IdentityInfrastructure/IdentityModule.cs @@ -5,7 +5,7 @@ using Common.Configuration; using Domain.Common.Identity; using Domain.Interfaces; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using IdentityApplication; using IdentityApplication.ApplicationServices; using IdentityApplication.Persistence; diff --git a/src/Infrastructure.Common/DomainServices/AesEncryptionService.cs b/src/Infrastructure.Common/DomainServices/AesEncryptionService.cs index f1395f9d..05fa4e39 100644 --- a/src/Infrastructure.Common/DomainServices/AesEncryptionService.cs +++ b/src/Infrastructure.Common/DomainServices/AesEncryptionService.cs @@ -1,5 +1,5 @@ using System.Security.Cryptography; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; namespace Infrastructure.Common.DomainServices; diff --git a/src/Infrastructure.Common/DomainServices/TenantSettingService.cs b/src/Infrastructure.Common/DomainServices/TenantSettingService.cs index 05a3945e..d508bda1 100644 --- a/src/Infrastructure.Common/DomainServices/TenantSettingService.cs +++ b/src/Infrastructure.Common/DomainServices/TenantSettingService.cs @@ -1,5 +1,5 @@ using Domain.Interfaces.Services; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; namespace Infrastructure.Common.DomainServices; diff --git a/src/Infrastructure.Shared/DomainServices/TokensService.cs b/src/Infrastructure.Shared/DomainServices/TokensService.cs index efc4e0cf..f831af64 100644 --- a/src/Infrastructure.Shared/DomainServices/TokensService.cs +++ b/src/Infrastructure.Shared/DomainServices/TokensService.cs @@ -1,7 +1,7 @@ using System.Security.Cryptography; using Common; using Domain.Interfaces.Validations; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Domain.Shared.Identities; namespace Infrastructure.Shared.DomainServices; diff --git a/src/Infrastructure.Web.Api.Common.UnitTests/AuthorizeAttributeSpec.cs b/src/Infrastructure.Web.Api.Common.UnitTests/AuthorizeAttributeSpec.cs index fb7259d2..6dd91baa 100644 --- a/src/Infrastructure.Web.Api.Common.UnitTests/AuthorizeAttributeSpec.cs +++ b/src/Infrastructure.Web.Api.Common.UnitTests/AuthorizeAttributeSpec.cs @@ -254,7 +254,7 @@ public void WhenCreatePolicyNameAndOnlyEmptySets_ThenReturnsDefaultPolicyName() result.Should() .Be( - $"POLICY:{{|Features|:{{|Platform|:[|basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Platform|:[|{PlatformFeatures.Basic.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); } [Fact] @@ -273,7 +273,7 @@ public void WhenCreatePolicyNameAndSetContainsUnknownRolesOrFeatures_ThenReturns result.Should() .Be( - $"POLICY:{{|Features|:{{|Platform|:[|basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Platform|:[|{PlatformFeatures.Basic.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); } [Fact] @@ -291,7 +291,7 @@ public void WhenCreatePolicyNameAndSetContainsOnlyRoleNoFeature_ThenReturnsUniqu result.Should() .Be( - $"POLICY:{{|Features|:{{|Platform|:[|basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Platform|:[|{PlatformFeatures.Basic.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); } [Fact] @@ -309,7 +309,7 @@ public void WhenCreatePolicyNameAndSetContainsOnlyFeatureNoRole_ThenReturnsUniqu result.Should() .Be( - $"POLICY:{{|Features|:{{|Platform|:[|paidtrial_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Platform|:[|{PlatformFeatures.PaidTrial.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); } [Fact] @@ -328,7 +328,7 @@ public void WhenCreatePolicyNameAndSetContainsRoleAndFeature_ThenReturnsUniquePo result.Should() .Be( - $"POLICY:{{|Features|:{{|Platform|:[|paidtrial_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Platform|:[|{PlatformFeatures.PaidTrial.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); } [Fact] @@ -352,9 +352,9 @@ public void WhenCreatePolicyNameAndTwoSetsContainingSameRolesAndFeatures_ThenRet result.Should() .Be( - $"POLICY:{{|Features|:{{|Platform|:[|paidtrial_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}" + $"POLICY:{{|Features|:{{|Platform|:[|{PlatformFeatures.PaidTrial.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}" + - $"POLICY:{{|Features|:{{|Platform|:[|paidtrial_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Platform|:[|{PlatformFeatures.PaidTrial.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); } [Fact] @@ -378,9 +378,9 @@ public void WhenCreatePolicyNameAndTwoSetsContainingDifferentRolesAndFeatures_Th result.Should() .Be( - $"POLICY:{{|Features|:{{|Tenant|:[|basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}" + $"POLICY:{{|Features|:{{|Tenant|:[|{TenantFeatures.Basic.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}" + - $"POLICY:{{|Features|:{{|Tenant|:[|paidtrial_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Tenant|:[|{TenantFeatures.PaidTrial.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); } [Fact] @@ -401,7 +401,7 @@ public void WhenCreatePolicyNameAndTwoSetsContainingMultipleRolesAndFeatures_The result.Should() .Be( - $"POLICY:{{|Features|:{{|Tenant|:[|paidtrial_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Tenant|:[|{TenantFeatures.PaidTrial.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); } } @@ -444,7 +444,7 @@ public void WhenParsePolicyNameAndHasRole_ThenReturnsRole() public void WhenParsePolicyNameAndHasRoleAndFeature_ThenReturnsRoleAndFeature() { var result = AuthorizeAttribute.ParsePolicyName( - $"POLICY:{{|Features|:{{|Platform|:[|basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Platform|:[|{PlatformFeatures.Basic.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); result.Count.Should().Be(1); result[0].Roles.All.Should().OnlyContain(rol => rol == PlatformRoles.Standard); @@ -459,23 +459,23 @@ public void WhenParsePolicyNameAndHasRoleAndFeature_ThenReturnsRoleAndFeature() public void WhenParsePolicyNameAndHasMultiplePolicies_ThenReturnsRolesAndFeatures() { var result = AuthorizeAttribute.ParsePolicyName( - $"POLICY:{{|Features|:{{|Tenant|:[|basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}" + $"POLICY:{{|Features|:{{|Tenant|:[|{TenantFeatures.Basic.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}" + - $"POLICY:{{|Features|:{{|Tenant|:[|paidtrial_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Tenant|:[|{TenantFeatures.PaidTrial.Name}|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}"); result.Count.Should().Be(2); result[0].Roles.All.Should().OnlyContain(rol => rol == PlatformRoles.Standard); result[0].Roles.Platform.Should().OnlyContain(rol => rol == PlatformRoles.Standard); result[0].Roles.Tenant.Should().BeEmpty(); - result[0].Features.All.Should().OnlyContain(feat => feat == PlatformFeatures.Basic); + result[0].Features.All.Should().OnlyContain(feat => feat == TenantFeatures.Basic); result[0].Features.Platform.Should().BeEmpty(); - result[0].Features.Tenant.Should().OnlyContain(feat => feat == PlatformFeatures.Basic); + result[0].Features.Tenant.Should().OnlyContain(feat => feat == TenantFeatures.Basic); result[1].Roles.All.Should().OnlyContain(rol => rol == PlatformRoles.Operations); result[1].Roles.Platform.Should().OnlyContain(rol => rol == PlatformRoles.Operations); result[1].Roles.Tenant.Should().BeEmpty(); - result[1].Features.All.Should().OnlyContain(feat => feat == PlatformFeatures.PaidTrial); + result[1].Features.All.Should().OnlyContain(feat => feat == TenantFeatures.PaidTrial); result[1].Features.Platform.Should().BeEmpty(); - result[1].Features.Tenant.Should().OnlyContain(feat => feat == PlatformFeatures.PaidTrial); + result[1].Features.Tenant.Should().OnlyContain(feat => feat == TenantFeatures.PaidTrial); } } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.IntegrationTests/AuthNApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/AuthNApiSpec.cs index 5ad88473..143c9082 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/AuthNApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/AuthNApiSpec.cs @@ -4,7 +4,7 @@ using Common.Configuration; using Domain.Interfaces; using Domain.Interfaces.Authorization; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using FluentAssertions; using IdentityInfrastructure.ApplicationServices; using Infrastructure.Web.Api.Common.Extensions; diff --git a/src/Infrastructure.Web.Api.IntegrationTests/AuthZApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/AuthZApiSpec.cs index 3260388a..e4040178 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/AuthZApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/AuthZApiSpec.cs @@ -4,7 +4,7 @@ using Common.Configuration; using Domain.Interfaces; using Domain.Interfaces.Authorization; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using FluentAssertions; using IdentityInfrastructure.ApplicationServices; using Infrastructure.Web.Api.Common.Extensions; diff --git a/src/Infrastructure.Web.Api.IntegrationTests/MultiTenancySpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/MultiTenancySpec.cs index cac194b2..38a92c95 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/MultiTenancySpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/MultiTenancySpec.cs @@ -167,7 +167,7 @@ private static void OverrideDependencies(IServiceCollection services) { #if TESTINGONLY services.AddSingleton(); - // Replace the store to use different settings + // Replace the tenanted IDataStore to use fake settings services.AddPerHttpRequest(c => LocalMachineJsonFileStore.Create( new FakeConfigurationSettings(c.GetRequiredService().Current))); diff --git a/src/Infrastructure.Web.Api.Interfaces/HttpConstants.cs b/src/Infrastructure.Web.Api.Interfaces/HttpConstants.cs index d8ce5289..8dea5e87 100644 --- a/src/Infrastructure.Web.Api.Interfaces/HttpConstants.cs +++ b/src/Infrastructure.Web.Api.Interfaces/HttpConstants.cs @@ -90,9 +90,10 @@ public static class StatusCodes /// public static readonly Dictionary> SupportedErrorCodesMap = new() { + //EXTEND: other HTTP status codes to error maps { HttpStatusCode.BadRequest, new List { ErrorCode.Validation, ErrorCode.RuleViolation } }, { HttpStatusCode.Unauthorized, new List { ErrorCode.NotAuthenticated } }, - { HttpStatusCode.PaymentRequired, new List { ErrorCode.NotSubscribed } }, + { HttpStatusCode.PaymentRequired, new List { ErrorCode.FeatureViolation } }, { HttpStatusCode.Forbidden, new List { ErrorCode.RoleViolation, ErrorCode.ForbiddenAccess } }, { HttpStatusCode.NotFound, new List { ErrorCode.EntityNotFound } }, { diff --git a/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/HMACAuthenticationHandlerSpec.cs b/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/HMACAuthenticationHandlerSpec.cs index ccf4ce87..0406afc0 100644 --- a/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/HMACAuthenticationHandlerSpec.cs +++ b/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/HMACAuthenticationHandlerSpec.cs @@ -105,7 +105,7 @@ await _handler.InitializeAsync(new AuthenticationScheme(HMACAuthenticationHandle && claim.Value == $"Platform_{PlatformRoles.ServiceAccount.Name}"); result.Ticket.Principal.Claims.Should().Contain(claim => claim.Type == AuthenticationConstants.Claims.ForFeature - && claim.Value == "Platform_basic_features"); + && claim.Value == $"Platform_{PlatformFeatures.Basic.Name}"); } [Fact] @@ -146,7 +146,7 @@ await _handler.InitializeAsync(new AuthenticationScheme(HMACAuthenticationHandle && claim.Value == $"Platform_{PlatformRoles.ServiceAccount.Name}"); result.Ticket.Principal.Claims.Should().Contain(claim => claim.Type == AuthenticationConstants.Claims.ForFeature - && claim.Value == "Platform_basic_features"); + && claim.Value == $"Platform_{PlatformFeatures.Basic.Name}"); _recorder.Verify(rec => rec.Audit(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } diff --git a/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/RolesAndFeaturesAuthorizationPolicyProviderSpec.cs b/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/RolesAndFeaturesAuthorizationPolicyProviderSpec.cs index 7af8ed37..6916c909 100644 --- a/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/RolesAndFeaturesAuthorizationPolicyProviderSpec.cs +++ b/src/Infrastructure.Web.Hosting.Common.UnitTests/Auth/RolesAndFeaturesAuthorizationPolicyProviderSpec.cs @@ -24,7 +24,7 @@ public RolesAndFeaturesAuthorizationPolicyProviderSpec() public async Task WhenGetPolicyAsyncAndNotCachedAndUnknown_ThenBuildsPolicy() { var policyName = - $"POLICY:{{|Features|:{{|Platform|:[|basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"; + $"POLICY:{{|Features|:{{|Platform|:[|platform_basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"; var result = await _provider.GetPolicyAsync(policyName); diff --git a/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFService.cs b/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFService.cs index a04df7ac..73ee75ca 100644 --- a/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFService.cs +++ b/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFService.cs @@ -1,6 +1,6 @@ using Application.Interfaces.Services; using Common; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; namespace Infrastructure.Web.Hosting.Common.Pipeline; diff --git a/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFTokenPair.cs b/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFTokenPair.cs index 2524decb..64483d61 100644 --- a/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFTokenPair.cs +++ b/src/Infrastructure.Web.Hosting.Common/Pipeline/CSRFTokenPair.cs @@ -1,7 +1,7 @@ using Common; using Common.Extensions; using Domain.Interfaces; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Infrastructure.Web.Api.Common; namespace Infrastructure.Web.Hosting.Common.Pipeline; diff --git a/src/SaaStack.sln.DotSettings b/src/SaaStack.sln.DotSettings index ec415dfd..b9e5cd54 100644 --- a/src/SaaStack.sln.DotSettings +++ b/src/SaaStack.sln.DotSettings @@ -341,7 +341,7 @@ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Methods"><ElementKinds><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="When" Suffix="" Style="AaBb_AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /><ExtraRule Prefix="Setup" Suffix="" Style="AaBb" /><ExtraRule Prefix="Configure" Suffix="" Style="AaBb" /></Policy></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Enum members"><ElementKinds><Kind Name="ENUM_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> - <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="Configure" Suffix="" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="Configure" Suffix="" Style="AaBb" /><ExtraRule Prefix="Setup" Suffix="" Style="AaBb" /></Policy></Policy> True True True @@ -610,7 +610,7 @@ public sealed class $name$ : SingleValueObjectBase<$name$, $datatype$> True public async Task<ApiDeleteResult> $Action$$Resource$($Action$$Resource$Request request, CancellationToken cancellationToken) {$END$$SELECTION$ - var $resource$ = await _application.$Action$$Resource$Async(_contextFactory.Create(), request.Id, cancellationToken); + var $resource$ = await _application.$Action$$Resource$Async(_callerFactory.Create(), request.Id, cancellationToken); return () => $resource$.HandleApplicationResult(); } @@ -766,7 +766,7 @@ public class $Action$$Resource$RequestValidator : AbstractValidator<$Action$$ True public async Task<ApiPostResult<$Resource$, $Action$$Resource$Response>> $Action$$Resource$($Action$$Resource$Request request, CancellationToken cancellationToken) {$END$$SELECTION$ - var $resource$ = await _application.$Action$$Resource$Async(_contextFactory.Create(), request.Id, cancellationToken); + var $resource$ = await _application.$Action$$Resource$Async(_callerFactory.Create(), request.Id, cancellationToken); return () => $resource$.HandleApplicationResult<$Resource$, $Action$$Resource$Response>(x => new PostResult<$Action$$Resource$Response>(new $Action$$Resource$Response { $Resource$ = x })); } @@ -829,7 +829,7 @@ public sealed class $class$ : DomainEvent True public async Task<ApiSearchResult<$Resource$, $Action$$Resource$sResponse>> $Action$$Resource$s($Action$$Resource$sRequest request, CancellationToken cancellationToken) {$END$$SELECTION$ - var $resource$s = await _application.$Action$$Resource$sAsync(_contextFactory.Create(), request.ToSearchOptions(), request.ToGetOptions(), cancellationToken); + var $resource$s = await _application.$Action$$Resource$sAsync(_callerFactory.Create(), request.ToSearchOptions(), request.ToGetOptions(), cancellationToken); return () => $resource$s.HandleApplicationResult(x => new $Action$$Resource$sResponse { $Resource$s = x.Results, Metadata = x.Metadata }); } @@ -902,11 +902,14 @@ public sealed class $class$ : IntegrationEvent namespace $ns$; +/// <summary> +/// +/// </summary> [Route("/$route$/{Id}", OperationMethod.Get, AccessType.Token)] [Authorize(Roles.Tenant_Member, Features.Tenant_PaidTrial)] public class $Action$$Resource$Request : TenantedRequest<$Action$$Resource$Response> { - public required string Id { get; set; } + public string? Id { get; set; } } //TODO: move to its own file @@ -943,11 +946,14 @@ public class $Action$$Resource$Response : IWebResponse namespace $ns$; +/// <summary> +/// +/// </summary> [Route("/$route$/{Id}", OperationMethod.Delete, AccessType.Token)] [Authorize(Roles.Tenant_Member, Features.Tenant_PaidTrial)] public class $Action$$Resource$Request : TenantedDeleteRequest { - public required string Id { get; set; } + public string? Id { get; set; } } SaaStack @@ -1128,11 +1134,14 @@ public class $Action$$Resource$Response : SearchResponse namespace $ns$; +/// <summary> +/// +/// </summary> [Route("/$route$", OperationMethod.Post, AccessType.Token)] [Authorize(Roles.Tenant_Member, Features.Tenant_PaidTrial)] public class $Action$$Resource$Request : TenantedRequest<$Action$$Resource$Response> { - public required string $Property$ { get; set; } + public string? $Property$ { get; set; } } //TODO: move to its own file @@ -1171,13 +1180,16 @@ public class $Action$$Resource$Response : IWebResponse namespace $ns$; +/// <summary> +/// +/// </summary> [Route("/$route$/{Id}", OperationMethod.PutPatch, AccessType.Token)] [Authorize(Roles.Tenant_Member, Features.Tenant_PaidTrial)] public class $Action$$Resource$Request : TenantedRequest<$Action$$Resource$Response> { - public required string Id { get; set; } + public string? Id { get; set; } - public required string $Property$ { get; set; } + public string? $Property$ { get; set; } } //TODO: move to its own file @@ -1224,7 +1236,7 @@ public class $Action$$Resource$Response : IWebResponse True public async Task<ApiGetResult<$Resource$, $Action$$Resource$Response>> $Action$$Resource$($Action$$Resource$Request request, CancellationToken cancellationToken) {$END$$SELECTION$ - var $resource$ = await _application.$Action$$Resource$Async(_contextFactory.Create(), request.Id, cancellationToken); + var $resource$ = await _application.$Action$$Resource$Async(_callerFactory.Create(), request.Id, cancellationToken); return () => $resource$.HandleApplicationResult<$Resource$, $Action$$Resource$Response>(x => new $Action$$Resource$Response { $Resource$ = x }); } @@ -1382,7 +1394,7 @@ public sealed class $name$Root : AggregateRootBase True public async Task<ApiPutPatchResult<$Resource$, $Action$$Resource$Response>> $Action$$Resource$($Action$$Resource$Request request, CancellationToken cancellationToken) {$END$$SELECTION$ - var $resource$ = await _application.$Action$$Resource$Async(_contextFactory.Create(), request.Id, cancellationToken); + var $resource$ = await _application.$Action$$Resource$Async(_callerFactory.Create(), request.Id, cancellationToken); return () => $resource$.HandleApplicationResult<$Resource$, $Action$$Resource$Response>(x => new $Action$$Resource$Response { $Resource$ = x }); } diff --git a/src/Tools.Generators.Web.Api.UnitTests/MinimalApiMediatRGeneratorSpec.cs b/src/Tools.Generators.Web.Api.UnitTests/MinimalApiMediatRGeneratorSpec.cs index 6609c686..2aa1b68c 100644 --- a/src/Tools.Generators.Web.Api.UnitTests/MinimalApiMediatRGeneratorSpec.cs +++ b/src/Tools.Generators.Web.Api.UnitTests/MinimalApiMediatRGeneratorSpec.cs @@ -774,7 +774,7 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") - .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}POLICY:{|Features|:{|Platform|:[|paidtrial_features|]},|Roles|:{|Platform|:[|platform_operations|]}}") + .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|platform_basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}POLICY:{|Features|:{|Platform|:[|platform_paidtrial_features|]},|Roles|:{|Platform|:[|platform_operations|]}}") .WithOpenApi(op => { op.OperationId = "A"; @@ -865,7 +865,7 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") - .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") + .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|platform_basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") .WithOpenApi(op => { op.OperationId = "A"; @@ -957,7 +957,7 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") - .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") + .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|platform_basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") .WithOpenApi(op => { op.OperationId = "A"; @@ -1044,7 +1044,7 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Mvc.FromForm] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") - .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") + .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|platform_basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") .DisableAntiforgery() .WithOpenApi(op => { @@ -1132,7 +1132,7 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Mvc.FromForm] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") - .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") + .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|platform_basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") .DisableAntiforgery() .WithOpenApi(op => { @@ -1145,7 +1145,7 @@ await mediator.Send(request, global::System.Threading.CancellationToken.None)) async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Mvc.FromForm] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") - .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") + .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|platform_basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") .DisableAntiforgery() .WithOpenApi(op => { @@ -1235,7 +1235,7 @@ public static void RegisterRoutes(this global::Microsoft.AspNetCore.Builder.WebA async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::ANamespace.ARequest request) => await mediator.Send(request, global::System.Threading.CancellationToken.None)) .RequireAuthorization("Token") - .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") + .RequireCallerAuthorization("POLICY:{|Features|:{|Platform|:[|platform_basic_features|]},|Roles|:{|Platform|:[|platform_standard|]}}") .WithOpenApi(op => { op.OperationId = "A"; diff --git a/src/Tools.Generators.Web.Api.UnitTests/WebApiAssemblyVisitorSpec.cs b/src/Tools.Generators.Web.Api.UnitTests/WebApiAssemblyVisitorSpec.cs index ac13b3d2..49fd8863 100644 --- a/src/Tools.Generators.Web.Api.UnitTests/WebApiAssemblyVisitorSpec.cs +++ b/src/Tools.Generators.Web.Api.UnitTests/WebApiAssemblyVisitorSpec.cs @@ -466,7 +466,7 @@ public string AMethod(ARequest request) registration.MethodName.Should().Be("AMethod"); registration.OperationMethod.Should().Be(OperationMethod.Get); registration.OperationAuthorization!.PolicyName.Should().Be( - $"POLICY:{{|Features|:{{|Platform|:[|basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Platform|:[|platform_basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); registration.RoutePath.Should().Be("aroute"); registration.IsTestingOnly.Should().BeFalse(); registration.RequestDto.Name.Should().Be("ARequest"); @@ -523,8 +523,8 @@ public string AMethod(ARequest request) registration.MethodName.Should().Be("AMethod"); registration.OperationMethod.Should().Be(OperationMethod.Get); registration.OperationAuthorization!.PolicyName.Should().Be( - $"POLICY:{{|Features|:{{|Platform|:[|paid2_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}" - + $"POLICY:{{|Features|:{{|Platform|:[|basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); + $"POLICY:{{|Features|:{{|Platform|:[|platform_paid2_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Operations.Name}|]}}}}" + + $"POLICY:{{|Features|:{{|Platform|:[|platform_basic_features|]}},|Roles|:{{|Platform|:[|{PlatformRoles.Standard.Name}|]}}}}"); registration.RoutePath.Should().Be("aroute"); registration.IsTestingOnly.Should().BeFalse(); registration.RequestDto.Name.Should().Be("ARequest"); diff --git a/src/Tools.Templates/InfrastructureProject/.template.config/template.json b/src/Tools.Templates/InfrastructureProject/.template.config/template.json index 4a8db2ab..8a382aa9 100644 --- a/src/Tools.Templates/InfrastructureProject/.template.config/template.json +++ b/src/Tools.Templates/InfrastructureProject/.template.config/template.json @@ -20,8 +20,9 @@ "type": "parameter", "datatype": "string", "isRequired": true, - "description": "Name of the subdomain (singular, Title-cased)", - "replaces": "{SubDomainName}" + "description": "Name of the subdomain (Singular, Title-cased)", + "replaces": "{SubDomainName}", + "fileRename": "{SubDomainName}" }, "SubDomainNameLower": { "type": "generated", diff --git a/src/Tools.Templates/InfrastructureProject/Api/{SubDomainName}s/{SubDomainName}sApi.cs b/src/Tools.Templates/InfrastructureProject/Api/{SubDomainName}s/{SubDomainName}sApi.cs index 3d73619f..7d834bd8 100644 --- a/src/Tools.Templates/InfrastructureProject/Api/{SubDomainName}s/{SubDomainName}sApi.cs +++ b/src/Tools.Templates/InfrastructureProject/Api/{SubDomainName}s/{SubDomainName}sApi.cs @@ -1,15 +1,19 @@ +using Infrastructure.Interfaces; +using Infrastructure.Web.Api.Interfaces; + namespace ProjectName.Api.{SubDomainName}s; -public class {SubDomainName}sApi : IwebApiService +public class {SubDomainName}sApi : IWebApiService { private readonly I{SubDomainName}sApplication _{SubDomainNameLower}sApplication; private readonly ICallerContextFactory _callerFactory; - public CarsApi(ICallerContextFactory callerFactory, I{SubDomainName}sApplication {SubDomainNameLower}sApplication) + public {SubDomainName}sApi(ICallerContextFactory callerFactory, I{SubDomainName}sApplication {SubDomainNameLower}sApplication) { _callerFactory = callerFactory; _{SubDomainNameLower}sApplication = {SubDomainNameLower}sApplication; } //TODO: Add your service operation methods here + //Tip: try: postapi and getapi } \ No newline at end of file diff --git a/src/Tools.Templates/InfrastructureProject/Notifications/{SubDomainName}Notifier.cs b/src/Tools.Templates/InfrastructureProject/Notifications/{SubDomainName}Notifier.cs new file mode 100644 index 00000000..5a06c68f --- /dev/null +++ b/src/Tools.Templates/InfrastructureProject/Notifications/{SubDomainName}Notifier.cs @@ -0,0 +1,8 @@ +using Infrastructure.Eventing.Common.Notifications; +using {SubDomainName}sDomain; + +namespace {SubDomainName}sInfrastructure.Persistence.Notifications; + +public class {SubDomainName}Notifier : NoOpEventNotificationRegistration<{SubDomainName}Root> +{ +} \ No newline at end of file diff --git a/src/Tools.Templates/InfrastructureProject/Persistence/ReadModels/{SubDomainName}Projection.cs b/src/Tools.Templates/InfrastructureProject/Persistence/ReadModels/{SubDomainName}Projection.cs new file mode 100644 index 00000000..df361e86 --- /dev/null +++ b/src/Tools.Templates/InfrastructureProject/Persistence/ReadModels/{SubDomainName}Projection.cs @@ -0,0 +1,40 @@ +using Application.Persistence.Common.Extensions; +using Application.Persistence.Interfaces; +using Common; +using Domain.Interfaces; +using Domain.Interfaces.Entities; +using {SubDomainName}Application.Persistence.ReadModels; +using {SubDomainName}Domain; +using Infrastructure.Persistence.Common; +using Infrastructure.Persistence.Interfaces; + +namespace ProjectName.Persistence.ReadModels; + +public class {SubDomainName}Projection : IReadModelProjection +{ + private readonly IReadModelStore<{SubDomainName}> _{SubDomainNameLower}s; + + public {SubDomainName}Projection(IRecorder recorder, IDomainFactory domainFactory, IDataStore store) + { + _{SubDomainNameLower}s = new ReadModelStore<{SubDomainName}>(recorder, domainFactory, store); + } + + public Type RootAggregateType => typeof({SubDomainName}Root); + + public async Task> ProjectEventAsync(IDomainEvent changeEvent, + CancellationToken cancellationToken) + { + switch (changeEvent) + { + case Created e: + return await _{SubDomainNameLower}s.HandleCreateAsync(e.RootId, dto => + { + dto.UserId = e.UserId; + }, + cancellationToken); + + default: + return false; + } + } +} \ No newline at end of file diff --git a/src/Tools.Templates/InfrastructureProject/ProjectNameModule.cs b/src/Tools.Templates/InfrastructureProject/{SubDomainName}sModule.cs similarity index 70% rename from src/Tools.Templates/InfrastructureProject/ProjectNameModule.cs rename to src/Tools.Templates/InfrastructureProject/{SubDomainName}sModule.cs index 5712dea8..9c6af166 100644 --- a/src/Tools.Templates/InfrastructureProject/ProjectNameModule.cs +++ b/src/Tools.Templates/InfrastructureProject/{SubDomainName}sModule.cs @@ -1,24 +1,25 @@ using System.Reflection; -using Application.Interfaces.Services; -using Infrastructure.Web.Hosting.Common; -using Common; using Domain.Interfaces; +using Infrastructure.Hosting.Common.Extensions; using Infrastructure.Persistence.Interfaces; using Infrastructure.Web.Hosting.Common; -using Infrastructure.Web.Hosting.Common.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using ProjectName.Api.Subscriptions; +using ProjectName.Persistence; +using ProjectName.Persistence.Notifications; +using ProjectName.Persistence.ReadModels; namespace ProjectName; -public class {SubDomainName}sModule : ISubDomainModule +public class {SubDomainName}sModule : ISubdomainModule { - public Assembly ApiAssembly => typeof({SubDomainName}sApi).Assembly; + public Assembly InfrastructureAssembly => typeof({SubDomainName}sApi).Assembly; public Assembly? DomainAssembly => typeof({SubDomainName}Root).Assembly; - public Dictionary AggregatePrefixes => new() + public Dictionary EntityPrefixes => new() { { typeof({SubDomainName}Root), "{SubDomainNameLower}" } }; @@ -38,7 +39,8 @@ public Action RegisterServices services.AddPerHttpRequest(); services.RegisterEventing<{SubDomainName}Root, {SubDomainName}Projection>( c => new {SubDomainName}Projection(c.GetRequiredService(), c.GetRequiredService(), - c.GetRequiredService()) + c.GetRequiredService()), + _ => new {SubDomainName}Notifier() ); }; } diff --git a/src/Tools.Templates/Tools.Templates.csproj b/src/Tools.Templates/Tools.Templates.csproj index cae5a1ea..d89754b9 100644 --- a/src/Tools.Templates/Tools.Templates.csproj +++ b/src/Tools.Templates/Tools.Templates.csproj @@ -7,6 +7,7 @@ + diff --git a/src/WebsiteHost/BackEndForFrontEndModule.cs b/src/WebsiteHost/BackEndForFrontEndModule.cs index 3d13a7e7..c0950925 100644 --- a/src/WebsiteHost/BackEndForFrontEndModule.cs +++ b/src/WebsiteHost/BackEndForFrontEndModule.cs @@ -1,7 +1,7 @@ using System.Reflection; using System.Text.Json; using Application.Interfaces.Services; -using Domain.Services.Shared.DomainServices; +using Domain.Services.Shared; using Infrastructure.Common.DomainServices; using Infrastructure.Web.Common.Clients; using Infrastructure.Web.Hosting.Common;