From 9c12a159bad30625e0d6063a3eba0104d525a6d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Sun, 22 Jan 2017 17:31:33 +0300 Subject: [PATCH] Resolved #62: Add domain tenant resolver. --- .../AbpAspNetMultiTenancyModule.cs | 1 - .../MultiTenancy/DomainTenantResolver.cs | 30 ++++ .../MultiTenancyOptionsExtensions.cs | 12 ++ .../Abp/MultiTenancy/MultiTenancyManager.cs | 2 - src/Volo.Abp/Properties/AssemblyInfo.cs | 3 + src/Volo.Abp/Volo/Abp/NameValue.cs | 60 ++++++++ .../Abp/Text/Formatting/FormatStringToken.cs | 15 ++ .../Text/Formatting/FormatStringTokenType.cs | 8 ++ .../Text/Formatting/FormatStringTokenizer.cs | 78 +++++++++++ .../FormattedStringValueExtracter.cs | 131 ++++++++++++++++++ .../Volo/ExtensionMethods/StringExtensions.cs | 28 +++- .../Volo/Abp/AspNetCore/App/Startup.cs | 6 + .../AspNetCoreMultiTenancy_Tests.cs | 16 +++ .../FormattedStringTokenizer_Test.cs | 56 ++++++++ .../FormattedStringValueExtracter_Tests.cs | 91 ++++++++++++ .../StringExtensions_Tests.cs | 6 + 16 files changed, 538 insertions(+), 5 deletions(-) create mode 100644 src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DomainTenantResolver.cs create mode 100644 src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyOptionsExtensions.cs create mode 100644 src/Volo.Abp/Volo/Abp/NameValue.cs create mode 100644 src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringToken.cs create mode 100644 src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringTokenType.cs create mode 100644 src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringTokenizer.cs create mode 100644 src/Volo.Abp/Volo/Abp/Text/Formatting/FormattedStringValueExtracter.cs create mode 100644 test/Volo.Abp.Tests/Volo/Abp/Text/Formatting/FormattedStringTokenizer_Test.cs create mode 100644 test/Volo.Abp.Tests/Volo/Abp/Text/Formatting/FormattedStringValueExtracter_Tests.cs diff --git a/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetMultiTenancyModule.cs b/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetMultiTenancyModule.cs index 6a1b00a8d8..4e62495b65 100644 --- a/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetMultiTenancyModule.cs +++ b/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetMultiTenancyModule.cs @@ -11,7 +11,6 @@ namespace Volo.Abp.AspNetCore.MultiTenancy { services.Configure(options => { - //TODO: domain/subdomain (not added by default) as first resolver! options.TenantResolvers.Insert(0, new QueryStringTenantResolver()); options.TenantResolvers.Insert(1, new RouteTenantResolver()); options.TenantResolvers.Insert(2, new HeaderTenantResolver()); diff --git a/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DomainTenantResolver.cs b/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DomainTenantResolver.cs new file mode 100644 index 0000000000..07b48fd592 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DomainTenantResolver.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Http; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Text.Formatting; +using Volo.ExtensionMethods; + +namespace Volo.Abp.AspNetCore.MultiTenancy +{ + public class DomainTenantResolver : HttpTenantResolverBase + { + private readonly string _domainFormat; + + public DomainTenantResolver(string domainFormat) + { + _domainFormat = domainFormat.RemovePreFix("http://", "https://"); + } + + protected override string GetTenantIdOrNameFromHttpContextOrNull(ITenantResolveContext context, HttpContext httpContext) + { + var hostName = httpContext.Request.Host.Host.RemovePreFix("http://", "https://"); + var extractResult = FormattedStringValueExtracter.Extract(hostName, _domainFormat, true); + + if (!extractResult.IsMatch) + { + return null; + } + + return extractResult.Matches[0].Value; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyOptionsExtensions.cs b/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyOptionsExtensions.cs new file mode 100644 index 0000000000..5ddb3a9399 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyOptionsExtensions.cs @@ -0,0 +1,12 @@ +using Volo.Abp.AspNetCore.MultiTenancy; + +namespace Volo.Abp.MultiTenancy +{ + public static class MultiTenancyOptionsExtensions + { + public static void AddDomainTenantResolver(this MultiTenancyOptions options, string domainFormat) + { + options.TenantResolvers.Insert(0, new DomainTenantResolver(domainFormat)); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyManager.cs b/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyManager.cs index f99f03531c..edeb7983fa 100644 --- a/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyManager.cs +++ b/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyManager.cs @@ -81,8 +81,6 @@ namespace Volo.Abp.MultiTenancy protected virtual TenantInformation GetCurrentTenantFromResolvers() { - //TODO: Can be optimized by some caching mechanism? - if (!_options.TenantResolvers.Any()) { return null; diff --git a/src/Volo.Abp/Properties/AssemblyInfo.cs b/src/Volo.Abp/Properties/AssemblyInfo.cs index 8f7c032cf8..b1dd0ae8b9 100644 --- a/src/Volo.Abp/Properties/AssemblyInfo.cs +++ b/src/Volo.Abp/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -9,6 +10,8 @@ using System.Runtime.InteropServices; [assembly: AssemblyProduct("Volo.Abp")] [assembly: AssemblyTrademark("")] +[assembly: InternalsVisibleTo("Volo.Abp.Tests")] + // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. diff --git a/src/Volo.Abp/Volo/Abp/NameValue.cs b/src/Volo.Abp/Volo/Abp/NameValue.cs new file mode 100644 index 0000000000..eff71991ba --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/NameValue.cs @@ -0,0 +1,60 @@ +namespace Volo.Abp +{ + /// + /// Can be used to store Name/Value (or Key/Value) pairs. + /// + //[Serializable] + public class NameValue : NameValue + { + /// + /// Creates a new . + /// + public NameValue() + { + + } + + /// + /// Creates a new . + /// + public NameValue(string name, string value) + { + Name = name; + Value = value; + } + } + + /// + /// Can be used to store Name/Value (or Key/Value) pairs. + /// + //[Serializable] + public class NameValue + { + /// + /// Name. + /// + public string Name { get; set; } + + /// + /// Value. + /// + public T Value { get; set; } + + /// + /// Creates a new . + /// + public NameValue() + { + + } + + /// + /// Creates a new . + /// + public NameValue(string name, T value) + { + Name = name; + Value = value; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringToken.cs b/src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringToken.cs new file mode 100644 index 0000000000..af562abf28 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringToken.cs @@ -0,0 +1,15 @@ +namespace Volo.Abp.Text.Formatting +{ + internal class FormatStringToken + { + public string Text { get; private set; } + + public FormatStringTokenType Type { get; private set; } + + public FormatStringToken(string text, FormatStringTokenType type) + { + Text = text; + Type = type; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringTokenType.cs b/src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringTokenType.cs new file mode 100644 index 0000000000..d12f3a72e9 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringTokenType.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.Text.Formatting +{ + internal enum FormatStringTokenType + { + ConstantText, + DynamicValue + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringTokenizer.cs b/src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringTokenizer.cs new file mode 100644 index 0000000000..481a005113 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringTokenizer.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Volo.Abp.Text.Formatting +{ + internal class FormatStringTokenizer + { + public List Tokenize(string format, bool includeBracketsForDynamicValues = false) + { + var tokens = new List(); + + var currentText = new StringBuilder(); + var inDynamicValue = false; + + for (var i = 0; i < format.Length; i++) + { + var c = format[i]; + switch (c) + { + case '{': + if (inDynamicValue) + { + throw new FormatException("Incorrect syntax at char " + i + "! format string can not contain nested dynamic value expression!"); + } + + inDynamicValue = true; + + if (currentText.Length > 0) + { + tokens.Add(new FormatStringToken(currentText.ToString(), FormatStringTokenType.ConstantText)); + currentText.Clear(); + } + + break; + case '}': + if (!inDynamicValue) + { + throw new FormatException("Incorrect syntax at char " + i + "! These is no opening brackets for the closing bracket }."); + } + + inDynamicValue = false; + + if (currentText.Length <= 0) + { + throw new FormatException("Incorrect syntax at char " + i + "! Brackets does not containt any chars."); + } + + var dynamicValue = currentText.ToString(); + if (includeBracketsForDynamicValues) + { + dynamicValue = "{" + dynamicValue + "}"; + } + + tokens.Add(new FormatStringToken(dynamicValue, FormatStringTokenType.DynamicValue)); + currentText.Clear(); + + break; + default: + currentText.Append(c); + break; + } + } + + if (inDynamicValue) + { + throw new FormatException("There is no closing } char for an opened { char."); + } + + if (currentText.Length > 0) + { + tokens.Add(new FormatStringToken(currentText.ToString(), FormatStringTokenType.ConstantText)); + } + + return tokens; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Text/Formatting/FormattedStringValueExtracter.cs b/src/Volo.Abp/Volo/Abp/Text/Formatting/FormattedStringValueExtracter.cs new file mode 100644 index 0000000000..6008836501 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Text/Formatting/FormattedStringValueExtracter.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Volo.ExtensionMethods.Collections.Generic; + +namespace Volo.Abp.Text.Formatting +{ + /// + /// This class is used to extract dynamic values from a formatted string. + /// It works as reverse of + /// + /// + /// Say that str is "My name is Neo." and format is "My name is {name}.". + /// Then Extract method gets "Neo" as "name". + /// + public class FormattedStringValueExtracter + { + /// + /// Extracts dynamic values from a formatted string. + /// + /// String including dynamic values + /// Format of the string + /// True, to search case-insensitive. + public static ExtractionResult Extract(string str, string format, bool ignoreCase = false) + { + var stringComparison = ignoreCase + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + + if (str == format) + { + return new ExtractionResult(true); + } + + var formatTokens = new FormatStringTokenizer().Tokenize(format); + if (formatTokens.IsNullOrEmpty()) + { + return new ExtractionResult(str == ""); + } + + var result = new ExtractionResult(true); + + for (var i = 0; i < formatTokens.Count; i++) + { + var currentToken = formatTokens[i]; + var previousToken = i > 0 ? formatTokens[i - 1] : null; + + if (currentToken.Type == FormatStringTokenType.ConstantText) + { + if (i == 0) + { + if (!str.StartsWith(currentToken.Text, stringComparison)) + { + result.IsMatch = false; + return result; + } + + str = str.Substring(currentToken.Text.Length); + } + else + { + var matchIndex = str.IndexOf(currentToken.Text, stringComparison); + if (matchIndex < 0) + { + result.IsMatch = false; + return result; + } + + Debug.Assert(previousToken != null, "previousToken can not be null since i > 0 here"); + + result.Matches.Add(new NameValue(previousToken.Text, str.Substring(0, matchIndex))); + str = str.Substring(matchIndex + currentToken.Text.Length); + } + } + } + + var lastToken = formatTokens.Last(); + if (lastToken.Type == FormatStringTokenType.DynamicValue) + { + result.Matches.Add(new NameValue(lastToken.Text, str)); + } + + return result; + } + + /// + /// Checks if given fits to given . + /// Also gets extracted values. + /// + /// String including dynamic values + /// Format of the string + /// Array of extracted values if matched + /// True, to search case-insensitive + /// True, if matched. + public static bool IsMatch(string str, string format, out string[] values, bool ignoreCase = false) + { + var result = Extract(str, format, ignoreCase); + if (!result.IsMatch) + { + values = new string[0]; + return false; + } + + values = result.Matches.Select(m => m.Value).ToArray(); + return true; + } + + /// + /// Used as return value of method. + /// + public class ExtractionResult + { + /// + /// Is fully matched. + /// + public bool IsMatch { get; set; } + + /// + /// List of matched dynamic values. + /// + public List Matches { get; private set; } + + internal ExtractionResult(bool isMatch) + { + IsMatch = isMatch; + Matches = new List(); + } + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/ExtensionMethods/StringExtensions.cs b/src/Volo.Abp/Volo/ExtensionMethods/StringExtensions.cs index a95f31963a..a2878fbf0d 100644 --- a/src/Volo.Abp/Volo/ExtensionMethods/StringExtensions.cs +++ b/src/Volo.Abp/Volo/ExtensionMethods/StringExtensions.cs @@ -116,6 +116,18 @@ namespace Volo.ExtensionMethods /// one or more postfix. /// Modified string or the same string if it has not any of given postfixes public static string RemovePostFix(this string str, params string[] postFixes) + { + return str.RemovePostFix(StringComparison.Ordinal, postFixes); + } + + /// + /// Removes first occurrence of the given postfixes from end of the given string. + /// + /// The string. + /// String comparison type + /// one or more postfix. + /// Modified string or the same string if it has not any of given postfixes + public static string RemovePostFix(this string str, StringComparison comparisonType, params string[] postFixes) { if (str.IsNullOrEmpty()) { @@ -129,7 +141,7 @@ namespace Volo.ExtensionMethods foreach (var postFix in postFixes) { - if (str.EndsWith(postFix)) + if (str.EndsWith(postFix, comparisonType)) { return str.Left(str.Length - postFix.Length); } @@ -145,6 +157,18 @@ namespace Volo.ExtensionMethods /// one or more prefix. /// Modified string or the same string if it has not any of given prefixes public static string RemovePreFix(this string str, params string[] preFixes) + { + return str.RemovePreFix(StringComparison.Ordinal, preFixes); + } + + /// + /// Removes first occurrence of the given prefixes from beginning of the given string. + /// + /// The string. + /// String comparison type + /// one or more prefix. + /// Modified string or the same string if it has not any of given prefixes + public static string RemovePreFix(this string str, StringComparison comparisonType, params string[] preFixes) { if (str.IsNullOrEmpty()) { @@ -158,7 +182,7 @@ namespace Volo.ExtensionMethods foreach (var preFix in preFixes) { - if (str.StartsWith(preFix)) + if (str.StartsWith(preFix, comparisonType)) { return str.Right(str.Length - preFix.Length); } diff --git a/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/App/Startup.cs b/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/App/Startup.cs index 1af71d75e8..18e9dc26d7 100644 --- a/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/App/Startup.cs +++ b/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/App/Startup.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Volo.Abp.MultiTenancy; namespace Volo.Abp.AspNetCore.App { @@ -10,6 +11,11 @@ namespace Volo.Abp.AspNetCore.App public void ConfigureServices(IServiceCollection services) { services.AddApplication(); + + services.Configure(options => + { + options.AddDomainTenantResolver("{0}.abp.io"); + }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) diff --git a/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Tests.cs b/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Tests.cs index 76008443ca..6c617b9f66 100644 --- a/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Tests.cs +++ b/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Tests.cs @@ -63,6 +63,22 @@ namespace Volo.Abp.AspNetCore.MultiTenancy result["TenantId"].ShouldBe(_testTenantId.ToString()); } + [Fact] + public async Task Should_Use_Domain_If_Specified() + { + var result = await GetResponseAsObjectAsync>("http://acme.abp.io"); + result["TenantId"].ShouldBe(_testTenantId.ToString()); + } + + [Fact] + public async Task Should_Use_Domain_As_First_Priority_If_Specified() + { + Client.DefaultRequestHeaders.Add(_options.TenantIdKey, Guid.NewGuid().ToString()); + + var result = await GetResponseAsObjectAsync>("http://acme.abp.io"); + result["TenantId"].ShouldBe(_testTenantId.ToString()); + } + [Fact] public async Task Should_Use_Cookie_Tenant_Id_If_Specified() { diff --git a/test/Volo.Abp.Tests/Volo/Abp/Text/Formatting/FormattedStringTokenizer_Test.cs b/test/Volo.Abp.Tests/Volo/Abp/Text/Formatting/FormattedStringTokenizer_Test.cs new file mode 100644 index 0000000000..4a69a01f6a --- /dev/null +++ b/test/Volo.Abp.Tests/Volo/Abp/Text/Formatting/FormattedStringTokenizer_Test.cs @@ -0,0 +1,56 @@ +using System; +using Shouldly; +using Volo.ExtensionMethods.Collections.Generic; +using Xunit; + +namespace Volo.Abp.Text.Formatting +{ + public class FormattedStringTokenizer_Test + { + [Fact] + public void Should_Throw_FormatException_For_Invalid_Format() + { + Assert.Throws(() => new FormatStringTokenizer().Tokenize("a sample { wrong format")); + Assert.Throws(() => new FormatStringTokenizer().Tokenize("a sample {0{1}} wrong format")); + Assert.Throws(() => new FormatStringTokenizer().Tokenize("} wrong format")); + Assert.Throws(() => new FormatStringTokenizer().Tokenize("wrong {} format")); + } + + [Fact] + public void Should_Tokenize_For_Valid_Format() + { + TokenizeTest(""); + TokenizeTest("a sample {0} value", "a sample ", "{0}", " value"); + TokenizeTest("{0} is {name} at this {1}.", "{0}", " is ", "{name}", " at this ", "{1}", "."); + } + + private void TokenizeTest(string format, params string[] expectedTokens) + { + var actualTokens = new FormatStringTokenizer().Tokenize(format); + if (expectedTokens.IsNullOrEmpty()) + { + actualTokens.Count.ShouldBe(0); + return; + } + + actualTokens.Count.ShouldBe(expectedTokens.Length); + + for (var i = 0; i < actualTokens.Count; i++) + { + var actualToken = actualTokens[i]; + var expectedToken = expectedTokens[i]; + + actualToken.Text.ShouldBe(expectedToken.Trim('{', '}')); + + if (expectedToken.StartsWith("{") && expectedToken.EndsWith("}")) + { + actualToken.Type.ShouldBe(FormatStringTokenType.DynamicValue); + } + else + { + actualToken.Type.ShouldBe(FormatStringTokenType.ConstantText); + } + } + } + } +} diff --git a/test/Volo.Abp.Tests/Volo/Abp/Text/Formatting/FormattedStringValueExtracter_Tests.cs b/test/Volo.Abp.Tests/Volo/Abp/Text/Formatting/FormattedStringValueExtracter_Tests.cs new file mode 100644 index 0000000000..c69678d8b6 --- /dev/null +++ b/test/Volo.Abp.Tests/Volo/Abp/Text/Formatting/FormattedStringValueExtracter_Tests.cs @@ -0,0 +1,91 @@ +using Shouldly; +using Xunit; + +namespace Volo.Abp.Text.Formatting +{ + public class FormattedStringValueExtracter_Tests + { + [Fact] + public void Test_Matched() + { + Test_Matched( + "My name is Neo.", + "My name is {0}.", + new NameValue("0", "Neo") + ); + + Test_Matched( + "User halil does not exist.", + "User {0} does not exist.", + new NameValue("0", "halil") + ); + + Test_Matched( + "abp.io", + "{domain}", + new NameValue("domain", "abp.io") + ); + } + + [Fact] + public void Test_Not_Matched() + { + Test_Not_Matched( + "My name is Neo.", + "My name is Marry." + ); + + Test_Not_Matched( + "Role {0} does not exist.", + "User name {0} is invalid, can only contain letters or digits." + ); + + Test_Not_Matched( + "{0} cannot be null or empty.", + "Incorrect password." + ); + + Test_Not_Matched( + "Incorrect password.", + "{0} cannot be null or empty." + ); + } + + [Fact] + public void IsMatch_Test() + { + string[] values; + FormattedStringValueExtracter.IsMatch("User halil does not exist.", "User {0} does not exist.", out values).ShouldBe(true); + values[0].ShouldBe("halil"); + } + + private static void Test_Matched(string str, string format, params NameValue[] expectedPairs) + { + var result = FormattedStringValueExtracter.Extract(str, format); + result.IsMatch.ShouldBe(true); + + if (expectedPairs == null) + { + result.Matches.Count.ShouldBe(0); + return; + } + + result.Matches.Count.ShouldBe(expectedPairs.Length); + + for (int i = 0; i < expectedPairs.Length; i++) + { + var actualMatch = result.Matches[i]; + var expectedPair = expectedPairs[i]; + + actualMatch.Name.ShouldBe(expectedPair.Name); + actualMatch.Value.ShouldBe(expectedPair.Value); + } + } + + private void Test_Not_Matched(string str, string format) + { + var result = FormattedStringValueExtracter.Extract(str, format); + result.IsMatch.ShouldBe(false); + } + } +} \ No newline at end of file diff --git a/test/Volo.Abp.Tests/Volo/ExtensionMethods/StringExtensions_Tests.cs b/test/Volo.Abp.Tests/Volo/ExtensionMethods/StringExtensions_Tests.cs index c2d8d302fe..41788436fd 100644 --- a/test/Volo.Abp.Tests/Volo/ExtensionMethods/StringExtensions_Tests.cs +++ b/test/Volo.Abp.Tests/Volo/ExtensionMethods/StringExtensions_Tests.cs @@ -170,6 +170,9 @@ namespace Volo.ExtensionMethods "MyTestAppService".RemovePostFix("AppService", "Service").ShouldBe("MyTest"); "MyTestAppService".RemovePostFix("Service", "AppService").ShouldBe("MyTestApp"); + //Ignore case + "TestString".RemovePostFix(StringComparison.OrdinalIgnoreCase, "string").ShouldBe("Test"); + //Unmatched case "MyTestAppService".RemovePostFix("Unmatched").ShouldBe("MyTestAppService"); } @@ -179,6 +182,9 @@ namespace Volo.ExtensionMethods { "Home.Index".RemovePreFix("NotMatchedPostfix").ShouldBe("Home.Index"); "Home.About".RemovePreFix("Home.").ShouldBe("About"); + + //Ignore case + "Https://abp.io".RemovePreFix(StringComparison.OrdinalIgnoreCase, "https://").ShouldBe("abp.io"); } [Fact]