Base solution for your next web application

Activities of "jefftindall"

We have some maturing products that could benefit from enhanced ApplicationService methods. We would like to use ApiVersioning to maintain backwards compability. Are there any best practices for applying this to ABP concepts? My Google-fu hasn't been strong on this one (mostly finding results focused on MVC and Swagger rather than dynamic APIs via IAppService pattern).

Cheers, Jeff

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

We currently have an application utilizing Hangfire with all jobs going to the "default" queue. We are attempting to utilize multiple queues now and are having no luck at this point.

The class that we want to defer to a different queue is decorated with the new queue name and we've added the following code to our App_Start\Startup.cs file:

app.UseHangfireServer(new BackgroundJobServerOptions
{
    Queues = new string[] { "default", "secondary" }
});

We are configuring our JobStorage in the PreInitialize function of the Abp startup module.

When we launch the application with this configuration, we receive the following errors in the log:

DEBUG 2018-08-09 14:31:51,751 [c94ad] Hangfire.Server.Worker - Background process 'Worker #5f7c94ad' started. DEBUG 2018-08-09 14:31:51,777 [28638] Hangfire.Server.Worker - Background process 'Worker #0f928638' started. DEBUG 2018-08-09 14:31:51,796 [c94ad] Hangfire.Server.Worker - Error occurred during execution of 'Worker #5f7c94ad' process. Execution will be retried (attempt #1) in 00:00:00 seconds. System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Transactions.TransactionException: The operation is not valid for the state of the transaction. ---> System.Transactions.TransactionPromotionException: Failure while attempting to promote transaction. ---> System.Data.SqlClient.SqlException: There is already an open DataReader associated with this Command which must be closed first. ---> System.ComponentModel.Win32Exception: The wait operation timed out --- End of inner exception stack trace ---

Additionally we are unable to see the new queue in the Hangfire dashboard, so it's obviously not working.

We had a thought that moving the JobStorage initilization into the Startup class, but when we do that I get: 'JobStorage.Current property value has not been initialized. You must set it before using Hangfire Client or Server API.'

Commenting out the UseHangfireServer configuration above will allow all default queue processing to work as expected.

Has anyone had any success getting multiple queues working with ASP.NET Zero/ASP.NET Boilerplate/Hangfire?

Question

I am trying to refresh the settings cache to "discover" a new setting without having to recycle the entire site, but can't seem to find a reasonable way to do so in the framework.

I believe that if I were to be able to call SettingDefinitionManager.Initialize(), then it would cause the new definitions to be discovered and included. Am I missing anything else that is obvious?

Thanks, Jeff

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?

I am having issues deploying an .NET Core/Angular2 template site and decided to go back to square one. I downloaded a clean template and then tried to build it out of the gate and I'm still running into issues. I'm sure it is something simple, but I've check all of the versions I can think of and am stuck. Versions and console output below. Any thoughts?

Visual Studio: 15.3.5 .NET Core SDK: 2.0.3 NodeJS: 6.9.5 TypeScript: 2.3.4.0 angular-cli: 1.5.5 npm: 4.3.1 yarn: 1.3.2

First error in log file: ERROR in Error at C:/dev/DockerTest/angular/node_modules/ngx-bootstrap/datepicker/daypicker.component.d.ts.DayPickerComponent.html(53,38): "let-" is only supported on ng-template elements. ("

Log File

Directory: C:\dev\DockerTest\aspnet-core\build


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----        12/5/2017   2:13 AM                outputs
  Restore completed in 55.81 ms for C:\dev\DockerTest\aspnet-core\src\dockertest.Application\dockertest.Application.csproj.
  Restore completed in 60.75 ms for C:\dev\DockerTest\aspnet-core\src\dockertest.Core\dockertest.Core.csproj.
  Restore completed in 70.86 ms for C:\dev\DockerTest\aspnet-core\src\dockertest.Migrator\dockertest.Migrator.csproj.
  Restore completed in 124.86 ms for C:\dev\DockerTest\aspnet-core\src\dockertest.EntityFrameworkCore\dockertest.EntityFrameworkCore.csproj.
  Restore completed in 38.95 ms for C:\dev\DockerTest\aspnet-core\src\dockertest.EntityFrameworkCore\dockertest.EntityFrameworkCore.csproj.
  Restore completed in 47.88 ms for C:\dev\DockerTest\aspnet-core\test\dockertest.Tests\dockertest.Tests.csproj.
  Restore completed in 106.18 ms for C:\dev\DockerTest\aspnet-core\src\dockertest.Web.Core\dockertest.Web.Core.csproj.
  Restore completed in 39.46 ms for C:\dev\DockerTest\aspnet-core\src\dockertest.Web.Host\dockertest.Web.Host.csproj.
  Restore completed in 82.14 ms for C:\dev\DockerTest\aspnet-core\src\dockertest.Web.Host\dockertest.Web.Host.csproj.
  Restore completed in 121.09 ms for C:\dev\DockerTest\aspnet-core\src\dockertest.Web.Host\dockertest.Web.Host.csproj.
Microsoft (R) Build Engine version 15.4.8.50001 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  dockertest.Core -> C:\dev\DockerTest\aspnet-core\src\dockertest.Core\bin\Debug\netcoreapp2.0\dockertest.Core.dll
  dockertest.Application -> C:\dev\DockerTest\aspnet-core\src\dockertest.Application\bin\Debug\netcoreapp2.0\dockertest.Application.dll
  dockertest.EntityFrameworkCore -> C:\dev\DockerTest\aspnet-core\src\dockertest.EntityFrameworkCore\bin\Debug\netcoreapp2.0\dockertest.EntityFrameworkCore.dll
  dockertest.Web.Core -> C:\dev\DockerTest\aspnet-core\src\dockertest.Web.Core\bin\Debug\netcoreapp2.0\dockertest.Web.Core.dll
  dockertest.Web.Host -> C:\dev\DockerTest\aspnet-core\src\dockertest.Web.Host\bin\Debug\netcoreapp2.0\dockertest.Web.Host.dll
  dockertest.Web.Host -> C:\dev\DockerTest\aspnet-core\build\outputs\Host\
yarn install v1.3.2
[1/4] Resolving packages...
success Already up-to-date.
Done in 1.23s.
Your global Angular CLI version (1.5.5) is greater than your local
version (1.5.3). The local Angular CLI version is used.

To disable this warning use "ng set --global warnings.versionMismatch=false".
 10% building modules 6/15 modules 9 active ...flags\dist\sprite\famfamfamDate: 2017-12-05T07:14:32.912Z
Hash: d35a40b3b2c18cde5f0f
Time: 23420ms
chunk {0} styles.0635b02e564d0eab45d1.bundle.css (styles) 429 kB [initial] [rendered]
chunk {1} polyfills.3bc34265385d52184eab.bundle.js (polyfills) 86 bytes [initial] [rendered]
chunk {2} main.e402deade8b026b7d50e.bundle.js (main) 84 bytes [initial] [rendered]
chunk {3} inline.b7cdad47c4c7d798d24b.bundle.js (inline) 1.45 kB [entry] [rendered]

ERROR in Error at C:/dev/DockerTest/angular/node_modules/ngx-bootstrap/datepicker/daypicker.component.d.ts.DayPickerComponent.html(53,38): "let-" is only supported on ng-template elements. ("
  </thead>
  <tbody>
    <template ngFor [ngForOf]="rows" [ERROR ->]let-rowz="$implicit" let-index="index">
      <tr *ngIf="!(datePicker.onlyCurrentMonth && rowz[0].sec")
Error at C:/dev/DockerTest/angular/node_modules/ngx-bootstrap/datepicker/daypicker.component.d.ts.DayPickerComponent.html(53,59): "let-" is only supported on ng-template elements. ("
  </thead>
  <tbody>
    <template ngFor [ngForOf]="rows" let-rowz="$implicit" [ERROR ->]let-index="index">
      <tr *ngIf="!(datePicker.onlyCurrentMonth && rowz[0].secondary && rowz[6].sec")

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 '';
                    }
                }
            };
Showing 1 to 9 of 9 entries