We're exploring the same feature. Did you ever make any progress on this @CC.P3608?
Here is the code we are using to setup the SqlServerStorage:
var optServiceBusQueue = ConfigurationManager.AppSettings[AppSettings.Hangfire.ServiceBusQueue];
var optServiceBusQueuePrefix = ConfigurationManager.AppSettings[AppSettings.Hangfire.ServiceBusQueuePrefix];
var sqlStorage = new SqlServerStorage("Default");
var connectionString = ConfigurationManager.ConnectionStrings["Microsoft.ServiceBus.ConnectionString"];
int timeToLiveDays = 14;
Action<QueueDescription> configureAction = qd =>
{
qd.MaxSizeInMegabytes = 5120;
qd.DefaultMessageTimeToLive = new TimeSpan(timeToLiveDays, 0, 0, 0);
};
sqlStorage.UseServiceBusQueues(new ServiceBusQueueOptions
{
ConnectionString = connectionString.ConnectionString,
Configure = configureAction,
// The actual queues used in Azure will have this prefix if specified
// (e.g. the "default" queue will be created as "my-prefix-default")
//
// This can be useful in development environments particularly where the machine
// name could be used to separate individual developers machines automatically
// (i.e. "my-prefix-{machine-name}".Replace("{machine-name}", Environment.MachineName))
QueuePrefix = optServiceBusQueuePrefix,
// The queues to monitor. This *must* be specified, even to set just
// the default queue as done here
Queues = new[] { EnqueuedState.DefaultQueue, optServiceBusQueue },
// By default queues will be checked and created on startup. This option
// can be disabled if the application will only be sending / listening to
// the queue and you want to remove the 'Manage' permission from the shared
// access policy.
//
// Note that the dashboard *must* have the 'Manage' permission otherwise the
// queue length cannot be read
CheckAndCreateQueues = true
});
Configuration.BackgroundJobs.UseHangfire(configuration =>
{
configuration.GlobalConfiguration.UseStorage(sqlStorage);
var autoRetry = new AutomaticRetryAttribute
{
Attempts = retryCount,
OnAttemptsExceeded = attemptsExceededAction
};
configuration.GlobalConfiguration.UseFilter(autoRetry);
});
That seemed to work. So I presume the template will need to have the package.json updated so it will build correctly out of the gate? Or is this something specific to my machine?
Halil,
In context, there is an interceptor to apply parameters to a data filter based on the user context. Everything in the WebAPI works well, but file download via WebAPI is not really great, so we created an MVC action. In order to ensure/enforce proper security, the data filters must be set. We could certainly apply them manually in the controller action, but the thought was to get part of the record through the AppService pipeline in hopes it would be automatic.
public ActionResult Download(int recordId)
{
var record = _appService.Get({id = recordId}); // Interceptor is not hit
var pdf = renderPdf(record);
return File(pdf);
}
I tried to look through the code to see how the interceptor is executed, but I did not find anything obvious. I presume it is registered somewhere in the IocManager, but never found it.
Thanks, Jeff
If the goal is to filter database requests based on the subdomain, then you can use an interceptor on the application services to set the tenant ID on the unit of work.
public class SubdomainTenantInterceptor : IInterceptor
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
public SubdomainTenantInterceptor(IUnitOfWorkManager unitOfWorkManager)
{
_unitOfWorkManager = unitOfWorkManager;
}
public void Intercept(IInvocation invocation)
{
if (_unitOfWorkManager.Current != null)
{
if (HttpContext.Current.Request.Url.HostNameType == UriHostNameType.Dns)
{
var cacheManager = IocManager.Instance.Resolve<ICacheManager>();
var tenantCache = cacheManager.GetCache<string, Tenant>("TenantCache");
// Try to get tenant name from sub-domain
string tenantName = HttpContext.Current.Request.Url.Host.Split('.')[0];
Tenant t = tenantCache.Get(tenantName,
() =>
{
// attempt to look up
return AsyncContext.Run<Tenant>(() => GetTenant(tenantName));
});
if (t != null)
{
// Set tenant ID
_unitOfWorkManager.Current.SetTenantId(t.Id);
}
}
}
invocation.Proceed();
}
private async Task<Tenant> GetTenant(string tenantName)
{
TenantManager manager = IocManager.Instance.Resolve<TenantManager>();
Tenant t = await manager.FindByTenancyNameAsync(tenantName);
return t;
}
}
and register it in the WebModule
public override void PreInitialize()
{
//...
Configuration.IocManager.IocContainer.Kernel.ComponentRegistered += Kernel_ComponentRegistered;
}
private void Kernel_ComponentRegistered(string key, Castle.MicroKernel.IHandler handler)
{
if (typeof(IApplicationService).IsAssignableFrom(handler.ComponentModel.Implementation))
{
handler.ComponentModel.Interceptors.Add
(new InterceptorReference(typeof(SubdomainTenantInterceptor)));
}
}
For unauthenticated requests, you may also need this information on the client side. In that case, modify the SessionAppService.
public async Task<GetCurrentLoginInformationsOutput> GetCurrentLoginInformations()
{
var output = new GetCurrentLoginInformationsOutput
{
User = (await GetCurrentUserAsync()).MapTo<UserLoginInfoDto>()
};
if (AbpSession.TenantId.HasValue)
{
output.Tenant = (await GetCurrentTenantAsync()).MapTo<TenantLoginInfoDto>();
}
else
{
int? tenantId = _unitOfWorkManager.Current.GetTenantId();
if (tenantId.HasValue)
{
output.Tenant = (await TenantManager.GetByIdAsync(tenantId.Value)).MapTo<TenantLoginInfoDto>();
}
}
return output;
}
If using the default header.js, then you'll need to add additional checks to avoid null references.
vm.getShownUserName = function () {
if (!abp.multiTenancy.isEnabled) {
return appSession.user.userName;
} else {
if (appSession.tenant && appSession.user) {
return appSession.tenant.tenancyName + '\\' + appSession.user.userName;
} else {
if (appSession.user)
return '.\\' + appSession.user.userName;
else
return '';
}
}
};