Base solution for your next web application

Activities of "michael.pear"

I wasn't able to find a folder ASP.NET in %AppData%, however, searching for other information on certificates for localhost, I finally found the problem. It appears that a certificate for "localhost" on my development system (virtual machine) had expired (4/3/24). A symptom that this had occurred was a warning message in the MIcrosoft Edge browser that my https//localhost: connection was not private when I started IIExpress from Visual Studio. The expiration date was 5 yrs after I initially setup my development environment for ASPNET Zero.

I was able to get things working with the following steps:

  1. Using "certmgr", delete the expired certificate for localhost from "Personal/Certificates".
  2. "Repair" IIExpress to generate a new certificate for localhost. (See https://stackoverflow.com/questions/20036984/how-do-i-restore-a-missing-iis-express-ssl-certificate)
  3. Delete the applicationhost.config file in the .vs folder of your project directory, and do a Clean/Build to restore it. Browser should now come up with no err when you start the debugger (see https://stackoverflow.com/questions/37352603/localhost-refused-to-connect-error-in-visual-studio).

Now, when I run nswag, everything generates normally.

So long term users, who may not have replaced their development systems or virtual machines and just updated them, you may run into your original development certificate expiring!

In the process of updating my project from 11.4 to 13.0.0, I encountered a problem in generating service-proxies with nswag. I have tried returning to my project @ 11.4, but I am still getting an error, so I can't attribute the problem to the upgrade process. As before this upgrade, i had not had changes to my API requiring regeneration of service proxies for several months, I don't have any comparison point recently that I can say the generation process worked.

Whenever I try running nswag (using refresh.bat), I am getting the exception below. Searching for possible solutions, I tried regenerating the development certificates with

dotnet dev-certs https --clean
dotnet dev-certs https 
dotnet dev-certs https --trust

with no success. I've also run nswag specifying runtime of .net 6.0 and .net 7.0 with no success. (.net 6.0 was original level when using ANZ 11.4).

To further complicate, my Windows configuration has updated numerous times over the past several months, and I"m running at Windows 10 22H2.

I'm looking for any suggestions on how to resolve this so I can finish the angular piece of my update. Has anyone else seen this problem, even in a different context?

Executing file 'C:\Users\micha\source\repos\NexusApps\angular\nswag\service.config.nswag' with variables ''... System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: NotTimeValid at System.Net.Security.SslStream.SendAuthResetSignal(ProtocolToken message, ExceptionDispatchInfo exception) at System.Net.Security.SslStream.CompleteHandshake(SslAuthenticationOptions sslAuthenticationOptions) at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](TIOAdapter adapter, Boolean receiveFirst, Byte[] reAuthenticationData, Boolean isApm) at System.Net.Security.SslStream.ProcessAuthenticationWithTelemetryAsync(Boolean isAsync, Boolean isApm, CancellationToken cancellationToken) at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken) --- End of inner exception stack trace --- at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.AddHttp11ConnectionAsync(HttpRequestMessage request) at System.Threading.Tasks.TaskCompletionSourceWithCancellation1.WaitWithCancellationAsync(CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.GetHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) at System.Net.Http.AuthenticationHelper.SendWithAuthAsync(HttpRequestMessage request, Uri authUri, Boolean async, ICredentials credentials, Boolean preAuthenticate, Boolean isProxyAuth, Boolean doRequestAuth, HttpConnectionPool pool, CancellationToken cancellationToken) at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken) at CallSite.Target(Closure , CallSite , Object ) at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
at NJsonSchema.Infrastructure.DynamicApis.HttpGetAsync(String url, CancellationToken cancellationToken) at NSwag.OpenApiDocument.FromUrlAsync(String url, CancellationToken cancellationToken) in //src/NSwag.Core/OpenApiDocument.cs:line 234 at NSwag.Commands.Generation.FromDocumentCommand.RunAsync() in //src/NSwag.Commands/Commands/Generation/FromDocumentCommand.cs:line 62 at NSwag.Commands.Generation.FromDocumentCommand.RunAsync(CommandLineProcessor processor, IConsoleHost host) in //src/NSwag.Commands/Commands/Generation/FromDocumentCommand.cs:line 53 at NSwag.Commands.NSwagDocumentBase.GenerateSwaggerDocumentAsync() in //src/NSwag.Commands/NSwagDocumentBase.cs:line 275 at NSwag.Commands.NSwagDocument.ExecuteAsync() in //src/NSwag.Commands/NSwagDocument.cs:line 81 at NSwag.Commands.Document.ExecuteDocumentCommand.ExecuteDocumentAsync(IConsoleHost host, String filePath) in //src/NSwag.Commands/Commands/Document/ExecuteDocumentCommand.cs:line 85 at NSwag.Commands.Document.ExecuteDocumentCommand.RunAsync(CommandLineProcessor processor, IConsoleHost host) in //src/NSwag.Commands/Commands/Document/ExecuteDocumentCommand.cs:line 48 at NConsole.CommandLineProcessor.ProcessSingleAsync(String[] args, Object input)
at NConsole.CommandLineProcessor.ProcessAsync(String[] args, Object input) at NSwag.Commands.NSwagCommandProcessor.ProcessAsync(String[] args) in /
/src/NSwag.Commands/NSwagCommandProcessor.cs:line 61node:child_process:965 throw err; ^

Error: Command failed: dotnet "C:\Users\micha\source\repos\NexusApps\angular\node_modules\nswag\bin/binaries/Net60/dotnet-nswag.dll" run /runtime:Net60 at checkExecSyncError (node:child_process:890:11) at Object.execSync (node:child_process:962:15) at C:\Users\micha\source\repos\NexusApps\angular\node_modules\nswag\bin\nswag.js:63:19 at ChildProcess.exithandler (node:child_process:414:7) at ChildProcess.emit (node:events:518:28) at maybeClose (node:internal/child_process:1105:16) at Socket.<anonymous> (node:internal/child_process:457:11) at Socket.emit (node:events:518:28) at Pipe.<anonymous> (node:net:337:12) { status: 4294967295, signal: null, output: [ null, null, null ], pid: 684, stdout: null, stderr: null }`

Yes, that works. Thank you.

For others that may run into this, this is the placeholder in WebUrlServiceBase.TenancyNamePlaceHolder.

Version 11.4.0

Angular client/ASP.Net Core

I am resolving tenants based on the URL accessing the angular client (e.g., https://{TENANT_NAME}.myproject.org ). I do not change any configuration for the ASP.Net Core server to recognize the tenant. This has been working very well and minimizes the configuration changes I need to make when deploying the system.

However, when a user attempts a password reset from the login page, the e-mail sent includes an address based on appsettings.json property App.ClientRootAddress, and does not include the tenant address.

How do I configure the system so that the tenant address frin the Angular client is used instead of the ASP.Net Core appsettings App.ClientRootAddress?

Response from AZ Support's review of my TrainingManagerImporter class found that I had inadvertently included an [AbpAuthorize] attribute on the class, which was the source of the problem. Removing that, and the job now runs. Goes to show how it is easy to be blind to the obvious when reviewing your own code.

In this process, I also refactored to make sure that no ApplicationServices (which require authorized users) were iinjected, so I switched to using domain services for the database functions. I saw that this could be a problem in other postings, such as StackOverflow https://stackoverflow.com/questions/52250802/background-job-fails-with-current-user-did-not-login-to-the-application

I sent the log file (with exception) and the source code to [email protected], rather than post in full here. Note I can't get any more detailed stacktrace than provided above. I can't catch the exception, as it appears to be happening in the BackgroundJob processing. The job is added to the AbpBackgroundJobs, and the exception appears to occur when the system tries to start the job, before my user code (in TrainingManagerImporter) is reached.

Using AZ version 11.4.0, Aspnet Core/Angular project.

I am implementing a file import using a background job, modeled after Web.Controllers.UsersControllerBase.ImportFromExcel(). While the Users import from excel, works without problem, when my implementation is failing on the "BackgroundJobManager.EnqueueAsync" call with the exception:

INFO  2023-02-07 14:50:02,585 [35   ] xusApps.Web.Controllers.ImportController - Training Content Import From Zip  by user 24@12
WARN  2023-02-07 14:50:10,901 [58   ] Abp.BackgroundJobs.BackgroundJobManager  - Current user did not login to the application!
Abp.Authorization.AbpAuthorizationException: Current user did not login to the application!
   at Abp.Authorization.AuthorizationHelper.AuthorizeAsync(IEnumerable`1 authorizeAttributes)
   at Abp.Authorization.AuthorizationHelper.CheckPermissionsAsync(MethodInfo methodInfo, Type type)
   at Abp.Authorization.AuthorizationHelper.AuthorizeAsync(MethodInfo methodInfo, Type type)
   at Abp.Authorization.AuthorizationInterceptor.InternalInterceptAsynchronous(IInvocation invocation)
   at Abp.BackgroundJobs.BackgroundJobManager.TryProcessJobAsync(BackgroundJobInfo jobInfo)

The call to the background job is:

                var jobId = await BackgroundJobManager.EnqueueAsync&lt;TrainingManagerImporter, TrainingManagerImportJobArgs&gt;(new TrainingManagerImportJobArgs
                {
                    TenantId = AbpSession.TenantId,
                    BinaryObjectId = fileObject.Id,
                    ImportType = importType,
                    User = AbpSession.ToUserIdentifier()
                    
                });

I am passing tenant Id and user Id in the background job arguments, but the exception occurs before the ExecuteAsync method in TrainingManagerImporter (which extends AsyncBackgroundJob) is called. I've tried a debug break on the entry statement, and it is never reached (similar breakpoint is reached when using the User ImportUsersToExcelJob) I can't find any place in the ImportUsersToExcel controller code where there is an explicit login as the user, and I am using the same tenant and user for that import successfully. I believe this was working before recently converting my project from single tenant to multitenant mode, and I have many other functions (other than BackgroundJobs) that are working. Also, the controller that the EnqueueAsync is done in is prefaced by an AbpMvcAuthorize, so I expect that to reach the EnqueueAsync, that the user needs to be logged in and has the permission required.

Please advise on what the source of this issue could be and whether there is other information needed to track it down.

I found a solution for finding and using the tenant name in my helper class.

  1. Kept the helper class general by adding a string property that allows changing the container name to include the tenant name.
  2. To resolve the tenant name from either a current session or current unit of work, introduced a static TenantNameHelper class:
using Abp.Domain.Uow;
using Abp.MultiTenancy;
using Abp.Runtime.Session;
using System;


namespace AZProject.Utilities.Helpers
{
    public static class TenantNameHelper
    {
        public static string GetTenantNameFromSession(IAbpSession session, ITenantCache tenantCache)
        {
           
            if (session.TenantId.HasValue)
            {
                return tenantCache.Get(session.TenantId.Value).TenancyName;
            } else
            {
                //If userid has value, then this is host, otherwise not logged in (blank tenanacy)
                return session.UserId.HasValue ? "host": String.Empty;
            }

        }
        public static string GetTenantNameFromUnitOfWork(IActiveUnitOfWork unitOfWork, ITenantCache tenantCache)
        {
            if (unitOfWork.GetTenantId().HasValue)
            {
                return tenantCache.Get(unitOfWork.GetTenantId().Value).TenancyName;
            }
            else
            {
                //Assumes host as tenant
                return "host";
            }
        }

    }
}
  1. Added an injection for ITenantCache ineach of the services where the TenantNameHelper is used. Most were an ApplicationService or Controller as base class, so AbpSession was available.

Haven't fully tested yet, so if you have suggestions on where I might run into problems, let me know.

I have been switching to using multitenancy, and have successfully converted my database connection string resolver to look up a connection string in the app configuration based on the tenant name. I introduced code for tenant atabases (recognized by DbContextConcreteType with my tenant db context) :

        private readonly ITenantCache _tenantCache;
        private readonly ICurrentUnitOfWorkProvider _unitOfWorkProvider;
           ....
             if (args["DbContextConcreteType"] as Type == typeof(TrainingManagerDbContext))
            {
                string tenantName = String.Empty;
                string tenantConnectionStringName = NexusAppsConsts.TrainingManagerSchemaConnectionStringName;
                if (_unitOfWorkProvider.Current.GetTenantId().HasValue)
                {
                    var tenantEntry = _tenantCache.Get(_unitOfWorkProvider.Current.GetTenantId().Value);
                    tenantName = tenantEntry.TenancyName;
                    tenantConnectionStringName = string.IsNullOrEmpty(tenantEntry.TenancyName)? tenantConnectionStringName:tenantConnectionStringName+"."+tenantEntry.TenancyName;
                }
                var tenantDbConnectionString = _appConfiguration.GetConnectionString(tenantConnectionStringName);
                if (string.IsNullOrEmpty(tenantDbConnectionString))
                {
                    throw new UserFriendlyException("NoDatabase", String.Format("No database for Tenant {0}", tenantName));
                }
                return tenantDbConnectionString;
            }
                

This was straightforward to implement, as injecting ITenantCache and ICurrentUnitOfWorkProvider in the ConnectionStringResolver worked without any other changes.

However, I now want to use the tenant name for separating file storage in Azure Storage based on the tenant name by setting up a container with in the storage account that includes the tenancy name. From my initial attempt using the approach above, it may be more complicated because use of ITenantCache and ICurrentUnitOfWorkProvider seem to require modifying the constructors not only of my BlobServicesClient class, but also including injection in the classes using my BlobServicesClient class.

Is there an example of accessing/resolving the tenant name in areas other than for connection string resolution? Currently, BlobServicesClient is just a helper class and doesn't extend a base class (like ApplicaitonService, AbpController, etc.) Would introducing a base class help in resolving the current tenant name? (like through Session to get TenantId and look up name in ITenantCache)

Please advise what direction I should take.

It has been complex in figuring out the multitenancy aspects of AspnetZero, but in the end the amount of change I needed to make to recognize tenant name from sub domain on the Angular client app and resolve to separate tenant database was very little....several days of investigating for ~ 10 lines of code change :-) All of the possible formulations of a multi tenant solution make it difficult to figure out what is applicable to your individual circumstance.

This was an extremely confusing post to address the issue of extending a localization source, which allows overriding existing entries and adding new ones. I wanted to organize things so that what I add for my application is separate from the AspNetZero supplied language files. To summarize the solution, you need to do the following assuming a project "YourProject" is the project that is provided from AspNetZero:

  1. In folder YourProject.Core/Localization, you find a YourProject folder with language files prefixed with "YourProject" (e.g. YourProject.xml, YourProject-de.xml, etc.
  2. You need to add a folder under YourProject.Core/Localization with a different name like "ExtendYourProject".
  3. In the new Folder, for any languages you want to extend, add a file with the SAME name as the language file you are extending (e.g., YourProject-de.xml). The file will contain any entries you want to override and any new entries.
  4. In the language configurer in YourProject.Core/Localization file YourProjectLocalizationConfigurer add the code:
            localizationConfiguration.Sources.Extensions.Add(
                new LocalizationSourceExtensionInfo("YourProject",
                    new XmlEmbeddedFileLocalizationDictionaryProvider(
                        typeof(YourProjectLocalizationConfigurer).GetAssembly(),
                        "YourProject.Localization.ExtendYourProject"
                    )
                 )
            );
           

Rebuild, and you should find that the extensions are now used. You do need to make sure that the "build action" for any files you add are showing "Embedded Resource". Any XML files that I added seem to be given that automatically in visual studio.

Hope that helps makes sense of a rather long involved post for something relatively simple in the end.

Showing 1 to 10 of 31 entries