I'm building a multitenant web application in ASP.net core MVC and I need to create multitenant support for configuration in appsettings.json.
I want my settings to look like this:
{
'Section1': {
'Key1': 'Value1',
'Key2': 'Value2',
}
'Tenants': {
'TenantA': {
'Section1': {
'Key1': 'Value 1 for Tenant A'
}
},
'TenantB': {
'Section1': {
'Key2': 'Value 2 for Tenant B'
}
}
}
}
In other words, the structure of the configuration should be duplicated in Tenants section and this configuration should override the default one.
When I resolve Section1 options using Options pattern (https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-7.0) I want to get these values:
For TenantA: Key1 = Value 1 for Tenant A; Key2 = Value 2
For TenantB: Key1 = Value 1; Key2 = Value 2 for Tenant B
I have my custom ITenantProvider interface which is Scoped service and it provides name of current tenant. It means that every http request can serve different tenant and so the options should be also different every http request.
I've tried to override IOptionsFactory (https://github.com/dotnet/runtime/blob/4aa4d28f951babd9b26c2e4cff99a3203c56aee8/src/libraries/Microsoft.Extensions.Options/src/OptionsFactory.cs) and do some magic there.
public TOptions Create(string name) {
var tenantProvider = _httpContextAccessor?.HttpContext?.RequestServices.GetService<ITenantProvider>();
// I have my current tenant key, but what to do now?
var tenantKey = tenantProvider?.GetTenant()?.Key;
TOptions options = CreateInstance(name);
foreach (IConfigureOptions<TOptions> setup in _setups) {
if (setup is IConfigureNamedOptions<TOptions> namedSetup) {
namedSetup.Configure(name, options);
} else if (name == Options.DefaultName) {
setup.Configure(options);
}
}
foreach (IPostConfigureOptions<TOptions> post in _postConfigures) {
post.PostConfigure(name, options);
}
if (_validations.Length > 0) {
var failures = new List<string>();
foreach (IValidateOptions<TOptions> validate in _validations) {
ValidateOptionsResult result = validate.Validate(name, options);
if (result is not null && result.Failed) {
failures.AddRange(result.Failures);
}
}
if (failures.Count > 0) {
throw new OptionsValidationException(name, typeof(TOptions), failures);
}
}
return options;
}
Actually, I've tried to use this tutorial https://michael-mckenna.com/multi-tenant-asp-dot-net-core-application-tenant-specific-configuration-options but this can only override the configuration using some callback Program.cs which isn't exactly what I need.
Does somebody have any idea how to implement this?
Related
My goal is to check if user is member of specific active directory group.
In .net mvc i was using this code inside my service
HttpContext.Current.Request.LogonUserIdentity.Groups
.Any(x => x.Translate(typeof(NTAccount)).Value == "some role"
and it worked well.
In .net core mvc 2.1.2 i pass IHttpContextAccessor into service constructor and try to use following
_httpAccessor.HttpContext.User.Identity.LogonUserIdentity.Groups
but there is an issue, because Identity does not contains LogonUserIdentity. I tried to find any solution but i was not successful, how can i get the list of user groups or check if user is member of specific one ?
Except using built-in function which check the permission by "Roles", if you want to check by specific AD Group, you can also use below codes :
public static class Security
{
public static bool IsInGroup(this ClaimsPrincipal User, string GroupName)
{
var groups = new List<string>();
var wi = (WindowsIdentity)User.Identity;
if (wi.Groups != null)
{
foreach (var group in wi.Groups)
{
try
{
groups.Add(group.Translate(typeof(NTAccount)).ToString());
}
catch (Exception)
{
// ignored
}
}
return groups.Contains(GroupName);
}
return false;
}
}
And using as:
if (User.IsInGroup("GroupName"))
{
}
I have a single rule that can handle multiple rule calls so I have created a custom attribute that is placed on the rule class. This attribute lists the names it is allowed to process. In structuremap I would like to register this same rule as multiple names by reading the custom attribute.
[RuleIdentifer(new string[] { "RunAction1","RunAction2","RunAction3" })]
I have tried to use the MissingNamedInstanceIs class but run into Bi-Directional dependency error. The following has been placed in the creation of the container after the Scan:
_.For<Rules.IRule>().MissingNamedInstanceIs.ConstructedBy("Pull Rule by Name from Attribute",r =>
{
return r.GetAllInstances<Rules.IRule>().FirstOrDefault<Rules.IRule>(r1 =>
{
var dnAttribute = r1.GetType().GetCustomAttributes(typeof(RuleIdentifer), true).FirstOrDefault() as RuleIdentifer;
if (dnAttribute != null && dnAttribute.Names.Contains<string>(r.RequestedName)) return true;
return true;
});
});
Is there a better way to do this in the scan section NameBy call:
x.AddAllTypesOf<Rules.IRule>().NameBy(t => t.Name);
When ahead and created my own RegistrationConvention. Work as expected now.
public class RuleAttributeConvention : IRegistrationConvention
{
public void ScanTypes(TypeSet types, Registry registry)
{
// Only work on concrete types
types.FindTypes(TypeClassification.Concretes | TypeClassification.Closed).Where(typ => typeof(Rules.IRule).IsAssignableFrom(typ)).ToList().ForEach(t =>
{
var dnAttribute = t.GetCustomAttributes(typeof(RuleIdentifer), true).FirstOrDefault() as RuleIdentifer;
if (null == dnAttribute) return;
foreach (var nm in dnAttribute.Names)
{
registry.For<Rules.IRule>().Use(t.Name).Name = nm;
}
});
}
}
In Swashbuckle there is a setting called OrderActionGroupsBy which is supposed to change the ordering within the API, but nothing I do is working and I'm can't determine whether this is a Swashbuckle problem, or due to my IComparer any idea what I'm doing wrong?
This is setting the configurations
config.EnableSwagger(c =>
{
...
c.OrderActionGroupsBy(new CustomStringComparer());
c.GroupActionsBy(apiDesc => GroupBy(apiDesc));
...
}
This is grouping the actions by type instead of controllerName.
private static string GroupBy(ApiDescription apiDesc)
{
var controllerName = apiDesc.ActionDescriptor.ControllerDescriptor.ControllerName;
var path = apiDesc.RelativePath;
if (controllerName.Contains("Original"))
{
controllerName = controllerName.Replace("Original", "");
}
// Check if it is one of the entities if so group by that
// Otherwise group by controller
var entities = new List<string>() { "Users", "Apps", "Groups" };
var e = entities.Where(x => attr.Contains(x.ToLower())).FirstOrDefault();
if (e != null)
{
return e;
}
return controllerName;
}
This is my attempt at an IComparer I want Users first and then after that alphabetical
class CustomStringComparer : IComparer<string>
{
public int Compare(string x, string y)
{
if (x.CompareTo(y) == 0)
return 0;
if (x.CompareTo("Users") == 0)
return -1;
if (y.CompareTo("Users") == 0)
return 1;
return x.CompareTo(y);
}
}
}
This isn't working it always defaults to alphabetical no matter what I do.
Looks like this is a bug with Swashbuckle/Swagger-ui
Using OrderActionGroupsBy is correctly sorting the JSON file, but then swagger ui automatically resorts this to alphabetical order.
I have filed bugs with both Swashbuckle and swagger-ui since this seems to go against what is said in swagger-ui's doc regarding apisSorter.
Apply a sort to the API/tags list. It can be 'alpha' (sort by name) or
a function (see Array.prototype.sort() to know how sort function
works). Default is the order returned by the server unchanged.
Swashbuckle issue
swagger-ui issue
swagger-ui specific stackoverflow question
I am updating a library that uses Autofac so that, in addition to the original configuration file (registered via Autofac), it can optionally take a function to accomplish that same goal (again, registered via Autofac). The original is something like this:
public MyClass(ConfigFile config = null)
{
this._activatorLoader = a => {
// old config code here...
}
}
The updated version I'd like is:
public MyClass(
Func<Input, IList<Activator>> activatorLoader = null,
ConfigFile config = null)
{
if (activatorLoader != null)
{
this._activatorLoader = activatorLoader;
}
else
{
this._activatorLoader = a => {
// old config code here...
}
}
}
The problem is that Autofac is seeing my request for a list of something and always providing the function. I tried switching to a delegate and get the same problem:
public delegate IList<Activator> ActivatorLoader(Input input);
public MyClass(
ActivatorLoader activatorLoader = null,
ConfigFile config = null)
{
if (activatorLoader != null)
{
this._activatorLoader = activatorLoader;
}
else
{
this._activatorLoader = a => {
// old config code here...
}
}
}
The loading of the activators must still be delayed, I'd like the flexibility of registering any function based on the situation, and old code (without an activator loader registered) should still work. Is there any way to prevent Autofac from autogenerating the Func?
The class will be instantiated through dependency injection in another class. At a later time, the activator loading code will be triggered (if needed).
var myObject = conatiner.Resolve<MyClass>();
// time passes...
myObject.DoActivatorLoading();
The primary goal is to prevent Autofac from creating the Func<Input, IList<Activator>> if it is not explicitly set. Is this possible?
Perhaps, it will be easier to create a separate class for activator loading ?
public class ActivatorsLoader
{
public IList<Activator> Load(Input input)
{
///
}
}
///
public Class(
ActivatorsLoader activatorsLoader = null,
ConfigFile config = null)
{
///
}
But it would be good to see the use cases of your Class.
I am fairly new to asp.net, and have little experience with iis. I would like to have each user of my application get their own sub-domain, but all use the same controllers. The subdomain would then control what content is displayed.
Example:
user1subdomain.mydomain.com/Whatever
user2subdomain.mydomain.com/Whatever
Will both use the same controller. Ideally a parameter could give the user name to the controller, which could then display the appropriate content. I would like it to be flexible enough that new subdomains could be added to the database without rewriting routing rules every time a new subdomain is added.
MVC is not bound to the domain, just to the path (e.g. http://domain/path).
To do this properly you need the following...
Wildcard DNS setup for
*.yourdomain.com pointing to your server.
The site in IIS setup with
no Host Header. Any other sites
hosted in that instance of IIS on
the same IP must have Host headers
specified.
Your application will need to check the
request host header either on page load,
session start or some other event.
I found an easier answer on this person's blog. Very surprised this works as well as it does and that this solution is more than 4 years old.
http://blog.maartenballiauw.be/post/2009/05/20/aspnet-mvc-domain-routing.aspx
A custom route implementation:
public class DomainRoute : Route
{
public string Domain { get; set; }
public override RouteData GetRouteData(HttpContextBase httpContext)
{
// Build regex
domainRegex = CreateRegex(Domain);
pathRegex = CreateRegex(Url);
// Request information
string requestDomain = httpContext.Request.Headers["host"];
if (!string.IsNullOrEmpty(requestDomain))
{
if (requestDomain.IndexOf(":") > 0)
{
requestDomain = requestDomain.Substring(0, requestDomain.IndexOf(":"));
}
}
else
{
requestDomain = httpContext.Request.Url.Host;
}
string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
// Match domain and route
Match domainMatch = domainRegex.Match(requestDomain);
Match pathMatch = pathRegex.Match(requestPath);
// Route data
RouteData data = null;
if (domainMatch.Success && pathMatch.Success)
{
data = new RouteData(this, RouteHandler);
// Add defaults first
if (Defaults != null)
{
foreach (KeyValuePair<string, object> item in Defaults)
{
data.Values[item.Key] = item.Value;
}
}
// Iterate matching domain groups
for (int i = 1; i < domainMatch.Groups.Count; i++)
{
Group group = domainMatch.Groups[i];
if (group.Success)
{
string key = domainRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key] = group.Value;
}
}
}
}
// Iterate matching path groups
for (int i = 1; i < pathMatch.Groups.Count; i++)
{
Group group = pathMatch.Groups[i];
if (group.Success)
{
string key = pathRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key] = group.Value;
}
}
}
}
}
return data;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return base.GetVirtualPath(requestContext, RemoveDomainTokens(values));
}
public DomainData GetDomainData(RequestContext requestContext, RouteValueDictionary values)
{
// Build hostname
string hostname = Domain;
foreach (KeyValuePair<string, object> pair in values)
{
hostname = hostname.Replace("{" + pair.Key + "}", pair.Value.ToString());
}
// Return domain data
return new DomainData
{
Protocol = "http",
HostName = hostname,
Fragment = ""
};
}}
And here is how it can be used.
routes.Add("DomainRoute", new DomainRoute(
"{controller}-{action}.example.com", // Domain with parameters
"{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
));
Mostly not a problem. I think!
In terms of the application/routing the routing starts where the domain ends so mapping multiple domains to the same application is not a problem, that will just work.
In terms of IIS you can map as many domains as you want (well there's bound to be a limit) to a single site - I'm not sure if you can use a wildcard - what version of IIS are you using?
When a request arrives there are events you can hook to look at the domain and hence set up parameters you want (user for example), the root URL for the request is available from the context later in the cycle too - but you'll want to pick it up early.
If you can do wildcards it becomes fairly trivial - pick up the request, validate the subdomain against the users in the database (if not valid redirect to the default site), set the user and carry on through the normal routing.
If you can't do wildcards then the challenge is adding host headers to the IIS application (website) on the fly from your application as users are added to the database.