Browse Source

Resolved #62: Add domain tenant resolver.

pull/81/head
Halil İbrahim Kalkan 10 years ago
parent
commit
9c12a159ba
  1. 1
      src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetMultiTenancyModule.cs
  2. 30
      src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/DomainTenantResolver.cs
  3. 12
      src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyOptionsExtensions.cs
  4. 2
      src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenancyManager.cs
  5. 3
      src/Volo.Abp/Properties/AssemblyInfo.cs
  6. 60
      src/Volo.Abp/Volo/Abp/NameValue.cs
  7. 15
      src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringToken.cs
  8. 8
      src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringTokenType.cs
  9. 78
      src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringTokenizer.cs
  10. 131
      src/Volo.Abp/Volo/Abp/Text/Formatting/FormattedStringValueExtracter.cs
  11. 28
      src/Volo.Abp/Volo/ExtensionMethods/StringExtensions.cs
  12. 6
      test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/App/Startup.cs
  13. 16
      test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo/Abp/AspNetCore/MultiTenancy/AspNetCoreMultiTenancy_Tests.cs
  14. 56
      test/Volo.Abp.Tests/Volo/Abp/Text/Formatting/FormattedStringTokenizer_Test.cs
  15. 91
      test/Volo.Abp.Tests/Volo/Abp/Text/Formatting/FormattedStringValueExtracter_Tests.cs
  16. 6
      test/Volo.Abp.Tests/Volo/ExtensionMethods/StringExtensions_Tests.cs

1
src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetMultiTenancyModule.cs

@ -11,7 +11,6 @@ namespace Volo.Abp.AspNetCore.MultiTenancy
{
services.Configure<MultiTenancyOptions>(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());

30
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;
}
}
}

12
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));
}
}
}

2
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;

3
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.

60
src/Volo.Abp/Volo/Abp/NameValue.cs

@ -0,0 +1,60 @@
namespace Volo.Abp
{
/// <summary>
/// Can be used to store Name/Value (or Key/Value) pairs.
/// </summary>
//[Serializable]
public class NameValue : NameValue<string>
{
/// <summary>
/// Creates a new <see cref="NameValue"/>.
/// </summary>
public NameValue()
{
}
/// <summary>
/// Creates a new <see cref="NameValue"/>.
/// </summary>
public NameValue(string name, string value)
{
Name = name;
Value = value;
}
}
/// <summary>
/// Can be used to store Name/Value (or Key/Value) pairs.
/// </summary>
//[Serializable]
public class NameValue<T>
{
/// <summary>
/// Name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Value.
/// </summary>
public T Value { get; set; }
/// <summary>
/// Creates a new <see cref="NameValue"/>.
/// </summary>
public NameValue()
{
}
/// <summary>
/// Creates a new <see cref="NameValue"/>.
/// </summary>
public NameValue(string name, T value)
{
Name = name;
Value = value;
}
}
}

15
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;
}
}
}

8
src/Volo.Abp/Volo/Abp/Text/Formatting/FormatStringTokenType.cs

@ -0,0 +1,8 @@
namespace Volo.Abp.Text.Formatting
{
internal enum FormatStringTokenType
{
ConstantText,
DynamicValue
}
}

78
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<FormatStringToken> Tokenize(string format, bool includeBracketsForDynamicValues = false)
{
var tokens = new List<FormatStringToken>();
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;
}
}
}

131
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
{
/// <summary>
/// This class is used to extract dynamic values from a formatted string.
/// It works as reverse of <see cref="string.Format(string,object)"/>
/// </summary>
/// <example>
/// Say that str is "My name is Neo." and format is "My name is {name}.".
/// Then Extract method gets "Neo" as "name".
/// </example>
public class FormattedStringValueExtracter
{
/// <summary>
/// Extracts dynamic values from a formatted string.
/// </summary>
/// <param name="str">String including dynamic values</param>
/// <param name="format">Format of the string</param>
/// <param name="ignoreCase">True, to search case-insensitive.</param>
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;
}
/// <summary>
/// Checks if given <see cref="str"/> fits to given <see cref="format"/>.
/// Also gets extracted values.
/// </summary>
/// <param name="str">String including dynamic values</param>
/// <param name="format">Format of the string</param>
/// <param name="values">Array of extracted values if matched</param>
/// <param name="ignoreCase">True, to search case-insensitive</param>
/// <returns>True, if matched.</returns>
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;
}
/// <summary>
/// Used as return value of <see cref="Extract"/> method.
/// </summary>
public class ExtractionResult
{
/// <summary>
/// Is fully matched.
/// </summary>
public bool IsMatch { get; set; }
/// <summary>
/// List of matched dynamic values.
/// </summary>
public List<NameValue> Matches { get; private set; }
internal ExtractionResult(bool isMatch)
{
IsMatch = isMatch;
Matches = new List<NameValue>();
}
}
}
}

28
src/Volo.Abp/Volo/ExtensionMethods/StringExtensions.cs

@ -116,6 +116,18 @@ namespace Volo.ExtensionMethods
/// <param name="postFixes">one or more postfix.</param>
/// <returns>Modified string or the same string if it has not any of given postfixes</returns>
public static string RemovePostFix(this string str, params string[] postFixes)
{
return str.RemovePostFix(StringComparison.Ordinal, postFixes);
}
/// <summary>
/// Removes first occurrence of the given postfixes from end of the given string.
/// </summary>
/// <param name="str">The string.</param>
/// <param name="comparisonType">String comparison type</param>
/// <param name="postFixes">one or more postfix.</param>
/// <returns>Modified string or the same string if it has not any of given postfixes</returns>
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
/// <param name="preFixes">one or more prefix.</param>
/// <returns>Modified string or the same string if it has not any of given prefixes</returns>
public static string RemovePreFix(this string str, params string[] preFixes)
{
return str.RemovePreFix(StringComparison.Ordinal, preFixes);
}
/// <summary>
/// Removes first occurrence of the given prefixes from beginning of the given string.
/// </summary>
/// <param name="str">The string.</param>
/// <param name="comparisonType">String comparison type</param>
/// <param name="preFixes">one or more prefix.</param>
/// <returns>Modified string or the same string if it has not any of given prefixes</returns>
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);
}

6
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<AppModule>();
services.Configure<MultiTenancyOptions>(options =>
{
options.AddDomainTenantResolver("{0}.abp.io");
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)

16
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<Dictionary<string, string>>("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<Dictionary<string, string>>("http://acme.abp.io");
result["TenantId"].ShouldBe(_testTenantId.ToString());
}
[Fact]
public async Task Should_Use_Cookie_Tenant_Id_If_Specified()
{

56
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<FormatException>(() => new FormatStringTokenizer().Tokenize("a sample { wrong format"));
Assert.Throws<FormatException>(() => new FormatStringTokenizer().Tokenize("a sample {0{1}} wrong format"));
Assert.Throws<FormatException>(() => new FormatStringTokenizer().Tokenize("} wrong format"));
Assert.Throws<FormatException>(() => 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);
}
}
}
}
}

91
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);
}
}
}

6
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]

Loading…
Cancel
Save